Skip to content

Commit

Permalink
Addition of Patch Job Type (kube-burner#149)
Browse files Browse the repository at this point in the history
* Created patch job type

* Added useful patch examples

* Added job iteration to Patch

* Revert addition of new line

* Removed debug print
  • Loading branch information
jaredoconnell authored Feb 10, 2022
1 parent 3c55e43 commit 1149d51
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 3 deletions.
2 changes: 2 additions & 0 deletions cmd/kube-burner/kube-burner.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ func steps(uuid string, p *prometheus.Prometheus, alertM *alerting.AlertManager)
}
case config.DeletionJob:
job.RunDeleteJob()
case config.PatchJob:
job.RunPatchJob()
}
if job.Config.JobPause > 0 {
log.Infof("Pausing for %v before finishing job", job.Config.JobPause)
Expand Down
33 changes: 30 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ All objects created by kube-burner are labeled with. `kube-burner-uuid=<UUID>,ku

## Job types

kube-burner support two types of jobs with different parameters each. The default job type is __create__. Which basically creates objects as described in the section [objects](#objects).
kube-burner support three types of jobs with different parameters each. The default job type is __create__. Which basically creates objects as described in the section [objects](#objects).

The other type is __delete__, this type of job deletes objects described in the objects list. Using delete as job type the objects list would have the following structure.
The second type is __delete__, this type of job deletes objects described in the objects list. Using delete as job type the objects list would have the following structure:

```yaml
objects:
Expand All @@ -97,7 +97,34 @@ Where:
- labelSelector: Map with the labelSelector.
- apiVersion: API version from the k8s object.

As mentioned previously, all objects created by kube-burner are labeled with `kube-burner-uuid=<UUID>,kube-burner-job=<jobName>,kube-burner-index=<objectIndex>`. Thanks to this we could design a workload with one job to create objects and another one able to remove the objects created by the previous

The third type is __patch__, which can patch objects described in the objects list with the template described in the object list. The objects list would have the following structure:

```yaml
objects:
- kind: Deployment
labelSelector: {kube-burner-job: cluster-density}
objectTemplate: templates/deployment_patch_add_label.json
patchType: "application/strategic-merge-patch+json"
apiVersion: apps/v1
```

Where:

- kind: Object kind of the k8s object to patch.
- labelSelector: Map with the labelSelector.
- objectTemplate: The YAML template or JSON file to patch.
- apiVersion: API version from the k8s object.
- patchType: The kubernetes request patch type (see below).

Valid patch types:
- application/json-patch+json
- application/merge-patch+json
- application/strategic-merge-patch+json
- application/apply-patch+yaml (requires YAML)

As mentioned previously, all objects created by kube-burner are labeled with `kube-burner-uuid=<UUID>,kube-burner-job=<jobName>,kube-burner-index=<objectIndex>`. Thanks to this we could design a workload with one job to create objects and another one able to patch or remove the objects created by the previous

```yaml
jobs:
Expand Down
22 changes: 22 additions & 0 deletions examples/workloads/api-intensive/api-intensive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ jobs:
- objectTemplate: templates/service.yaml
replicas: 1

- name: api-intensive-patch
jobType: patch
jobIterations: 10
qps: 2
burst: 2
objects:
- kind: Deployment
objectTemplate: templates/deployment_patch_add_label.json
labelSelector: {kube-burner-job: api-intensive}
patchType: "application/json-patch+json"
apiVersion: apps/v1
- kind: Deployment
objectTemplate: templates/deployment_patch_add_pod_2.yaml
labelSelector: {kube-burner-job: api-intensive}
patchType: "application/apply-patch+yaml"
apiVersion: apps/v1
- kind: Deployment
objectTemplate: templates/deployment_patch_add_pod_3.yaml
labelSelector: {kube-burner-job: api-intensive}
patchType: "application/strategic-merge-patch+json"
apiVersion: apps/v1

- name: api-intensive-remove
qps: 2
burst: 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"op": "add",
"path": "/metadata/labels/new_key",
"value": "new_value"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
new_key_{{.Iteration}}: new_value_{{.Iteration}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- image: k8s.gcr.io/pause:3.1
name: api-intensive-2
resources:
requests:
cpu: 10m
memory: 10M
3 changes: 3 additions & 0 deletions pkg/burner/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type object struct {
unstructured *unstructured.Unstructured
inputVars map[string]interface{}
labelSelector map[string]string
patchType string
}

// Executor contains the information required to execute a job
Expand Down Expand Up @@ -72,6 +73,8 @@ func NewExecutorList(uuid string) []Executor {
ex = setupCreateJob(job)
} else if job.JobType == config.DeletionJob {
ex = setupDeleteJob(job)
} else if job.JobType == config.PatchJob {
ex = setupPatchJob(job)
} else {
log.Fatalf("Unknown jobType: %s", job.JobType)
}
Expand Down
188 changes: 188 additions & 0 deletions pkg/burner/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2022 The Kube-burner Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package burner

import (
"context"
"io"
"io/ioutil"
"strings"
"sync"

"github.com/cloud-bulldozer/kube-burner/log"
"github.com/cloud-bulldozer/kube-burner/pkg/config"
"github.com/cloud-bulldozer/kube-burner/pkg/util"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
)

func setupPatchJob(jobConfig config.Job) Executor {
var f io.Reader
var err error
log.Infof("Preparing patch job: %s", jobConfig.Name)
var ex Executor
for _, o := range jobConfig.Objects {
if o.APIVersion == "" {
o.APIVersion = "v1"
}
log.Debugf("Processing template: %s", o.ObjectTemplate)
f, err = util.ReadConfig(o.ObjectTemplate)
if err != nil {
log.Fatalf("Error reading template %s: %s", o.ObjectTemplate, err)
}
t, err := ioutil.ReadAll(f)
if err != nil {
log.Fatalf("Error reading template %s: %s", o.ObjectTemplate, err)
}

// Unlike create, we don't want to create the gvk with the entire template,
// because it would try to use the properties of the patch data to find
// the objects to patch.
gvk := schema.FromAPIVersionAndKind(o.APIVersion, o.Kind)
gvr, _ := meta.UnsafeGuessKindToResource(gvk)
if len(o.LabelSelector) == 0 {
log.Fatalf("Empty labelSelectors not allowed with: %s", o.Kind)
}
if len(o.PatchType) == 0 {
log.Fatalln("Empty Patch Type not allowed")
}
obj := object{
gvr: gvr,
objectSpec: t,
objectTemplate: o.ObjectTemplate,
inputVars: o.InputVars,
labelSelector: o.LabelSelector,
patchType: o.PatchType,
}
log.Infof("Job %s: Patch %s with selector %s", jobConfig.Name, gvk.Kind, labels.Set(obj.labelSelector))
ex.objects = append(ex.objects, obj)
}
return ex
}

// RunPatchJob executes a patch job
func (ex *Executor) RunPatchJob() {
var itemList *unstructured.UnstructuredList
_, RestConfig, err := config.GetClientSet(ex.Config.QPS, ex.Config.Burst)
if err != nil {
log.Fatalf("Error creating restConfig for kube-burner: %s", err)
}
dynamicClient, err = dynamic.NewForConfig(RestConfig)
if err != nil {
log.Fatalf("Error creating DynamicClient: %s", err)
}

log.Infof("Running patch job %s", ex.Config.Name)
var wg sync.WaitGroup
for _, obj := range ex.objects {

labelSelector := labels.Set(obj.labelSelector).String()
listOptions := metav1.ListOptions{
LabelSelector: labelSelector,
}

// Try to find the list of resources by GroupVersionResource.
err = RetryWithExponentialBackOff(func() (done bool, err error) {
itemList, err = dynamicClient.Resource(obj.gvr).List(context.TODO(), listOptions)
if err != nil {
log.Errorf("Error found listing %s labeled with %s: %s", obj.gvr.Resource, labelSelector, err)
return false, nil
}
return true, nil
})
if err != nil {
continue
}
log.Infof("Found %d %s with selector %s; patching them", len(itemList.Items), obj.gvr.Resource, labelSelector)
for i := 1; i <= ex.Config.JobIterations; i++ {
for _, item := range itemList.Items {
wg.Add(1)
go ex.patchHandler(obj, item, i, &wg)
}
}
}
wg.Wait()
}

func (ex *Executor) patchHandler(obj object, originalItem unstructured.Unstructured,
iteration int, wg *sync.WaitGroup) {

defer wg.Done()

// There are several patch modes. Three of them are client-side, and one
// of them is server-side.
var data []byte
patchOptions := metav1.PatchOptions{}

if strings.HasSuffix(obj.objectTemplate, "json") {
if obj.patchType == string(types.ApplyPatchType) {
log.Fatalf("Apply patch type requires YAML")
}
data = obj.objectSpec
} else {
// Processing template
templateData := map[string]interface{}{
jobName: ex.Config.Name,
jobIteration: iteration,
jobUUID: ex.uuid,
}
for k, v := range obj.inputVars {
templateData[k] = v
}
renderedObj, err := util.RenderTemplate(obj.objectSpec, templateData, util.MissingKeyError)
if err != nil {
log.Fatalf("Template error in %s: %s", obj.objectTemplate, err)
}

// Converting to JSON if patch type is not Apply
if obj.patchType == string(types.ApplyPatchType) {
data = renderedObj
patchOptions.FieldManager = "kube-controller-manager"
} else {
newObject := &unstructured.Unstructured{}
yamlToUnstructured(renderedObj, newObject)
data, err = newObject.MarshalJSON()
if err != nil {
log.Errorf("Error converting patch to JSON")
}
}
}

log.Debugf("Patching %s/%s in namespace %s", originalItem.GetKind(),
originalItem.GetName(), originalItem.GetNamespace())
ex.limiter.Wait(context.TODO())

uns, err := dynamicClient.Resource(obj.gvr).Namespace(originalItem.GetNamespace()).
Patch(context.TODO(), originalItem.GetName(),
types.PatchType(obj.patchType), data, patchOptions)
if err != nil {
if errors.IsForbidden(err) {
log.Fatalf("Authorization error patching %s/%s: %s", originalItem.GetKind(), originalItem.GetName(), err)
} else if err != nil {
log.Errorf("Error patching object %s/%s in namespace %s: %s", originalItem.GetKind(),
originalItem.GetName(), originalItem.GetNamespace(), err)
}
} else {
ns := originalItem.GetNamespace()
log.Debugf("Patched %s/%s in namespace %s", uns.GetKind(), uns.GetName(), ns)
}

}
4 changes: 4 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const (
CreationJob JobType = "create"
// DeletionJob used to delete objects
DeletionJob JobType = "delete"
// PatchJob used to patch objects
PatchJob JobType = "patch"
)

// Spec configuration root
Expand Down Expand Up @@ -82,6 +84,8 @@ type Object struct {
InputVars map[string]interface{} `yaml:"inputVars" json:"inputVars,omitempty"`
// Kind object kind to delete
Kind string `yaml:"kind" json:"kind,omitempty"`
// The type of patch mode
PatchType string `yaml:"patchType" json:"patchType,omitempty"`
// APIVersion apiVersion of the object to remove
APIVersion string `yaml:"apiVersion" json:"apiVersion,omitempty"`
// LabelSelector objects with this labels will be removed
Expand Down

0 comments on commit 1149d51

Please sign in to comment.