Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object manipulation framework #239

Merged
merged 12 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions KUBERNETES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Modifying Kubernetes objects

You can delegate Kubernetes object manipulation to the shell-operator.

To do this, you have to write one or more JSON or YAML documents describing operation type and its parameters to a file.
List of possible operations and corresponding JSON specifications can be found below.

The path to the file is found in the `$KUBERNETES_PATCH_PATH` environment variable.

## Operations

### Create

* `operation` — specifies an operation's type.
* `CreateOrUpdate` — accept a Kubernetes object.
It retrieves an object, and if it already exists, computes a JSON Merge Patch and applies it (will not update .status field).
If it does not exist, we create the object.
* `Create` — will fail if an object already exists
* `CreateIfNotExists` — create an object if such an object does not already
exist by namespace/name.
* `object` — full object specification including "apiVersion", "kind" and all necessary metadata.

#### Example

```json
{
"operation": "CreateOrUpdate",
"object": {
"apiVersion": "apps/v1",
"kind": "DaemonSet",
"metadata": {
"name": "flannel",
"namespace": "d8-flannel"
},
"spec": {
"selector": {
"matchLabels": {
"app": "flannel"
}
},
"template": {
"metadata": {
"labels": {
"app": "flannel",
"tier": "node"
}
},
"spec": {
"containers": [
{
"args": [
"--ip-masq",
"--kube-subnet-mgr"
],
"image": "flannel:v0.11",
"name": "kube-flannel",
"securityContext": {
"privileged": true
}
}
],
"hostNetwork": true,
"imagePullSecrets": [
{
"name": "registry"
}
],
"terminationGracePeriodSeconds": 5
}
},
"updateStrategy": {
"type": "RollingUpdate"
}
}
}
}
```

### Delete

* `operation` — specifies an operation's type. Deletion types map directly to Kubernetes
DELETE's [`propagationPolicy`](https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/).
* `Delete` — foreground deletion. Hook will block the queue until the referenced object and all its descendants are deleted.
* `DeleteInBackground` — will delete the referenced object immediately. All its descendants will be removed by Kubernetes'
garbage collector.
* `DeleteNonCascading` — will delete the referenced object immediately, and orphan all its descendants.
* `apiVersion` — optional field that specifies object's apiVersion. If not present, we'll use preferred apiVersion
for the given kind.
* `kind` — object's Kind.
* `namespace` — object's namespace. If empty, implies operation on a cluster-level resource.
* `name` — object's name.
* `subresource` — a subresource name if subresource is to be transformed. For example, `status`.

#### Example

```json
{
"operation": "Delete",
"kind": "Pod",
"namespace": "default",
"name": "nginx"
}
```

### Patch

Use `JQPatch` for almost everything. Consider using `MergePatch` or `JSONPatch` if you are attempting to modify
rapidly changing object, for example `status` field with many concurrent changes (and incrementing `resourceVersion`).

Be careful, when updating a `.status` field. If a `/status` subresource is enabled on a resource,
it'll ignore updates to the `.status` field if you haven't specified `subresource: status` in the operation spec.
More info [here](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status).

#### JQPatch

* `operation` — specifies an operation's type.
* `apiVersion` — optional field that specifies object's apiVersion. If not present, we'll use preferred apiVersion
for the given kind.
* `kind` — object's Kind.
* `namespace` — object's Namespace. If empty, implies operation on a Cluster-level resource.
* `name` — object's name.
* `jqFilter` — describes transformations to perform on an object.
* `subresource` — a subresource name if subresource is to be transformed. For example, `status`.
##### Example

```json
{
"operation": "JQPatch",
"kind": "Deployment",
"namespace": "default",
"name": "nginx",
"jqFilter": ".spec.replicas = 1"
}
```

#### MergePatch

* `operation` — specifies an operation's type.
* `apiVersion` — optional field that specifies object's apiVersion. If not present, we'll use preferred apiVersion
for the given kind.
* `kind` — object's Kind.
* `namespace` — object's Namespace. If empty, implies operation on a Cluster-level resource.
* `name` — object's name.
* `mergePatch` — describes transformations to perform on an object.
* `subresource` — e.g., `status`.

