Repository: grafana/k6-operator
Pull request #182 (2023-01-20 13:12:30)

Scuttle config: change scuttleLogging to disableLogging
Scuttle logging is enabled by default:
https://github.com/redboxllc/scuttle/blob/1c2b697935c10ef4b3e5e0dc0cf573a4d8766269/scuttle_config.go#L31

So the setting for it should be "disable logging" instead of "enable logging" as it has been so far.

This is a breaking change for those who rely on `scuttle` configuration.

This PR also contains cleaned up manifests (after `make manifests`) as it appears some of previous changes weren't properly applied before.


Patch:
diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml
index c1ab27f..480df66 100644
--- a/.github/workflows/push.yaml
+++ b/.github/workflows/push.yaml
@@ -2,6 +2,11 @@
 name: "push"
 
 on:
+  workflow_dispatch:
+    inputs:
+      image_tag:
+        description: 'Image tag name.'
+        required: true
   release:
     types:
       - published
@@ -13,8 +18,20 @@ jobs:
     steps:
       - name: "Build:checkout"
         uses: actions/checkout@v2
+      - name: "Set image tag name"
+        run: |
+          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+            echo "IMAGETAG=${{ github.event.inputs.image_tag }}" >> $GITHUB_ENV
+          else
+            echo "IMAGETAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
+          fi
+      - name: "Check image tag name"
+        run: |
+          echo "IMAGETAG=${{env.IMAGETAG}}"
       - name: "Build:buildx"
         uses: docker/setup-buildx-action@v1
+        with:
+          version: v0.9.1 # Buildx version
       - name: "Build:login"
         uses: docker/login-action@v1
         with:
@@ -29,18 +46,18 @@ jobs:
           file: Dockerfile.controller
           build-args: |
             GO_BUILDER_IMG=golang:1.17
-          tags: ghcr.io/grafana/operator:latest,ghcr.io/grafana/operator:controller-${{ github.event.release.tag_name }}
+          tags: ghcr.io/grafana/operator:latest,ghcr.io/grafana/operator:controller-${{env.IMAGETAG}}
       - name: "Build:dockerimage"
         uses: docker/build-push-action@v2
         with:
           context: .
           push: true
           file: Dockerfile.runner
-          tags: ghcr.io/grafana/operator:latest-runner,ghcr.io/grafana/operator:runner-${{ github.event.release.tag_name }}
+          tags: ghcr.io/grafana/operator:latest-runner,ghcr.io/grafana/operator:runner-${{env.IMAGETAG}}
       - name: "Build:dockerimage"
         uses: docker/build-push-action@v2
         with:
           context: .
           push: true
           file: Dockerfile.starter
-          tags: ghcr.io/grafana/operator:latest-starter,ghcr.io/grafana/operator:starter-${{ github.event.release.tag_name }}
+          tags: ghcr.io/grafana/operator:latest-starter,ghcr.io/grafana/operator:starter-${{env.IMAGETAG}}
diff --git a/.github/workflows/yamllint.yaml b/.github/workflows/yamllint.yaml
index 18849c2..99be38b 100644
--- a/.github/workflows/yamllint.yaml
+++ b/.github/workflows/yamllint.yaml
@@ -1,6 +1,8 @@
 ---
 name: Yaml Lint
-on: [push]
+on:
+  - push
+  - pull_request
 jobs:
   lintAllTheThings:
     runs-on: ubuntu-latest
diff --git a/.yamllint.yaml b/.yamllint.yaml
index 83aa071..ab07c4d 100644
--- a/.yamllint.yaml
+++ b/.yamllint.yaml
@@ -8,6 +8,7 @@ rules:
     min-spaces-from-content: 1
   trailing-spaces:
     level: warning
+  indentation: disable # kustomize can rewrite yaml files and break this rule
   truthy:
     ignore: |
       /.github/workflows
