Skip to content

Commit 0e0de65

Browse files
author
Amit Kumar Das
authored
feat(command): initial code for Command controller (#96)
This is the first commit to realise Command controller. Command is a new custom resource that lets users to run one or more os commands or a complete shell script from within a container. In addition this Command resource has the ability to grab the output after running these commands and update the results in its status field. Applying a Command resource will result in either of following Kubernetes resources: - Job // a native resource - DaemonJob // a custom resource refer - #1 Signed-off-by: AmitKumarDas <[email protected]>
1 parent 59e2d10 commit 0e0de65

File tree

12 files changed

+1023
-7
lines changed

12 files changed

+1023
-7
lines changed

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ PWD := ${CURDIR}
22

33
OS = $(shell uname)
44

5-
GIT_TAGS = $(shell git fetch --all --tags)
65
PACKAGE_VERSION ?= $(shell git describe --always --tags)
6+
GIT_TAGS = $(shell git fetch --all --tags)
77
ALL_SRC = $(shell find . -name "*.go" | grep -v -e "vendor")
88

99
# We are using docker hub as the default registry
@@ -45,6 +45,7 @@ testv:
4545
image: $(GIT_TAGS)
4646
docker build -t $(IMG_REPO):$(PACKAGE_VERSION) .
4747

48+
4849
.PHONY: push
4950
push: image
5051
docker push $(IMG_REPO):$(PACKAGE_VERSION)

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,12 @@ It is important to understand that these declarative patterns are built upon pro
99
### When to use d-operators
1010
D-operators is not meant to build complex controllers like Deployment, StatefulSet or Pod in a declarative yaml. However, if one needs to use Deployment, StatefulSet, Pod, etc. to build new k8s controller(s) then d-operators' declarative constructs _(read custom resources)_ should be considered to build one.
1111

12-
However, any controller which is complex and at the same time is **generic** enough to be used along with other kubernetes resources, can be implemented as a core d-operator custom resource.
12+
However, any controller which is complex and at the same time is **generic** enough to be used along with other kubernetes resources, can be implemented as a core d-operator custom resource.
13+
14+
### Available controllers
15+
- [x] kind: Recipe
16+
- [ ] kind: HTTP
17+
- [ ] kind: Chef
18+
- [ ] kind: Command
19+
- [ ] kind: DaemonJob
20+
- [ ] kind: MasterChef

cmd/commander/main.go

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*
2+
Copyright 2020 The MayaData Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"flag"
21+
"os"
22+
"path/filepath"
23+
24+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"k8s.io/apimachinery/pkg/runtime/schema"
27+
"k8s.io/client-go/dynamic"
28+
"k8s.io/client-go/rest"
29+
"k8s.io/client-go/tools/clientcmd"
30+
"k8s.io/client-go/util/homedir"
31+
"k8s.io/client-go/util/retry"
32+
"k8s.io/klog/v2"
33+
34+
"mayadata.io/d-operators/common/unstruct"
35+
"mayadata.io/d-operators/pkg/command"
36+
types "mayadata.io/d-operators/types/command"
37+
)
38+
39+
var (
40+
commandKind = flag.String(
41+
"command-kind",
42+
"Command",
43+
"Kind of Command custom resource",
44+
)
45+
commandResource = flag.String(
46+
"command-resource",
47+
"commands",
48+
"Resource name of Command custom resource",
49+
)
50+
commandGroup = flag.String(
51+
"command-group",
52+
"dope.metacontroller.io",
53+
"Group of Command custom resource",
54+
)
55+
commandVersion = flag.String(
56+
"command-version",
57+
"v1",
58+
"Version of Command custom resource",
59+
)
60+
commandName = flag.String(
61+
"command-name",
62+
"",
63+
"Name of Command custom resource",
64+
)
65+
commandNamespace = flag.String(
66+
"command-ns",
67+
"",
68+
"Namespace of Command custom resource",
69+
)
70+
71+
kubeAPIServerURL = flag.String(
72+
"kube-apiserver-url",
73+
"",
74+
`Kubernetes api server url (same format as used by kubectl).
75+
If not specified, uses in-cluster config`,
76+
)
77+
kubeconfig *string
78+
79+
clientGoQPS = flag.Float64(
80+
"client-go-qps",
81+
5,
82+
"Number of queries per second client-go is allowed to make (default 5)",
83+
)
84+
clientGoBurst = flag.Int(
85+
"client-go-burst",
86+
10,
87+
"Allowed burst queries for client-go (default 10)",
88+
)
89+
)
90+
91+
// main function is the entry point of this binary.
92+
//
93+
// This binary is meant to be run to completion. In other
94+
// words this does not expose any long running service.
95+
//
96+
// NOTE:
97+
// A kubernetes **Job** can make use of this binary
98+
func main() {
99+
if home := homedir.HomeDir(); home != "" {
100+
kubeconfig = flag.String(
101+
"kubeconfig",
102+
filepath.Join(home, ".kube", "config"),
103+
"(optional) absolute path to the kubeconfig file",
104+
)
105+
} else {
106+
kubeconfig = flag.String(
107+
"kubeconfig",
108+
"",
109+
"absolute path to the kubeconfig file",
110+
)
111+
}
112+
113+
flag.Set("alsologtostderr", "true")
114+
flag.Parse()
115+
116+
klogFlags := flag.NewFlagSet("klog", flag.ExitOnError)
117+
klog.InitFlags(klogFlags)
118+
119+
// Sync the glog and klog flags.
120+
flag.CommandLine.VisitAll(func(f1 *flag.Flag) {
121+
f2 := klogFlags.Lookup(f1.Name)
122+
if f2 != nil {
123+
value := f1.Value.String()
124+
f2.Value.Set(value)
125+
}
126+
})
127+
defer klog.Flush()
128+
129+
if *commandName == "" {
130+
klog.Fatal("Invalid arguments: Flag 'command-name' must be set")
131+
}
132+
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)
139+
140+
runCommand(getRestConfig())
141+
os.Exit(0)
142+
}
143+
144+
func getRestConfig() *rest.Config {
145+
var config *rest.Config
146+
var err error
147+
if *kubeconfig != "" {
148+
klog.V(1).Infof("Using kubeconfig %s", *kubeconfig)
149+
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
150+
} else if *kubeAPIServerURL != "" {
151+
klog.V(1).Infof("Using kubernetes api server url %s", *kubeAPIServerURL)
152+
config, err = clientcmd.BuildConfigFromFlags(*kubeAPIServerURL, "")
153+
} else {
154+
klog.V(1).Info("Using in-cluster kubeconfig")
155+
config, err = rest.InClusterConfig()
156+
}
157+
if err != nil {
158+
klog.Fatal(err)
159+
}
160+
config.QPS = float32(*clientGoQPS)
161+
config.Burst = *clientGoBurst
162+
return config
163+
}
164+
165+
func runCommand(config *rest.Config) {
166+
client, err := dynamic.NewForConfig(config)
167+
if err != nil {
168+
klog.Fatal(err)
169+
}
170+
171+
gvr := schema.GroupVersionResource{
172+
Group: *commandGroup,
173+
Version: *commandVersion,
174+
Resource: *commandResource,
175+
}
176+
177+
got, err := client.Resource(gvr).
178+
Namespace(*commandNamespace).
179+
Get(
180+
*commandName,
181+
v1.GetOptions{},
182+
)
183+
if err != nil {
184+
klog.Fatal(err)
185+
}
186+
187+
var c types.Command
188+
// convert from unstructured instance to typed instance
189+
err = unstruct.ToTyped(got, &c)
190+
if err != nil {
191+
klog.Fatal(err)
192+
}
193+
194+
cmder, err := command.NewCommander(
195+
command.CommandableConfig{
196+
Command: &c,
197+
},
198+
)
199+
if err != nil {
200+
klog.Fatal(err)
201+
}
202+
status, err := cmder.Run()
203+
if err != nil {
204+
klog.Fatal(err)
205+
}
206+
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+
}
250+
}

