Skip to content

Commit

Permalink
Huge config refactoring (#45)
Browse files Browse the repository at this point in the history
* Introduce versioning for hooks configuration API #36
* Hooks configuration API refactoring #37
* Add support for field selector for onKubernetesEvent hooks #35
  • Loading branch information
diafour authored and Dmitry Stolyarov committed Sep 2, 2019
1 parent 67e2f74 commit a169318
Show file tree
Hide file tree
Showing 23 changed files with 1,305 additions and 729 deletions.
53 changes: 38 additions & 15 deletions HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Shell-operator runs the hook with the `--config` flag. In response the hook shou

```
{
"configVersion": "v1",
"onStartup": ORDER,
"schedule": [
{SCHEDULE_PARAMETERS},
Expand All @@ -33,6 +34,8 @@ Shell-operator runs the hook with the `--config` flag. In response the hook shou
}
```

`configVersion` field specifies a version of configuration schema. The latest schema version is "v1" and it is described below.

Event binding is an event type (onStartup, schedule, and onKubernetesEvent) plus parameters required for a subscription.

#### onStartup
Expand All @@ -42,6 +45,7 @@ The execution at the Shell-operator’ startup.
Syntax:
```
{
"configVersion": "v1",
"onStartup": ORDER
}
```
Expand All @@ -57,14 +61,15 @@ Scheduled execution. You can bind a hook to any number of schedules.
Syntax:
```
{
"configVersion": "v1",
"schedule": [
{
"name": "Every 20 minutes",
"crontab": "0 */20 * * * *",
"allowFailure": true|false,
},
{
"crontab": "* * * * * *,
"crontab": "* * * * * *",
"allowFailure": true|false,
},
...
Expand All @@ -87,12 +92,13 @@ Run a hook on a Kubernetes object changes.
Syntax:
```
{
"configVersion": "v1",
"onKubernetesEvent": [
{
"name": "Monitor labeled pods in cache tier",
"kind": "Pod",
"event": [ "add", "update", "delete" ],
"selector": {
"labelSelector": {
"matchLabels": {
"myLabel": "myLabelValue",
"someKey": "someValue",
Expand All @@ -101,16 +107,26 @@ Syntax:
"matchExpressions": [
{
"key": "tier",
"operation": "In",
"operator": "In",
"values": ["cache"],
},
...
],
},
"objectName": "backend-7zxyf",
"namespaceSelector": {
"matchNames": ["somenamespace", "proj-production", "proj-stage"],
"any": true|false,
"fieldSelector": {
"matchExpressions": [
{
"field": "status.phase",
"operator": "Equal",
"value": "Pending",
},
...
],
},
"namespace": {
"nameSelector": {
"matchNames": ["somenamespace", "proj-production", proj-stage"],
}
},
"jqFilter": ".metadata.labels",
"allowFailure": true|false,
Expand All @@ -127,42 +143,44 @@ Parameters:
- `name` is an optional identifier. It is used to distinguish different bindings during runtime. For more info see [binding context](#binding-context).

- `kind` is the type of a monitored Kubernetes resource. CRDs are supported, but resource should be registered in cluster before shell-operator starts. This can be checked with `kubectl api-resources` command. You can specify case-insensitive name, kind or short name in this field. For example, to monitor a DaemonSet these forms are valid:

```
"kind": "DaemonSet"
"kind": "Daemonset"
"kind": "daemonsets"
"kind": "DaemonSets"
"kind": "ds"
"kind": "ds"
```

- `event` — the list of monitored events (add, update, delete). By default all events will be monitored.

- `selector`[standard](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#labelselector-v1-meta) selector of objects (examples [of use](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels)).
- `labelSelector`[standard](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#labelselector-v1-meta) selector of objects by labels (examples [of use](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels)).
If the selector is not set, then all objects are selected.

- `objectName`allows to specify a concrete Kubernetes object name. Can be used with or without `selector`.
- `fieldSelector`selector of objects by their fields, works like `--field-selector=''` flag of `kubectl`. Due to limits of API, supported operators are Equal and NotEqual and all expressions are combined with AND.

- `namespaceSelector`specifies the filter for selecting namespaces. If you omit it, the events from all namespaces will be monitored.
- `namespace`a filter to choose namespaces. If omitted, then the events from all namespaces will be monitored. Currently supported only `nameSelector` filter which can monitor event from particular list of namespaces.

- `jqFilter` — an optional parameter that specifies additional event filtering with [jq syntax](https://stedolan.github.io/jq/manual/). The hook will be triggered only when the properties of an object are changed after the filter is applied. See example [102-monitor-namespaces](examples/102-monitor-namespaces).

- `allowFailure` — if ‘true’, Shell-operator skips the hook execution errors. If ‘false’ or the parameter is not set, the hook is restarted after a 5 seconds delay in case of an error.

Example:
```
```json
{
"configVersion": "v1",
"onKubernetesEvent": [
{
"name": "Trigger on labels changes of Pods with myLabel:myLabelValue in any namespace",
"kind": "pod",
"event": ["update"],
"selector": {
"labelSelector": {
"matchLabels": {
"myLabel": "myLabelValue"
}
},
"namespaceSelector": {
"any": true
"namespace": {
"nameSelector": ["default"]
},
"jqFilter": ".metadata.labels",
"allowFailure": true
Expand All @@ -171,6 +189,10 @@ Example:
}
```

This hook configuration will execute hook on each change in labels of pods labeled with myLabel=myLabelValue in default namespace.

> Note: unlike `kubectl` you should explicitly define namespace.nameSelector to monitor events from `default` namespace.
> Note: Shell-operator requires a ServiceAccount with the appropriate [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) permissions. See examples with RBAC: [monitor-pods](examples/101-monitor-pods) and [monitor-namespaces](examples/102-monitor-namespaces).
## Main cycle: events tracking and hooks execution
Expand Down Expand Up @@ -212,6 +234,7 @@ For example, if you have the following binding configuration of a hook:

```json
{
"configVersion": "v1",
"schedule": [
{
"name": "incremental",
Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ Let's create a small operator that will watch for all Pods in all Namespaces and

if [[ $1 == "--config" ]] ; then
cat <<EOF
{"onKubernetesEvent": [
{"kind":"Pod",
"event":["add"]
{
"configVersion":"v1",
"onKubernetesEvent": [
{
"kind":"Pod",
"event":["add"]
}
]
}
]}
EOF
else
podName=$(jq -r .[0].resourceName $BINDING_CONTEXT_PATH)
Expand Down Expand Up @@ -145,7 +149,10 @@ This binding has only one parameter: order of execution. Hooks are loaded at sta
Example `hook --config`:

```
{"onStartup":10}
{
"configVersion": "v1",
"onStartup":10
}
```

__schedule__
Expand All @@ -156,6 +163,7 @@ Example `hook --config` with 2 schedules:

```
{
"configVersion": "v1",
"schedule": [
{"name":"every 10 min",
"crontab":"0 */10 * * * *",
Expand All @@ -178,6 +186,7 @@ Example of `hook --config`:

```
{
"configVersion": "v1",
"onKubernetesEvent": [
{"name":"Execute on changes of namespace labels",
"kind": "namespace",
Expand Down
10 changes: 2 additions & 8 deletions pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,11 @@ import (

var ExecutorLock = &sync.Mutex{}

func Run(cmd *exec.Cmd, debug bool) error {
func Run(cmd *exec.Cmd) error {
ExecutorLock.Lock()
defer ExecutorLock.Unlock()

if debug {
dir := ""
if cmd.Dir != "" {
dir = " in '" + cmd.Dir + "'"
}
rlog.Debugf("Executing command%s: '%s'", dir, strings.Join(cmd.Args, " "))
}
rlog.Debugf("Executing command %q in %q dir", strings.Join(cmd.Args, " "), cmd.Dir)

return cmd.Run()
}
Expand Down
23 changes: 12 additions & 11 deletions pkg/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import (
"github.com/romana/rlog"
)

// ошибка waitid: no child process приходит от bash, который запускается основным процессом,
// если reaper почистил дочерний процесс этого bash-а.
// Нельзя запускать reaper параллельно с работающими cmd.Run или cmd.Output, потому что
// даже если отловить ECHILD, то не получится узнать exitCode — его уже собрал reaper :(
func TestCmdRun(t *testing.T) {
//t.SkipNow()
// Not really a test, more like a proof of concept.
//
// Reaper should not work in parallel with cmd.Run or cmd.Output:
// - cmd.Run do wait children, but reaper will clean them
// - reaper prevent to get real exitCode from cmd.Run
func Test_CmdRun_And_Reaper_PoC_Code(t *testing.T) {
t.SkipNow()
os.Setenv("RLOG_LOG_LEVEL", "DEBUG")
rlog.UpdateEnv()

Expand Down Expand Up @@ -95,9 +96,9 @@ func TestCmdRun(t *testing.T) {
return
}

// Проверка блокировок между reaper и вызовом cmd.Run
// Запуск 10 горутин с вызовом bash и рипера
func TestExecutorCmdRun(t *testing.T) {
// Check that reaper and cmd.Run are locked.
// Run 10 go-routines that execute bash.
func Test_Executor_Reaper_And_CmdRun_AreLocked(t *testing.T) {
t.SkipNow()
os.Setenv("RLOG_LOG_LEVEL", "DEBUG")
rlog.UpdateEnv()
Expand Down Expand Up @@ -128,7 +129,7 @@ func TestExecutorCmdRun(t *testing.T) {
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf

err := Run(cmd, true)
err := Run(cmd)

stdout := outbuf.String()
stderr := errbuf.String()
Expand All @@ -154,7 +155,7 @@ func TestExecutorCmdRun(t *testing.T) {
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
}
rlog.Debugf("command result, stdout: %v, stderr: %v, exitCode: %v, echild: %v", stdout, stderr, exitCode)
rlog.Debugf("command result, stdout: %v, stderr: %v, exitCode: %v", stdout, stderr, exitCode)
stopCh <- true
}()

Expand Down
Loading

0 comments on commit a169318

Please sign in to comment.