diff --git a/api/v1alpha1/k6_types.go b/api/v1alpha1/k6_types.go
index 30ad5b8..97eb3ed 100644
--- a/api/v1alpha1/k6_types.go
+++ b/api/v1alpha1/k6_types.go
@@ -45,7 +45,7 @@ type K6Scuttle struct {
 	EnvoyAdminApi           string `json:"envoyAdminApi,omitempty"`
 	NeverKillIstio          bool   `json:"neverKillIstio,omitempty"`
 	NeverKillIstioOnFailure bool   `json:"neverKillIstioOnFailure,omitempty"`
-	ScuttleLogging          bool   `json:"scuttleLogging,omitempty"`
+	DisableLogging          bool   `json:"disableLogging,omitempty"`
 	StartWithoutEnvoy       bool   `json:"startWithoutEnvoy,omitempty"`
 	WaitForEnvoyTimeout     string `json:"waitForEnvoyTimeout,omitempty"`
 	IstioQuitApi            string `json:"istioQuitApi,omitempty"`
@@ -94,7 +94,7 @@ type K6Configmap struct {
 type Cleanup string
 
 // Stage describes which stage of the test execution lifecycle our runners are in
-// +kubebuilder:validation:Enum=initialization;initialized;created;started;finished
+// +kubebuilder:validation:Enum=initialization;initialized;created;started;finished;error
 type Stage string
 
 // K6Status defines the observed state of K6
@@ -105,6 +105,8 @@ type K6Status struct {
 // K6 is the Schema for the k6s API
 // +kubebuilder:object:root=true
 // +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Stage",type="string",JSONPath=".status.stage",description="Stage"
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
 type K6 struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index bc7166b..5c947af 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -214,6 +214,13 @@ func (in *Pod) DeepCopyInto(out *Pod) {
 			(*out)[key] = val
 		}
 	}
+	if in.Tolerations != nil {
+		in, out := &in.Tolerations, &out.Tolerations
+		*out = make([]v1.Toleration, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	in.Resources.DeepCopyInto(&out.Resources)
 	in.SecurityContext.DeepCopyInto(&out.SecurityContext)
 	if in.EnvFrom != nil {
diff --git a/config/crd/bases/k6.io_k6s.yaml b/config/crd/bases/k6.io_k6s.yaml
index aae74d7..4a3303e 100644
--- a/config/crd/bases/k6.io_k6s.yaml
+++ b/config/crd/bases/k6.io_k6s.yaml
@@ -16,7 +16,15 @@ spec:
     singular: k6
   scope: Namespaced
   versions:
-  - name: v1alpha1
+  - additionalPrinterColumns:
+    - description: Stage
+      jsonPath: .status.stage
+      name: Stage
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha1
     schema:
       openAPIV3Schema:
         description: K6 is the Schema for the k6s API
@@ -848,6 +856,10 @@ spec:
                     type: array
                   image:
                     type: string
+                  imagePullPolicy:
+                    description: PullPolicy describes a policy for if/when to pull
+                      a container image
+                    type: string
                   imagePullSecrets:
                     items:
                       description: LocalObjectReference contains enough information
@@ -1102,6 +1114,8 @@ spec:
                 type: object
               scuttle:
                 properties:
+                  disableLogging:
+                    type: boolean
                   enabled:
                     type: string
                   envoyAdminApi:
@@ -1116,8 +1130,6 @@ spec:
                     type: boolean
                   quitWithoutEnvoyTimeout:
                     type: string
-                  scuttleLogging:
-                    type: boolean
                   startWithoutEnvoy:
                     type: boolean
                   waitForEnvoyTimeout:
@@ -1888,6 +1900,10 @@ spec:
                     type: array
                   image:
                     type: string
+                  imagePullPolicy:
+                    description: PullPolicy describes a policy for if/when to pull
+                      a container image
+                    type: string
                   imagePullSecrets:
                     items:
                       description: LocalObjectReference contains enough information
@@ -2129,6 +2145,7 @@ spec:
                 - created
                 - started
                 - finished
+                - error
                 type: string
             type: object
         type: object
diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml
index d087e5c..78f0459 100644
--- a/config/manager/kustomization.yaml
+++ b/config/manager/kustomization.yaml
@@ -1,9 +1,8 @@
----
 resources:
-  - manager.yaml
+- manager.yaml
 images:
-  - name: controller
-    newName: ghcr.io/grafana/operator
-    newTag: latest
+- name: controller
+  newName: ghcr.io/grafana/operator
+  newTag: latest
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
diff --git a/controllers/k6_controller.go b/controllers/k6_controller.go
index 03a2e16..7f4bf33 100644
--- a/controllers/k6_controller.go
+++ b/controllers/k6_controller.go
@@ -17,6 +17,14 @@ package controllers
 import (
 	"context"
 	"fmt"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
 
 	"github.com/go-logr/logr"
 	"github.com/grafana/k6-operator/api/v1alpha1"
@@ -27,6 +35,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+const k6CrLabelName = "k6_cr"
+
 // K6Reconciler reconciles a K6 object
 type K6Reconciler struct {
 	client.Client
@@ -42,7 +52,7 @@ type K6Reconciler struct {
 // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
 func (r *K6Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	ctx := context.Background()
-	log := r.Log.WithValues("k6", req.NamespacedName)
+	log := r.Log.WithValues("namespace", req.Namespace, "name", req.Name)
 
 	// Fetch the CRD
 	k6 := &v1alpha1.K6{}
@@ -62,10 +72,7 @@ func (r *K6Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	case "":
 		return InitializeJobs(ctx, log, k6, r)
 	case "initialization":
-		// here we're just waiting until initialize is done
-		// Note: it is present as a separate stage to ensure there's only one
-		// initialization job at a time
-		return ctrl.Result{}, nil
+		return RunValidations(ctx, log, k6, r)
 	case "initialized":
 		return CreateJobs(ctx, log, k6, r)
 	case "created":
@@ -73,7 +80,7 @@ func (r *K6Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	case "started":
 		// wait for test to finish and then mark as finished
 		return FinishJobs(ctx, log, k6, r)
-	case "finished":
+	case "error", "finished":
 		// delete if configured
 		if k6.Spec.Cleanup == "post" {
 			log.Info("Cleaning up all resources")
@@ -93,5 +100,27 @@ func (r *K6Reconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.K6{}).
 		Owns(&batchv1.Job{}).
+		Watches(&source.Kind{Type: &v1.Pod{}},
+			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(
+				func(object handler.MapObject) []reconcile.Request {
+					pod := object.Object.(*v1.Pod)
+					k6CrName, ok := pod.GetLabels()[k6CrLabelName]
+					if !ok {
+						return nil
+					}
+					return []reconcile.Request{
+						{NamespacedName: types.NamespacedName{
+							Name:      k6CrName,
+							Namespace: object.Meta.GetNamespace(),
+						}}}
+				})},
+			builder.WithPredicates(predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) bool {
+				pod := object.(*v1.Pod)
+				_, ok := pod.GetLabels()[k6CrLabelName]
+				if !ok {
+					return false
+				}
+				return true
+			}))).
 		Complete(r)
 }
diff --git a/controllers/k6_finish.go b/controllers/k6_finish.go
index 8dffd10..d633bb9 100644
--- a/controllers/k6_finish.go
+++ b/controllers/k6_finish.go
@@ -3,7 +3,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"time"
 
 	"github.com/go-logr/logr"
 	"github.com/grafana/k6-operator/api/v1alpha1"
@@ -11,63 +10,46 @@ import (
 	"github.com/grafana/k6-operator/pkg/types"
 	batchv1 "k8s.io/api/batch/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/util/wait"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 // FinishJobs waits for the pods to finish, performs finishing call for cloud output and moves state to "finished".
 func FinishJobs(ctx context.Context, log logr.Logger, k6 *v1alpha1.K6, r *K6Reconciler) (ctrl.Result, error) {
-	log.Info("Waiting for pods to finish")
+	log.Info("Checking if all runner pods are finished")
 
-	// Here we assume that the test runs for some time and there is no need to
-	// check it more often than twice in a minute.
-	//
-	// The total timeout for the test is set to duration of the test + 2 min.
-	// These 2 min are meant to cover the time needed to start the pods: sometimes
-	// pods are ready a bit later than operator reaches this stage so from the
-	// viewpoint of operator it takes longer. This behaviour depends on the setup of
-	// cluster. 2 min are meant to be a sufficient safeguard for such cases.
-
-	testDuration := inspectOutput.TotalDuration.TimeDuration()
-
-	err := wait.PollImmediate(time.Second*30, testDuration+time.Minute*2, func() (done bool, err error) {
-		selector := labels.SelectorFromSet(map[string]string{
-			"app":    "k6",
-			"k6_cr":  k6.Name,
-			"runner": "true",
-		})
-
-		opts := &client.ListOptions{LabelSelector: selector, Namespace: k6.Namespace}
-		jl := &batchv1.JobList{}
+	var err error
+	selector := labels.SelectorFromSet(map[string]string{
+		"app":    "k6",
+		"k6_cr":  k6.Name,
+		"runner": "true",
+	})
 
-		if err := r.List(ctx, jl, opts); err != nil {
-			log.Error(err, "Could not list jobs")
-			return false, nil
-		}
+	opts := &client.ListOptions{LabelSelector: selector, Namespace: k6.Namespace}
+	jl := &batchv1.JobList{}
 
-		// TODO: We should distinguish between Suceeded/Failed/Unknown
-		var finished int32
-		for _, job := range jl.Items {
-			if job.Status.Active != 0 {
-				continue
-			}
-			finished++
-		}
-
-		log.Info(fmt.Sprintf("%d/%d jobs complete", finished, k6.Spec.Parallelism))
+	if err := r.List(ctx, jl, opts); err != nil {
+		log.Error(err, "Could not list jobs")
+		return ctrl.Result{}, nil
+	}
 
-		if finished >= k6.Spec.Parallelism {
-			return true, nil
+	// TODO: We should distinguish between Suceeded/Failed/Unknown
+	var finished int32
+	for _, job := range jl.Items {
+		if job.Status.Active != 0 {
+			continue
 		}
+		finished++
+	}
 
-		return false, nil
-	})
+	log.Info(fmt.Sprintf("%d/%d jobs complete", finished, k6.Spec.Parallelism))
 
-	if err != nil {
-		log.Error(err, "Waiting for pods to finish ended with error")
+	if finished < k6.Spec.Parallelism {
+		return ctrl.Result{}, nil
 	}
 
+	log.Info("All runner pods are finished")
+
 	// If this is a test run with cloud output, try to finalize it regardless.
 	if cli := types.ParseCLI(&k6.Spec); cli.HasCloudOut {
 		if err = cloud.FinishTestRun(testRunId); err != nil {
diff --git a/controllers/k6_initialize.go b/controllers/k6_initialize.go
index ef3a609..aa2fcf5 100644
--- a/controllers/k6_initialize.go
+++ b/controllers/k6_initialize.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/grafana/k6-operator/pkg/types"
 	"io"
 	"time"
 
@@ -12,11 +13,9 @@ import (
 	"github.com/grafana/k6-operator/api/v1alpha1"
 	"github.com/grafana/k6-operator/pkg/cloud"
 	"github.com/grafana/k6-operator/pkg/resources/jobs"
-	"github.com/grafana/k6-operator/pkg/types"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -54,100 +53,109 @@ func InitializeJobs(ctx context.Context, log logr.Logger, k6 *v1alpha1.K6, r *K6
 
 	if err = ctrl.SetControllerReference(k6, initializer, r.Scheme); err != nil {
 		log.Error(err, "Failed to set controller reference for the initialize job")
-		return
+		return res, err
 	}
 
 	if err = r.Create(ctx, initializer); err != nil {
 		log.Error(err, "Failed to launch k6 test initializer")
-		return
+		return res, err
 	}
-	err = wait.PollImmediate(time.Second*5, time.Second*60, func() (done bool, err error) {
-		var (
-			listOpts = &client.ListOptions{
-				Namespace: k6.Namespace,
-				LabelSelector: labels.SelectorFromSet(map[string]string{
-					"app":      "k6",
-					"k6_cr":    k6.Name,
-					"job-name": fmt.Sprintf("%s-initializer", k6.Name),
-				}),
-			}
-			podList = &corev1.PodList{}
-		)
-		if err := r.List(ctx, podList, listOpts); err != nil {
-			log.Error(err, "Could not list pods")
-			return false, err
-		}
-		if len(podList.Items) < 1 {
-			log.Info("No initializing pod found yet")
-			return false, nil
-		}
 
-		// there should be only 1 initializer pod
-		if podList.Items[0].Status.Phase != "Succeeded" {
-			log.Info("Waiting for initializing pod to finish")
-			return false, nil
-		}
-
-		// Here we need to get the output of the pod
-		// pods/log is not currently supported by controller-runtime client and it is officially
-		// recommended to use REST client instead:
-		// https://github.com/kubernetes-sigs/controller-runtime/issues/1229
+	return res, nil
+}
 
-		config, err := rest.InClusterConfig()
-		if err != nil {
-			log.Error(err, "unable to fetch in-cluster REST config")
-			// don't return here
-			return false, nil
-		}
+func RunValidations(ctx context.Context, log logr.Logger, k6 *v1alpha1.K6, r *K6Reconciler) (res ctrl.Result, err error) {
+	cli := types.ParseCLI(&k6.Spec)
 
-		clientset, err := kubernetes.NewForConfig(config)
-		if err != nil {
-			log.Error(err, "unable to get access to clientset")
-			// don't return here
-			return false, nil
+	var (
+		listOpts = &client.ListOptions{
+			Namespace: k6.Namespace,
+			LabelSelector: labels.SelectorFromSet(map[string]string{
+				"app":      "k6",
+				"k6_cr":    k6.Name,
+				"job-name": fmt.Sprintf("%s-initializer", k6.Name),
+			}),
 		}
-		req := clientset.CoreV1().Pods(k6.Namespace).GetLogs(podList.Items[0].Name, &corev1.PodLogOptions{
-			Container: "k6",
-		})
-		ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
-		defer cancel()
-
-		podLogs, err := req.Stream(ctx)
-		if err != nil {
-			log.Error(err, "unable to stream logs from the pod")
-			// don't return here
-			return false, nil
-		}
-		defer podLogs.Close()
+		podList = &corev1.PodList{}
+	)
+	if err = r.List(ctx, podList, listOpts); err != nil {
+		log.Error(err, "Could not list pods")
+		return ctrl.Result{}, err
+	}
+	if len(podList.Items) < 1 {
+		log.Info("No initializing pod found yet")
+		return ctrl.Result{}, err
+	}
 
-		buf := new(bytes.Buffer)
-		_, err = io.Copy(buf, podLogs)
-		if err != nil {
-			log.Error(err, "unable to copy logs from the pod")
-			return false, err
-		}
+	// there should be only 1 initializer pod
+	if podList.Items[0].Status.Phase != "Succeeded" {
+		log.Info("Waiting for initializing pod to finish")
+		return ctrl.Result{}, err
+	}
 
-		if err := json.Unmarshal(buf.Bytes(), &inspectOutput); err != nil {
-			// this shouldn't normally happen but if it does, let's log output by default
-			log.Error(err, fmt.Sprintf("unable to marshal: `%s`", buf.String()))
-			return true, err
-		}
+	// Here we need to get the output of the pod
+	// pods/log is not currently supported by controller-runtime client and it is officially
+	// recommended to use REST client instead:
+	// https://github.com/kubernetes-sigs/controller-runtime/issues/1229
 
-		log.Info(fmt.Sprintf("k6 inspect: %+v", inspectOutput))
-
-		if int32(inspectOutput.MaxVUs) < k6.Spec.Parallelism {
-			err = fmt.Errorf("number of instances > number of VUs")
-			// TODO maybe change this to a warning and simply set parallelism = maxVUs and proceed with execution?
-			// But logr doesn't seem to have warning level by default, only with V() method...
-			// It makes sense to return to this after / during logr VS logrus issue https://github.com/grafana/k6-operator/issues/84
-			log.Error(err, "Parallelism argument cannot be larger than maximum VUs in the script",
-				"maxVUs", inspectOutput.MaxVUs,
-				"parallelism", k6.Spec.Parallelism)
-			return false, err
-		}
+	config, err := rest.InClusterConfig()
+	if err != nil {
+		log.Error(err, "unable to fetch in-cluster REST config")
+		// don't return here
+		return ctrl.Result{}, err
+	}
 
-		return true, nil
+	clientset, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		log.Error(err, "unable to get access to clientset")
+		// don't return here
+		return ctrl.Result{}, err
+	}
+	req := clientset.CoreV1().Pods(k6.Namespace).GetLogs(podList.Items[0].Name, &corev1.PodLogOptions{
+		Container: "k6",
 	})
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
+	defer cancel()
+
+	podLogs, err := req.Stream(ctx)
+	if err != nil {
+		log.Error(err, "unable to stream logs from the pod")
+		// don't return here
+		return ctrl.Result{}, err
+	}
+	defer podLogs.Close()
+
+	buf := new(bytes.Buffer)
+	_, err = io.Copy(buf, podLogs)
+	if err != nil {
+		log.Error(err, "unable to copy logs from the pod")
+		return ctrl.Result{}, err
+	}
+
+	if err := json.Unmarshal(buf.Bytes(), &inspectOutput); err != nil {
+		// this shouldn't normally happen but if it does, let's log output by default
+		log.Error(err, fmt.Sprintf("unable to marshal: `%s`", buf.String()))
+		return ctrl.Result{}, err
+	}
+
+	log.Info(fmt.Sprintf("k6 inspect: %+v", inspectOutput))
+
+	if int32(inspectOutput.MaxVUs) < k6.Spec.Parallelism {
+		err = fmt.Errorf("number of instances > number of VUs")
+		// TODO maybe change this to a warning and simply set parallelism = maxVUs and proceed with execution?
+		// But logr doesn't seem to have warning level by default, only with V() method...
+		// It makes sense to return to this after / during logr VS logrus issue https://github.com/grafana/k6-operator/issues/84
+		log.Error(err, "Parallelism argument cannot be larger than maximum VUs in the script",
+			"maxVUs", inspectOutput.MaxVUs,
+			"parallelism", k6.Spec.Parallelism)
+
+		k6.Status.Stage = "error"
+		if err = r.Client.Status().Update(ctx, k6); err != nil {
+			log.Error(err, "Could not update status of custom resource")
+			return
+		}
+		return ctrl.Result{}, err
+	}
 
 	if err != nil {
 		log.Error(err, "Failed to initialize the script")
diff --git a/controllers/k6_start.go b/controllers/k6_start.go
index 8355efe..512bf28 100644
--- a/controllers/k6_start.go
+++ b/controllers/k6_start.go
@@ -3,17 +3,15 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"net/http"
-	"time"
-
 	"github.com/go-logr/logr"
 	"github.com/grafana/k6-operator/api/v1alpha1"
 	"github.com/grafana/k6-operator/pkg/resources/jobs"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/util/wait"
+	"net/http"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"time"
 )
 
 func isServiceReady(log logr.Logger, service *v1.Service) bool {
@@ -31,65 +29,65 @@ func isServiceReady(log logr.Logger, service *v1.Service) bool {
 func StartJobs(ctx context.Context, log logr.Logger, k6 *v1alpha1.K6, r *K6Reconciler) (ctrl.Result, error) {
 	log.Info("Waiting for pods to get ready")
 
-	err := wait.PollImmediate(time.Second*5, time.Second*60, func() (done bool, err error) {
-		selector := labels.SelectorFromSet(map[string]string{
-			"app":    "k6",
-			"k6_cr":  k6.Name,
-			"runner": "true",
-		})
-
-		opts := &client.ListOptions{LabelSelector: selector, Namespace: k6.Namespace}
-		pl := &v1.PodList{}
-		if e := r.List(ctx, pl, opts); e != nil {
-			log.Error(e, "Could not list pods")
-			return false, e
-		}
-
-		var count int
-		for _, pod := range pl.Items {
-			if pod.Status.Phase != "Running" {
-				continue
-			}
-			count++
-		}
+	selector := labels.SelectorFromSet(map[string]string{
+		"app":    "k6",
+		"k6_cr":  k6.Name,
+		"runner": "true",
+	})
 
-		log.Info(fmt.Sprintf("%d/%d pods ready", count, k6.Spec.Parallelism))
+	opts := &client.ListOptions{LabelSelector: selector, Namespace: k6.Namespace}
+	pl := &v1.PodList{}
+	if e := r.List(ctx, pl, opts); e != nil {
+		log.Error(e, "Could not list pods")
+		return ctrl.Result{}, e
+	}
 
-		if count != int(k6.Spec.Parallelism) {
-			return false, nil
+	var count int
+	for _, pod := range pl.Items {
+		if pod.Status.Phase != "Running" {
+			continue
 		}
+		count++
+	}
 
-		var hostnames []string
+	log.Info(fmt.Sprintf("%d/%d runner pods ready", count, k6.Spec.Parallelism))
 
-		sl := &v1.ServiceList{}
+	if count != int(k6.Spec.Parallelism) {
+		return ctrl.Result{}, nil
+	}
 
-		if e := r.List(ctx, sl, opts); e != nil {
-			log.Error(e, "Could not list services")
-			return false, e
-		}
+	var hostnames []string
+	var err error
+	sl := &v1.ServiceList{}
+
+	if e := r.List(ctx, sl, opts); e != nil {
+		log.Error(e, "Could not list services")
+		return ctrl.Result{}, e
+	}
 
-		for _, service := range sl.Items {
-			hostnames = append(hostnames, service.Spec.ClusterIP)
+	for _, service := range sl.Items {
+		hostnames = append(hostnames, service.Spec.ClusterIP)
 
-			if !isServiceReady(log, &service) {
-				log.Info(fmt.Sprintf("%v service is not ready, aborting", service.ObjectMeta.Name))
-				return false, nil
-			}
+		if !isServiceReady(log, &service) {
+			log.Info(fmt.Sprintf("%v service is not ready, aborting", service.ObjectMeta.Name))
+			return ctrl.Result{RequeueAfter: time.Second}, nil
+		} else {
+			log.Info(fmt.Sprintf("%v service is ready", service.ObjectMeta.Name))
 		}
+	}
 
-		starter := jobs.NewStarterJob(k6, hostnames)
+	starter := jobs.NewStarterJob(k6, hostnames)
 
-		if err = ctrl.SetControllerReference(k6, starter, r.Scheme); err != nil {
-			log.Error(err, "Failed to set controller reference for the start job")
-		}
+	if err = ctrl.SetControllerReference(k6, starter, r.Scheme); err != nil {
+		log.Error(err, "Failed to set controller reference for the start job")
+	}
 
-		if err = r.Create(ctx, starter); err != nil {
-			log.Error(err, "Failed to launch k6 test starter")
-			return true, err
-		}
+	if err = r.Create(ctx, starter); err != nil {
+		log.Error(err, "Failed to launch k6 test starter")
+		return ctrl.Result{}, err
+	}
 
-		return true, nil
-	})
+	log.Info("Created started job")
 
 	if err != nil {
 		log.Error(err, "Failed to start all jobs")
diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go
index 6e8965c..954e453 100644
--- a/pkg/cloud/cloud.go
+++ b/pkg/cloud/cloud.go
@@ -11,6 +11,7 @@ import (
 	"go.k6.io/k6/lib"
 	"go.k6.io/k6/lib/consts"
 	"go.k6.io/k6/lib/types"
+	"go.k6.io/k6/stats"
 	"gopkg.in/guregu/null.v3"
 )
 
@@ -23,9 +24,9 @@ type InspectOutput struct {
 			ProjectID int64  `json:"projectID"`
 		} `json:"loadimpact"`
 	} `json:"ext"`
-	TotalDuration types.NullDuration  `json:"totalDuration"`
-	MaxVUs        uint64              `json:"maxVUs"`
-	Thresholds    map[string][]string `json:"thresholds,omitempty"`
+	TotalDuration types.NullDuration           `json:"totalDuration"`
+	MaxVUs        uint64                       `json:"maxVUs"`
+	Thresholds    map[string]*stats.Thresholds `json:"thresholds,omitempty"`
 }
 
 type TestRun struct {
@@ -56,8 +57,11 @@ func CreateTestRun(opts InspectOutput, instances int32, host, token string, log
 		Level:     logrus.InfoLevel,
 	}
 
-	if opts.Thresholds == nil {
-		opts.Thresholds = make(map[string][]string)
+	thresholds := make(map[string][]string, len(opts.Thresholds))
+	for name, t := range opts.Thresholds {
+		for _, threshold := range t.Thresholds {
+			thresholds[name] = append(thresholds[name], threshold.Source)
+		}
 	}
 
 	if len(host) == 0 {
@@ -69,7 +73,7 @@ func CreateTestRun(opts InspectOutput, instances int32, host, token string, log
 		Name:              opts.External.Loadimpact.Name,
 		ProjectID:         cloudConfig.ProjectID.Int64,
 		VUsMax:            int64(opts.MaxVUs),
-		Thresholds:        opts.Thresholds,
+		Thresholds:        thresholds,
 		Duration:          int64(opts.TotalDuration.TimeDuration().Seconds()),
 		ProcessThresholds: true,
 		Instances:         instances,
diff --git a/pkg/resources/jobs/helpers.go b/pkg/resources/jobs/helpers.go
index 3a8110a..b0d09b5 100644
--- a/pkg/resources/jobs/helpers.go
+++ b/pkg/resources/jobs/helpers.go
@@ -84,10 +84,10 @@ func newIstioEnvVar(istio v1alpha1.K6Scuttle, istioEnabled bool) []corev1.EnvVar
 			})
 		}
 
-		if istio.ScuttleLogging {
+		if istio.DisableLogging {
 			env = append(env, corev1.EnvVar{
 				Name:  "SCUTTLE_LOGGING",
-				Value: strconv.FormatBool(istio.ScuttleLogging),
+				Value: strconv.FormatBool(false),
 			})
 		}
 
diff --git a/pkg/resources/jobs/initializer.go b/pkg/resources/jobs/initializer.go
index 59e34d9..3837f8e 100644
--- a/pkg/resources/jobs/initializer.go
+++ b/pkg/resources/jobs/initializer.go
@@ -54,7 +54,7 @@ func NewInitializerJob(k6 *v1alpha1.K6, argLine string) (*batchv1.Job, error) {
 	var (
 		// k6 allows to run archive command on archives too so type of file here doesn't matter
 		scriptName  = script.FullName()
-		archiveName = fmt.Sprintf("./%s.archived.tar", script.Filename)
+		archiveName = fmt.Sprintf("/tmp/%s.archived.tar", script.Filename)
 	)
 	command, istioEnabled := newIstioCommand(k6.Spec.Scuttle.Enabled, []string{"sh", "-c"})
 	command = append(command, fmt.Sprintf(


Test patch:
diff --git a/pkg/resources/jobs/helpers_test.go b/pkg/resources/jobs/helpers_test.go
index 70def50..eee2eb2 100644
--- a/pkg/resources/jobs/helpers_test.go
+++ b/pkg/resources/jobs/helpers_test.go
@@ -110,7 +110,7 @@ func TestNewIstioEnvVarTrueValues(t *testing.T) {
 		},
 		{
 			Name:  "SCUTTLE_LOGGING",
-			Value: "true",
+			Value: "false",
 		},
 	}
 
@@ -118,7 +118,7 @@ func TestNewIstioEnvVarTrueValues(t *testing.T) {
 		EnvoyAdminApi:       "",
 		IstioQuitApi:        "",
 		WaitForEnvoyTimeout: "",
-		ScuttleLogging:      true,
+		DisableLogging:      true,
 	}, true)
 
 	if !reflect.DeepEqual(envVars, expectedOutcome) {
@@ -146,7 +146,7 @@ func TestNewIstioEnvVarFalseValues(t *testing.T) {
 		EnvoyAdminApi:       "",
 		IstioQuitApi:        "",
 		WaitForEnvoyTimeout: "",
-		ScuttleLogging:      false,
+		DisableLogging:      false,
 	}, true)
 
 	if !reflect.DeepEqual(envVars, expectedOutcome) {
diff --git a/pkg/resources/jobs/initializer_test.go b/pkg/resources/jobs/initializer_test.go
index 53248e3..861025c 100644
--- a/pkg/resources/jobs/initializer_test.go
+++ b/pkg/resources/jobs/initializer_test.go
@@ -60,7 +60,7 @@ func TestNewInitializerJob(t *testing.T) {
 							Name:            "k6",
 							Command: []string{
 								"sh", "-c",
-								"k6 archive /test/test.js -O ./test.js.archived.tar --out cloud 2> /tmp/k6logs && k6 inspect --execution-requirements ./test.js.archived.tar 2> /tmp/k6logs ; ! cat /tmp/k6logs | grep 'level=error'",
+								"k6 archive /test/test.js -O /tmp/test.js.archived.tar --out cloud 2> /tmp/k6logs && k6 inspect --execution-requirements /tmp/test.js.archived.tar 2> /tmp/k6logs ; ! cat /tmp/k6logs | grep 'level=error'",
 							},
 							Env:          []corev1.EnvVar{},
 							Resources:    corev1.ResourceRequirements{},