controller/command/reconciler.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2020 The MayaData Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package command
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
21+
"openebs.io/metac/controller/generic"
22+
23+
commonctrl "mayadata.io/d-operators/common/controller"
24+
"mayadata.io/d-operators/common/unstruct"
25+
"mayadata.io/d-operators/pkg/command"
26+
types "mayadata.io/d-operators/types/command"
27+
)
28+
29+
// Reconciler manages reconciliation of Command custom resource
30+
type Reconciler struct {
31+
commonctrl.Reconciler
32+
33+
ObservedCommand *types.Command
34+
Attachment *unstructured.Unstructured
35+
}
36+
37+
func (r *Reconciler) eval() {
38+
var c types.Command
39+
// convert from unstructured instance to typed instance
40+
err := unstruct.ToTyped(r.HookRequest.Watch, &c)
41+
if err != nil {
42+
r.Err = err
43+
return
44+
}
45+
r.ObservedCommand = &c
46+
}
47+
48+
func (r *Reconciler) invoke() {
49+
if r.ObservedCommand.Status.Phase != "" && !r.HookRequest.Attachments.IsEmpty() {
50+
return
51+
}
52+
builder := command.NewJobBuilder(
53+
command.JobBuilderConfig{
54+
Command: *r.ObservedCommand,
55+
},
56+
)
57+
r.Attachment, r.Err = builder.Build()
58+
}
59+
60+
func (r *Reconciler) setSyncResponse() {
61+
if len(r.HookResponse.Attachments) == 0 {
62+
r.HookResponse.SkipReconcile = true
63+
}
64+
65+
if r.ObservedCommand != nil &&
66+
r.ObservedCommand.Spec.Refresh.ResyncAfterSeconds != nil {
67+
r.HookResponse.ResyncAfterSeconds = *r.ObservedCommand.Spec.Refresh.ResyncAfterSeconds
68+
}
69+
}
70+
71+
// Sync implements the idempotent logic to sync Command resource
72+
//
73+
// NOTE:
74+
// SyncHookRequest is the payload received as part of reconcile
75+
// request. Similarly, SyncHookResponse is the payload sent as a
76+
// response as part of reconcile response.
77+
//
78+
// NOTE:
79+
// This controller watches Command custom resource
80+
func Sync(request *generic.SyncHookRequest, response *generic.SyncHookResponse) error {
81+
r := &Reconciler{
82+
Reconciler: commonctrl.Reconciler{
83+
Name: "command-sync-reconciler",
84+
HookRequest: request,
85+
HookResponse: response,
86+
},
87+
}
88+
// add functions to achieve desired state
89+
r.ReconcileFns = []func(){
90+
r.eval,
91+
r.invoke,
92+
r.setSyncResponse,
93+
}
94+
// run reconcile
95+
return r.Reconcile()
96+
}

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module mayadata.io/d-operators
33
go 1.13
44

55
require (
6-
9fans.net/go v0.0.2
6+
github.com/go-cmd/cmd v1.2.0
77
github.com/go-resty/resty/v2 v2.2.0
88
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
99
github.com/google/go-cmp v0.4.0
@@ -13,7 +13,6 @@ require (
1313
k8s.io/apiextensions-apiserver v0.17.3
1414
k8s.io/apimachinery v0.17.3
1515
k8s.io/client-go v0.17.3
16-
k8s.io/klog v1.0.0
1716
k8s.io/klog/v2 v2.0.0
1817
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f
1918
openebs.io/metac v0.3.0

0 commit comments

Comments
 (0)