Skip to content

Commit 090d341

Browse files
authored
Merge pull request #146 from AmitKumarDas/code-39
feat(command): add command cr that creates a k8s job
2 parents 4540187 + 0aff669 commit 090d341

20 files changed

+3040
-411
lines changed

cmd/commander/main.go

+209-83
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"path/filepath"
2323

24+
"github.com/pkg/errors"
2425
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2627
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -39,33 +40,38 @@ import (
3940
var (
4041
commandKind = flag.String(
4142
"command-kind",
42-
"Command",
43-
"Kind of Command custom resource",
43+
"Command", // default
44+
"Kubernetes custom resource kind",
4445
)
46+
4547
commandResource = flag.String(
4648
"command-resource",
47-
"commands",
48-
"Resource name of Command custom resource",
49+
"commands", // default
50+
"Kubernetes custom resource name",
4951
)
52+
5053
commandGroup = flag.String(
5154
"command-group",
52-
"dope.metacontroller.io",
53-
"Group of Command custom resource",
55+
"dope.metacontroller.io", // default
56+
"Kubernetes custom resource group",
5457
)
58+
5559
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",
5963
)
64+
6065
commandName = flag.String(
6166
"command-name",
6267
"",
63-
"Name of Command custom resource",
68+
"Name of the command",
6469
)
70+
6571
commandNamespace = flag.String(
6672
"command-ns",
6773
"",
68-
"Namespace of Command custom resource",
74+
"Namespace of the command",
6975
)
7076

7177
kubeAPIServerURL = flag.String(
@@ -74,24 +80,29 @@ var (
7480
`Kubernetes api server url (same format as used by kubectl).
7581
If not specified, uses in-cluster config`,
7682
)
83+
7784
kubeconfig *string
7885

7986
clientGoQPS = flag.Float64(
8087
"client-go-qps",
81-
5,
88+
5, // default
8289
"Number of queries per second client-go is allowed to make (default 5)",
8390
)
91+
8492
clientGoBurst = flag.Int(
8593
"client-go-burst",
86-
10,
94+
10, // default
8795
"Allowed burst queries for client-go (default 10)",
8896
)
8997
)
9098

91-
// main function is the entry point of this binary.
99+
// This is the entry point of this binary
92100
//
93101
// This binary is meant to be run to completion. In other
94102
// 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+
//
95106
//
96107
// NOTE:
97108
// A kubernetes **Job** can make use of this binary
@@ -127,45 +138,71 @@ func main() {
127138
defer klog.Flush()
128139

129140
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")
131145
}
132146

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)
139153

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+
}
141166
os.Exit(0)
142167
}
143168

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) {
145181
var config *rest.Config
146182
var err error
183+
147184
if *kubeconfig != "" {
148-
klog.V(1).Infof("Using kubeconfig %s", *kubeconfig)
185+
klog.V(2).Infof("Using kubeconfig %q", *kubeconfig)
149186
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
150187
} 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)
152189
config, err = clientcmd.BuildConfigFromFlags(*kubeAPIServerURL, "")
153190
} else {
154-
klog.V(1).Info("Using in-cluster kubeconfig")
191+
klog.V(2).Info("Using in-cluster kubeconfig")
155192
config, err = rest.InClusterConfig()
156193
}
157194
if err != nil {
158-
klog.Fatal(err)
195+
return nil, err
159196
}
197+
198+
// configure kubernetes client config with additional settings
199+
// to manage deluge of requests to kubernetes API server
160200
config.QPS = float32(*clientGoQPS)
161201
config.Burst = *clientGoBurst
162-
return config
163-
}
164202

165-
func runCommand(config *rest.Config) {
166203
client, err := dynamic.NewForConfig(config)
167204
if err != nil {
168-
klog.Fatal(err)
205+
return nil, err
169206
}
170207

171208
gvr := schema.GroupVersionResource{
@@ -174,77 +211,166 @@ func runCommand(config *rest.Config) {
174211
Resource: *commandResource,
175212
}
176213

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).
178336
Namespace(*commandNamespace).
179337
Get(
180338
*commandName,
181339
v1.GetOptions{},
182340
)
183341
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+
)
185348
}
186349

187350
var c types.Command
188351
// convert from unstructured instance to typed instance
189352
err = unstruct.ToTyped(got, &c)
190353
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+
)
192360
}
193361

194-
cmder, err := command.NewCommander(
195-
command.CommandableConfig{
196-
Command: &c,
362+
cmdRunner, err := command.NewRunner(
363+
command.RunnableConfig{
364+
Command: c,
197365
},
198366
)
199367
if err != nil {
200-
klog.Fatal(err)
368+
return err
201369
}
202-
status, err := cmder.Run()
370+
a.commandStatus, err = cmdRunner.Run()
203371
if err != nil {
204-
klog.Fatal(err)
372+
return err
205373
}
206374

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()
250376
}

0 commit comments

Comments
 (0)