@@ -21,6 +21,7 @@ import (
21
21
"os"
22
22
"path/filepath"
23
23
24
+ "github.com/pkg/errors"
24
25
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25
26
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26
27
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -39,33 +40,38 @@ import (
39
40
var (
40
41
commandKind = flag .String (
41
42
"command-kind" ,
42
- "Command" ,
43
- "Kind of Command custom resource" ,
43
+ "Command" , // default
44
+ "Kubernetes custom resource kind " ,
44
45
)
46
+
45
47
commandResource = flag .String (
46
48
"command-resource" ,
47
- "commands" ,
48
- "Resource name of Command custom resource" ,
49
+ "commands" , // default
50
+ "Kubernetes custom resource name " ,
49
51
)
52
+
50
53
commandGroup = flag .String (
51
54
"command-group" ,
52
- "dope.metacontroller.io" ,
53
- "Group of Command custom resource" ,
55
+ "dope.metacontroller.io" , // default
56
+ "Kubernetes custom resource group " ,
54
57
)
58
+
55
59
commandVersion = flag .String (
56
- "command-version" ,
57
- "v1" ,
58
- "Version of Command custom resource" ,
60
+ "command-api- version" ,
61
+ "v1" , // default
62
+ "Kubernetes custom resource api version " ,
59
63
)
64
+
60
65
commandName = flag .String (
61
66
"command-name" ,
62
67
"" ,
63
- "Name of Command custom resource " ,
68
+ "Name of the command " ,
64
69
)
70
+
65
71
commandNamespace = flag .String (
66
72
"command-ns" ,
67
73
"" ,
68
- "Namespace of Command custom resource " ,
74
+ "Namespace of the command " ,
69
75
)
70
76
71
77
kubeAPIServerURL = flag .String (
@@ -74,24 +80,29 @@ var (
74
80
`Kubernetes api server url (same format as used by kubectl).
75
81
If not specified, uses in-cluster config` ,
76
82
)
83
+
77
84
kubeconfig * string
78
85
79
86
clientGoQPS = flag .Float64 (
80
87
"client-go-qps" ,
81
- 5 ,
88
+ 5 , // default
82
89
"Number of queries per second client-go is allowed to make (default 5)" ,
83
90
)
91
+
84
92
clientGoBurst = flag .Int (
85
93
"client-go-burst" ,
86
- 10 ,
94
+ 10 , // default
87
95
"Allowed burst queries for client-go (default 10)" ,
88
96
)
89
97
)
90
98
91
- // main function is the entry point of this binary.
99
+ // This is the entry point of this binary
92
100
//
93
101
// This binary is meant to be run to completion. In other
94
102
// words this does not expose any long running service.
103
+ // This executes the commands or scripts specified in the
104
+ // custom resource and updates this resource post execution.
105
+ //
95
106
//
96
107
// NOTE:
97
108
// A kubernetes **Job** can make use of this binary
@@ -127,45 +138,71 @@ func main() {
127
138
defer klog .Flush ()
128
139
129
140
if * commandName == "" {
130
- klog .Fatal ("Invalid arguments: Flag 'command-name' must be set" )
141
+ klog .Exit ("Invalid arguments: Flag 'command-name' must be set" )
142
+ }
143
+ if * commandNamespace == "" {
144
+ klog .Exit ("Invalid arguments: Flag 'command-ns' must be set" )
131
145
}
132
146
133
- klog .V (1 ).Infof ("Command custom resource: kind %s " , * commandKind )
134
- klog .V (1 ).Infof ("Command custom resource: resource %s " , * commandResource )
135
- klog .V (1 ).Infof ("Command custom resource: group %s " , * commandGroup )
136
- klog .V (1 ).Infof ("Command custom resource: version %s " , * commandVersion )
137
- klog .V (1 ).Infof ("Command custom resource: name %s " , * commandName )
138
- klog .V (1 ).Infof ("Command custom resource: namespace %s " , * commandNamespace )
147
+ klog .V (1 ).Infof ("Command custom resource: group %q " , * commandGroup )
148
+ klog .V (1 ).Infof ("Command custom resource: version %q " , * commandVersion )
149
+ klog .V (1 ).Infof ("Command custom resource: kind %q " , * commandKind )
150
+ klog .V (1 ).Infof ("Command custom resource: resource %q " , * commandResource )
151
+ klog .V (1 ).Infof ("Command custom resource: namespace %q " , * commandNamespace )
152
+ klog .V (1 ).Infof ("Command custom resource: name %q " , * commandName )
139
153
140
- runCommand (getRestConfig ())
154
+ r , err := NewRunner ()
155
+ if err != nil {
156
+ // This should lead to crashloopback if this
157
+ // is running from within a Kubernetes pod
158
+ klog .Exit (err )
159
+ }
160
+ err = r .Run ()
161
+ if err != nil {
162
+ // This should lead to crashloopback if this
163
+ // is running from within a Kubernetes pod
164
+ klog .Exit (err )
165
+ }
141
166
os .Exit (0 )
142
167
}
143
168
144
- func getRestConfig () * rest.Config {
169
+ // Runnable helps in executing the Kubernetes command resource.
170
+ // It does so by executing the commands or scripts specified in
171
+ // the resource and updating this resource post execution.
172
+ type Runnable struct {
173
+ Client dynamic.Interface
174
+ GVR schema.GroupVersionResource
175
+
176
+ commandStatus * types.CommandStatus
177
+ }
178
+
179
+ // NewRunner returns a new instance of Runnable
180
+ func NewRunner () (* Runnable , error ) {
145
181
var config * rest.Config
146
182
var err error
183
+
147
184
if * kubeconfig != "" {
148
- klog .V (1 ).Infof ("Using kubeconfig %s " , * kubeconfig )
185
+ klog .V (2 ).Infof ("Using kubeconfig %q " , * kubeconfig )
149
186
config , err = clientcmd .BuildConfigFromFlags ("" , * kubeconfig )
150
187
} else if * kubeAPIServerURL != "" {
151
- klog .V (1 ).Infof ("Using kubernetes api server url %s " , * kubeAPIServerURL )
188
+ klog .V (2 ).Infof ("Using kubernetes api server url %q " , * kubeAPIServerURL )
152
189
config , err = clientcmd .BuildConfigFromFlags (* kubeAPIServerURL , "" )
153
190
} else {
154
- klog .V (1 ).Info ("Using in-cluster kubeconfig" )
191
+ klog .V (2 ).Info ("Using in-cluster kubeconfig" )
155
192
config , err = rest .InClusterConfig ()
156
193
}
157
194
if err != nil {
158
- klog . Fatal ( err )
195
+ return nil , err
159
196
}
197
+
198
+ // configure kubernetes client config with additional settings
199
+ // to manage deluge of requests to kubernetes API server
160
200
config .QPS = float32 (* clientGoQPS )
161
201
config .Burst = * clientGoBurst
162
- return config
163
- }
164
202
165
- func runCommand (config * rest.Config ) {
166
203
client , err := dynamic .NewForConfig (config )
167
204
if err != nil {
168
- klog . Fatal ( err )
205
+ return nil , err
169
206
}
170
207
171
208
gvr := schema.GroupVersionResource {
@@ -174,77 +211,166 @@ func runCommand(config *rest.Config) {
174
211
Resource : * commandResource ,
175
212
}
176
213
177
- got , err := client .Resource (gvr ).
214
+ return & Runnable {
215
+ Client : client ,
216
+ GVR : gvr ,
217
+ }, nil
218
+ }
219
+
220
+ func (a * Runnable ) updateWithRetries () error {
221
+ var statusNew interface {}
222
+ err := unstruct .MarshalThenUnmarshal (a .commandStatus , & statusNew )
223
+ if err != nil {
224
+ return errors .Wrapf (
225
+ err ,
226
+ "Marshal unmarshal failed: Command %q %q" ,
227
+ * commandNamespace ,
228
+ * commandName ,
229
+ )
230
+ }
231
+
232
+ // Command is updated with latest labels
233
+ labels := map [string ]string {
234
+ // this label key is set with same value as that of status.phase
235
+ types .LblKeyCommandPhase : string (a .commandStatus .Phase ),
236
+ }
237
+
238
+ var runtimeErr error
239
+
240
+ // This uses exponential backoff to avoid exhausting
241
+ // the apiserver
242
+ retryErr := retry .RetryOnConflict (
243
+ retry .DefaultRetry ,
244
+ func () error {
245
+ // Retrieve the latest version of Command
246
+ cmd , err := a .Client .
247
+ Resource (a .GVR ).
248
+ Namespace (* commandNamespace ).
249
+ Get (* commandName , v1.GetOptions {})
250
+ if err != nil {
251
+ // Retry this error since this might be a temporary
252
+ return errors .Wrapf (
253
+ err ,
254
+ "Failed to get command: %q %q" ,
255
+ * commandNamespace ,
256
+ * commandName ,
257
+ )
258
+ }
259
+
260
+ // Mutate command resource's status field
261
+ err = unstructured .SetNestedField (
262
+ cmd .Object ,
263
+ statusNew ,
264
+ "status" ,
265
+ )
266
+ if err != nil {
267
+ runtimeErr = errors .Wrapf (
268
+ err ,
269
+ "Set unstruct failed: Command %q %q" ,
270
+ * commandNamespace ,
271
+ * commandName ,
272
+ )
273
+ // Return nil to avoid retry
274
+ //
275
+ // NOTE:
276
+ // Setting unstructured instance should not be
277
+ // retried since every retry will result in the
278
+ // same error
279
+ return nil
280
+ }
281
+
282
+ // Merge existing labels with desired pair(s)
283
+ unstruct .SetLabels (cmd , labels )
284
+
285
+ updated , err := a .Client .
286
+ Resource (a .GVR ).
287
+ Namespace (* commandNamespace ).
288
+ Update (cmd , v1.UpdateOptions {})
289
+
290
+ if err != nil {
291
+ // Update error is returned to be retried since this
292
+ // might be temporary
293
+ return errors .Wrapf (
294
+ err ,
295
+ "Update failed: Command %q %q" ,
296
+ * commandNamespace ,
297
+ * commandName ,
298
+ )
299
+ }
300
+
301
+ // Mutate command instance with latest resource version
302
+ // before trying update status. This is done since previous
303
+ // update would have modified resource version.
304
+ cmd .SetResourceVersion (updated .GetResourceVersion ())
305
+ // Update command status as a **sub resource** update
306
+ _ , err = a .Client .
307
+ Resource (a .GVR ).
308
+ Namespace (* commandNamespace ).
309
+ UpdateStatus (cmd , v1.UpdateOptions {})
310
+
311
+ // If update status resulted in an error it will be
312
+ // returned so that update can be retried
313
+ return errors .Wrapf (
314
+ err ,
315
+ "Update status failed: Command %q %q" ,
316
+ * commandNamespace ,
317
+ * commandName ,
318
+ )
319
+ })
320
+
321
+ if runtimeErr != nil {
322
+ return errors .Wrapf (
323
+ runtimeErr ,
324
+ "Update failed: Runtime error: Command: %q %q" ,
325
+ * commandNamespace ,
326
+ * commandName ,
327
+ )
328
+ }
329
+ return retryErr
330
+ }
331
+
332
+ // Run executes the command resource
333
+ func (a * Runnable ) Run () error {
334
+ got , err := a .Client .
335
+ Resource (a .GVR ).
178
336
Namespace (* commandNamespace ).
179
337
Get (
180
338
* commandName ,
181
339
v1.GetOptions {},
182
340
)
183
341
if err != nil {
184
- klog .Fatal (err )
342
+ return errors .Wrapf (
343
+ err ,
344
+ "Failed to get command: %q %q" ,
345
+ * commandNamespace ,
346
+ * commandName ,
347
+ )
185
348
}
186
349
187
350
var c types.Command
188
351
// convert from unstructured instance to typed instance
189
352
err = unstruct .ToTyped (got , & c )
190
353
if err != nil {
191
- klog .Fatal (err )
354
+ return errors .Wrapf (
355
+ err ,
356
+ "Failed to convert unstructured to typed instance: %q %q" ,
357
+ * commandNamespace ,
358
+ * commandName ,
359
+ )
192
360
}
193
361
194
- cmder , err := command .NewCommander (
195
- command.CommandableConfig {
196
- Command : & c ,
362
+ cmdRunner , err := command .NewRunner (
363
+ command.RunnableConfig {
364
+ Command : c ,
197
365
},
198
366
)
199
367
if err != nil {
200
- klog . Fatal ( err )
368
+ return err
201
369
}
202
- status , err := cmder .Run ()
370
+ a . commandStatus , err = cmdRunner .Run ()
203
371
if err != nil {
204
- klog . Fatal ( err )
372
+ return err
205
373
}
206
374
207
- retryErr := retry .RetryOnConflict (retry .DefaultRetry , func () error {
208
- // Retrieve the latest version of Command before attempting update
209
- // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
210
- got , err = client .Resource (gvr ).
211
- Namespace (* commandNamespace ).
212
- Get (
213
- * commandName ,
214
- v1.GetOptions {},
215
- )
216
- if err != nil {
217
- klog .Fatal (err )
218
- }
219
-
220
- // update labels
221
- lbls := got .GetLabels ()
222
- if len (lbls ) == 0 {
223
- lbls = make (map [string ]string )
224
- }
225
- lbls ["command.dope.metacontroller.io/phase" ] = string (status .Phase )
226
- got .SetLabels (lbls )
227
-
228
- // update status
229
- err = unstructured .SetNestedField (
230
- got .Object ,
231
- status ,
232
- "status" ,
233
- )
234
- if err != nil {
235
- klog .Fatal (err )
236
- }
237
-
238
- // update command resource
239
- _ , updateErr := client .Resource (gvr ).
240
- Namespace (* commandNamespace ).
241
- Update (
242
- got ,
243
- v1.UpdateOptions {},
244
- )
245
- return updateErr
246
- })
247
- if retryErr != nil {
248
- klog .Fatal (retryErr )
249
- }
375
+ return a .updateWithRetries ()
250
376
}
0 commit comments