##### Example

```json
{
"operation": "MergePatch",
"kind": "Deployment",
"namespace": "default",
"name": "nginx",
"mergePatch": {
"spec": {
"replicas": 1
}
}
}
```

#### JSONPatch

* `operation` — specifies an operation's type.
* `apiVersion` — optional field that specifies object's apiVersion. If not present, we'll use preferred apiVersion
for the given kind.
* `kind` — object's Kind.
* `namespace` — object's Namespace. If empty, implies operation on a Cluster-level resource.
* `name` — object's name.
* `jsonPatch` — describes transformations to perform on an object.
* `subresource` — a subresource name if subresource is to be transformed. For example, `status`.

##### Example

```json
{
"operation": "JSONPatch",
"kind": "Deployment",
"namespace": "default",
"name": "nginx",
"jsonPatch": [
{"op": "replace", "path": "/spec/replicas", "value": 1}
]
}
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5
gopkg.in/satori/go.uuid.v1 v1.2.0
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
k8s.io/api v0.17.0
k8s.io/apiextensions-apiserver v0.17.0
k8s.io/apimachinery v0.17.0
Expand Down
2 changes: 1 addition & 1 deletion pkg/hook/binding_context/binding_context.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hook
package binding_context

import (
"encoding/json"
Expand Down
2 changes: 1 addition & 1 deletion pkg/hook/binding_context/binding_context_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hook
package binding_context

import (
"testing"
Expand Down
32 changes: 28 additions & 4 deletions pkg/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ type CommonHook interface {
}

type HookResult struct {
Usage *executor.CmdUsage
Metrics []operation.MetricOperation
ConversionResponse *conversion.Response
ValidatingResponse *ValidatingResponse
Usage *executor.CmdUsage
Metrics []operation.MetricOperation
ConversionResponse *conversion.Response
ValidatingResponse *ValidatingResponse
KubernetesPatchBytes []byte
}

type Hook struct {
Expand Down Expand Up @@ -108,12 +109,18 @@ func (h *Hook) Run(bindingType BindingType, context []BindingContext, logLabels
return nil, err
}

kubernetesPatchPath, err := h.prepareObjectPatchFile()
if err != nil {
return nil, err
}

// remove tmp file on hook exit
defer func() {
if app.DebugKeepTmpFiles != "yes" {
os.Remove(contextPath)
os.Remove(metricsPath)
os.Remove(validatingPath)
os.Remove(kubernetesPatchPath)
}
}()

Expand All @@ -124,6 +131,7 @@ func (h *Hook) Run(bindingType BindingType, context []BindingContext, logLabels
envs = append(envs, fmt.Sprintf("METRICS_PATH=%s", metricsPath))
envs = append(envs, fmt.Sprintf("CONVERSION_RESPONSE_PATH=%s", conversionPath))
envs = append(envs, fmt.Sprintf("VALIDATING_RESPONSE_PATH=%s", validatingPath))
envs = append(envs, fmt.Sprintf("KUBERNETES_PATCH_PATH=%s", kubernetesPatchPath))
}

hookCmd := executor.MakeCommand(path.Dir(h.Path), h.Path, []string{}, envs)
Expand All @@ -150,6 +158,11 @@ func (h *Hook) Run(bindingType BindingType, context []BindingContext, logLabels
return result, fmt.Errorf("got bad conversion response: %s", err)
}

result.KubernetesPatchBytes, err = ioutil.ReadFile(kubernetesPatchPath)
if err != nil {
return result, fmt.Errorf("can't read object patch file: %s", err)
}

return result, nil
}

Expand Down Expand Up @@ -283,3 +296,14 @@ func CreateRateLimiter(cfg *config.HookConfig) *rate.Limiter {
}
return rate.NewLimiter(limit, burst)
}

func (h *Hook) prepareObjectPatchFile() (string, error) {
objectPatchPath := filepath.Join(h.TmpDir, fmt.Sprintf("%s-object-patch-%s", h.SafeName(), uuid.NewV4().String()))

err := ioutil.WriteFile(objectPatchPath, []byte{}, 0644)
if err != nil {
return "", err
}

return objectPatchPath, nil
}
Loading