Repository: iter8-tools/iter8
Pull request #1217 (2022-04-10 21:20:45)

Kubernetes experiments
While attempting to run Kubernetes, I ran into a bunch of problems. This PR fixes some of them, and includes tests for them.

- [x] Created a post-release test for Kubernetes experimentation. This is post-release since we want to test with the Docker image created by the release process.
- [x] [A previous PR](#1206) removed payload / other URLs from HTTP and gRPC tasks in favor of local files. This PR enables them through charts (and curl commands). So payload URLs can be specified, without the need to worry about certificates etc. during load tests.
- [x] I had to remove the Docker build and push from Makefile. `ITER8_IMG ?= iter8/iter8:edge` is an invalid setting in makefile due to the presence of the colon. Additionally, using a colon in variable/filename is complicated as documented [here](https://stackoverflow.com/questions/2052406/escaping-colons-in-filenames-in-a-makefile). In particular, `make clean install` fails if I include that Docker build/push conf, and passes otherwise. Given that `docker build` and `push` can be accomplished through one-liners outside of the makefile, and given that we don't have a real need to build Docker images through CLIs, and given that it is starting to interfere with other commands in the makefile that are actually being used both during dev, and as part of CI, I think it makes sense to remove Docker build/push from makefile. Of course, if this is really required, we can enable it again in the future.
- [x] HTTP task now supports only a single version. This simplification makes every part of the code (iter8 CLI + charts) simpler. We can consider re-enabling multiple versions in the task if it ever becomes required during load tests in the future.
- [x] Removed appVersions which partly addresses #1212
- [x] Minor changes (like replacing the word subcommand with command in some places)
- [x] Previously, the owner of a result secret was the job that created it. Now it is the spec secret associated with it. They both have similar effect upon upgrades/deletions of experiments, however the code becomes simpler now.

***
**What remains to be done**

- [ ] Set `values.yaml` appropriately and also create proper documentation with `values.yaml`. TBD after the merge, since we will use the task  links within values.yaml docs to describe parameters. 




Patch:
diff --git a/.github/workflows/assets.yaml b/.github/workflows/assets.yaml
index a6770e1c..f12d635d 100644
--- a/.github/workflows/assets.yaml
+++ b/.github/workflows/assets.yaml
@@ -92,3 +92,31 @@ jobs:
         push: true
         build-args: |
           TAG=v${{ env.VERSION }}
+
+  kubernetes-experiment:
+    name: Kubernetes http load test    
+    runs-on: ubuntu-latest
+    needs: build-and-push
+    steps:
+    - name: Install Iter8
+      run: |
+        tagref=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
+        wget https://github.com/iter8-tools/iter8/releases/download/$tagref/iter8-linux-amd64.tar.gz
+        tar -xvf iter8-linux-amd64.tar.gz
+        mv linux-amd64/iter8 /usr/local/bin
+    - uses: engineerd/setup-kind@v0.5.0
+    - name: create app
+      run: |
+        kubectl create deployment httpbin --image=kennethreitz/httpbin
+        kubectl expose deployment httpbin --type=ClusterIP --port=80
+    - name: iter8 k launch
+      run: |
+        iter8 k launch -c load-test-http \
+        --set url="http://httpbin.default/get" \
+        --set payloadURL="https://httpbin.org/stream/1"
+    - name: try other iter8 k commands
+      run: |
+        iter8 k assert -c completed -c nofailure --timeout 60s
+        iter8 k report
+        iter8 k log
+        iter8 k delete
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f89fd7c6..b75b32bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -144,3 +144,6 @@ iter8
 # iter8lib tgz
 iter8lib*.tgz
 
+# data
+payload.dat
+
diff --git a/Makefile b/Makefile
index 27c4da9a..6b0c8e4a 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,6 @@ INSTALL_PATH ?= /usr/local/bin
 DIST_DIRS   := find * -type d -exec
 TARGETS     := darwin/amd64 linux/amd64 linux/386 windows/amd64
 BINNAME     ?= iter8
-ITER8_IMG ?= iter8/iter8:edge
 
 GOBIN         = $(shell go env GOBIN)
 ifeq ($(GOBIN),)
@@ -84,17 +83,6 @@ dist: build-cross
 clean:
 	@rm -rf '$(BINDIR)' ./_dist
 
-# ------------------------------------------------------------------------------
-#	 Docker
-
-.PHONY: docker-build
- docker-build:
- 	docker build -f Dockerfile -t $(ITER8_IMG) .
-
- .PHONY: docker-push
- docker-push:
- 	docker push $(ITER8_IMG)
-
 # ------------------------------------------------------------------------------
 #  test
 
diff --git a/base/collect_grpc.go b/base/collect_grpc.go
index e5606039..81441e76 100644
--- a/base/collect_grpc.go
+++ b/base/collect_grpc.go
@@ -9,12 +9,6 @@ import (
 	gd "github.com/mcuadros/go-defaults"
 )
 
-// // collectGRPCInputs holds all the inputs for this task
-// type collectGRPCInputs struct {
-// 	// Config is the ghz runner.Config object embedded in the input
-// 	runner.Config
-// }
-
 const (
 	// CollectGRPCTaskName is the name of this task which performs load generation and metrics collection for gRPC services.
 	CollectGRPCTaskName = "gen-load-and-collect-metrics-grpc"
diff --git a/base/collect_http.go b/base/collect_http.go
index 1d9db625..44563415 100644
--- a/base/collect_http.go
+++ b/base/collect_http.go
@@ -1,7 +1,6 @@
 package base
 
 import (
-	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -14,14 +13,6 @@ import (
 	log "github.com/iter8-tools/iter8/base/log"
 )
 
-// versionHTTP contains header and url information needed to send requests to each version.
-type versionHTTP struct {
-	// HTTP headers to use in the query for this version; optional
-	Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
-	// URL to use for querying this version
-	URL string `json:"url" yaml:"url"`
-}
-
 // errorRange has lower and upper limits for HTTP status codes. HTTP status code within this range is considered an error
 type errorRange struct {
 	// Lower end of the range
@@ -32,26 +23,28 @@ type errorRange struct {
 
 // collectHTTPInputs contain the inputs to the metrics collection task to be executed.
 type collectHTTPInputs struct {
-	// NumRequests is the number of requests to be sent to each version. Default value is 100.
+	// NumRequests is the number of requests to be sent to the app. Default value is 100.
 	NumRequests *int64 `json:"numRequests" yaml:"numRequests"`
 	// Duration of this task. Specified in the Go duration string format (example, 5s). If both duration and numQueries are specified, then duration is ignored.
 	Duration *string `json:"duration" yaml:"duration"`
-	// QPS is the number of requests per second sent to each version. Default value is 8.0.
+	// QPS is the number of requests per second sent to the app. Default value is 8.0.
 	QPS *float32 `json:"qps" yaml:"qps"`
 	// Connections is the number of number of parallel connections used to send load. Default value is 4.
 	Connections *int `json:"connections" yaml:"connections"`
-	// PayloadStr is the string data to be sent as payload. If this field is specified, Iter8 will send HTTP POST requests to versions using this string as the payload.
+	// PayloadStr is the string data to be sent as payload. If this field is specified, Iter8 will send HTTP POST requests to the app using this string as the payload.
 	PayloadStr *string `json:"payloadStr" yaml:"payloadStr"`
-	// PayloadFile is payload file. If this field is specified, Iter8 will send HTTP POST requests to versions using data in this file. If both `payloadStr` and `payloadFile` are specified, the former is ignored.
+	// PayloadFile is payload file. If this field is specified, Iter8 will send HTTP POST requests to the app using data in this file. If both `payloadStr` and `payloadFile` are specified, the former is ignored.
 	PayloadFile *string `json:"payloadFile" yaml:"payloadFile"`
-	// ContentType is the type of the payload. Indicated using the Content-Type HTTP header value. This is intended to be used in conjunction with one of the `payload*` fields above. If this field is specified, Iter8 will send HTTP POST requests to versions using this content type header value.
+	// ContentType is the type of the payload. Indicated using the Content-Type HTTP header value. This is intended to be used in conjunction with one of the `payload*` fields above. If this field is specified, Iter8 will send HTTP POST requests to the app using this content type header value.
 	ContentType *string `json:"contentType" yaml:"contentType"`
 	// ErrorRanges is a list of errorRange values. Each range specifies an upper and/or lower limit on HTTP status codes. HTTP responses that fall within these error ranges are considered error. Default value is {{lower: 400},} - i.e., HTTP status codes >= 400 are considered as error.
 	ErrorRanges []errorRange `json:"errorRanges" yaml:"errorRanges"`
 	// Percentiles are the latency percentiles collected by this task. Percentile values have a single digit precision (i.e., rounded to one decimal place). Default value is {50.0, 75.0, 90.0, 95.0, 99.0, 99.9,}.
 	Percentiles []float64 `json:"percentiles" yaml:"percentiles"`
-	// VersionInfo is a non-empty list of version values.
-	VersionInfo []*versionHTTP `json:"versionInfo" yaml:"versionInfo"`
+	// HTTP headers to use in the query; optional
+	Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
+	// URL to use for querying the app
+	URL string `json:"url" yaml:"url"`
 }
 
 const (
@@ -145,7 +138,7 @@ func (t *collectHTTPTask) validateInputs() error {
 }
 
 // getFortioOptions constructs Fortio's HTTP runner options based on collect task inputs
-func (t *collectHTTPTask) getFortioOptions(j int) (*fhttp.HTTPRunnerOptions, error) {
+func (t *collectHTTPTask) getFortioOptions() (*fhttp.HTTPRunnerOptions, error) {
 	fortioLog.SetOutput(io.Discard)
 	// basic runner
 	fo := &fhttp.HTTPRunnerOptions{
@@ -157,7 +150,7 @@ func (t *collectHTTPTask) getFortioOptions(j int) (*fhttp.HTTPRunnerOptions, err
 			Out:         io.Discard,
 		},
 		HTTPOptions: fhttp.HTTPOptions{
-			URL: t.With.VersionInfo[j].URL,
+			URL: t.With.URL,
 		},
 	}
 
@@ -196,18 +189,18 @@ func (t *collectHTTPTask) getFortioOptions(j int) (*fhttp.HTTPRunnerOptions, err
 	}
 
 	// headers
-	for key, value := range t.With.VersionInfo[j].Headers {
+	for key, value := range t.With.Headers {
 		fo.AddAndValidateExtraHeader(key + ":" + value)
 	}
 
 	return fo, nil
 }
 
-// resultForVersion collects Fortio result for a given version
-func (t *collectHTTPTask) resultForVersion(j int) (*fhttp.HTTPRunnerResults, error) {
+// getFortioResults collects Fortio run results
+func (t *collectHTTPTask) getFortioResults() (*fhttp.HTTPRunnerResults, error) {
 	// the main idea is to run Fortio with proper options
 
-	fo, err := t.getFortioOptions(j)
+	fo, err := t.getFortioOptions()
 	if err != nil {
 		return nil, err
 	}
@@ -233,131 +226,111 @@ func (t *collectHTTPTask) run(exp *Experiment) error {
 
 	t.initializeDefaults()
 
-	if len(t.With.VersionInfo) == 0 {
-		log.Logger.Error("collect task must specify info for at least one version")
-		return errors.New("collect task must specify info for at least one version")
-	}
-
-	fm := make([]*fhttp.HTTPRunnerResults, len(t.With.VersionInfo))
-
-	// run fortio queries for each version sequentially
-	for j := range t.With.VersionInfo {
-		log.Logger.Trace("initiating fortio for version ", j)
-		var data *fhttp.HTTPRunnerResults
-		var err error
-		if t.With.VersionInfo[j] == nil {
-			data = nil
-		} else {
-			data, err = t.resultForVersion(j)
-			if err == nil {
-				fm[j] = data
-			} else {
-				return err
-			}
-		}
+	// run fortio
+	data, err := t.getFortioResults()
+	if err != nil {
+		return err
 	}
 
-	// this task creates insights for different versions
-	// initialize num versions
-	err = exp.Result.initInsightsWithNumVersions(len(t.With.VersionInfo))
+	// this task populates insights in the experiment
+	// hence, initialize insights with num versions (= 1)
+	err = exp.Result.initInsightsWithNumVersions(1)
 	if err != nil {
 		return err
 	}
 	in := exp.Result.Insights
 
-	for i := range t.With.VersionInfo {
-		if fm[i] != nil {
-			// request count
-			m := httpMetricPrefix + "/" + builtInHTTPRequestCountId
-			mm := MetricMeta{
-				Description: "number of requests sent",
-				Type:        CounterMetricType,
-			}
-			in.updateMetric(m, mm, i, float64(fm[i].DurationHistogram.Count))
-
-			// error count & rate
-			val := float64(0)
-			for code, count := range fm[i].RetCodes {
-				if t.errorCode(code) {
-					val += float64(count)
-				}
-			}
-			// error count
-			m = httpMetricPrefix + "/" + builtInHTTPErrorCountId
-			mm = MetricMeta{
-				Description: "number of responses that were errors",
-				Type:        CounterMetricType,
-			}
-			in.updateMetric(m, mm, i, val)
-
-			// error-rate
-			m = httpMetricPrefix + "/" + builtInHTTPErrorRateId
-			rc := float64(fm[i].DurationHistogram.Count)
-			if rc != 0 {
-				mm = MetricMeta{
-					Description: "fraction of responses that were errors",
-					Type:        GaugeMetricType,
-				}
-				in.updateMetric(m, mm, i, val/rc)
-			}
+	if data != nil {
+		// request count
+		m := httpMetricPrefix + "/" + builtInHTTPRequestCountId
+		mm := MetricMeta{
+			Description: "number of requests sent",
+			Type:        CounterMetricType,
+		}
+		in.updateMetric(m, mm, 0, float64(data.DurationHistogram.Count))
 
-			// mean-latency
-			m = httpMetricPrefix + "/" + builtInHTTPLatencyMeanId
-			mm = MetricMeta{
-				Description: "mean of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
+		// error count & rate
+		val := float64(0)
+		for code, count := range data.RetCodes {
+			if t.errorCode(code) {
+				val += float64(count)
 			}
-			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Avg)
+		}
+		// error count
+		m = httpMetricPrefix + "/" + builtInHTTPErrorCountId
+		mm = MetricMeta{
+			Description: "number of responses that were errors",
+			Type:        CounterMetricType,
+		}
+		in.updateMetric(m, mm, 0, val)
 
-			// stddev-latency
-			m = httpMetricPrefix + "/" + builtInHTTPLatencyStdDevId
+		// error-rate
+		m = httpMetricPrefix + "/" + builtInHTTPErrorRateId
+		rc := float64(data.DurationHistogram.Count)
+		if rc != 0 {
 			mm = MetricMeta{
-				Description: "standard deviation of observed latency values",
+				Description: "fraction of responses that were errors",
 				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
 			}
-			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.StdDev)
+			in.updateMetric(m, mm, 0, val/rc)
+		}
 
-			// min-latency
-			m = httpMetricPrefix + "/" + builtInHTTPLatencyMinId
-			mm = MetricMeta{
-				Description: "minimum of observed latency values",
-				Type:        GaugeMetricType,
-				Units:       StringPointer("msec"),
-			}
-			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Min)
+		// mean-latency
+		m = httpMetricPrefix + "/" + builtInHTTPLatencyMeanId
+		mm = MetricMeta{
+			Description: "mean of observed latency values",
+			Type:        GaugeMetricType,
+			Units:       StringPointer("msec"),
+		}
+		in.updateMetric(m, mm, 0, 1000.0*data.DurationHistogram.Avg)
+
+		// stddev-latency
+		m = httpMetricPrefix + "/" + builtInHTTPLatencyStdDevId
+		mm = MetricMeta{
+			Description: "standard deviation of observed latency values",
+			Type:        GaugeMetricType,
+			Units:       StringPointer("msec"),
+		}
+		in.updateMetric(m, mm, 0, 1000.0*data.DurationHistogram.StdDev)
+
+		// min-latency
+		m = httpMetricPrefix + "/" + builtInHTTPLatencyMinId
+		mm = MetricMeta{
+			Description: "minimum of observed latency values",
+			Type:        GaugeMetricType,
+			Units:       StringPointer("msec"),
+		}
+		in.updateMetric(m, mm, 0, 1000.0*data.DurationHistogram.Min)
+
+		// max-latency
+		m = httpMetricPrefix + "/" + builtInHTTPLatencyMaxId
+		mm = MetricMeta{
+			Description: "maximum of observed latency values",
+			Type:        GaugeMetricType,
+			Units:       StringPointer("msec"),
+		}
+		in.updateMetric(m, mm, 0, 1000.0*data.DurationHistogram.Max)
 
-			// max-latency
-			m = httpMetricPrefix + "/" + builtInHTTPLatencyMaxId
+		// percentiles
+		for _, p := range data.DurationHistogram.Percentiles {
+			m = fmt.Sprintf("%v/%v%v", httpMetricPrefix, builtInHTTPLatencyPercentilePrefix, p.Percentile)
 			mm = MetricMeta{
-				Description: "maximum of observed latency values",
+				Description: fmt.Sprintf("%v-th percentile of observed latency values", p.Percentile),
 				Type:        GaugeMetricType,
 				Units:       StringPointer("msec"),
 			}
-			in.updateMetric(m, mm, i, 1000.0*fm[i].DurationHistogram.Max)
-
-			// percentiles
-			for _, p := range fm[i].DurationHistogram.Percentiles {
-				m = fmt.Sprintf("%v/%v%v", httpMetricPrefix, builtInHTTPLatencyPercentilePrefix, p.Percentile)
-				mm = MetricMeta{
-					Description: fmt.Sprintf("%v-th percentile of observed latency values", p.Percentile),
-					Type:        GaugeMetricType,
-					Units:       StringPointer("msec"),
-				}
-				in.updateMetric(m, mm, i, 1000.0*p.Value)
-			}
+			in.updateMetric(m, mm, 0, 1000.0*p.Value)
+		}
 
-			// latency histogram
-			m = httpMetricPrefix + "/" + builtInHTTPLatencyHistId
-			mm = MetricMeta{
-				Description: "Latency Histogram",
-				Type:        HistogramMetricType,
-				Units:       StringPointer("msec"),
-			}
-			lh := latencyHist(fm[i].DurationHistogram)
-			in.updateMetric(m, mm, i, lh)
+		// latency histogram
+		m = httpMetricPrefix + "/" + builtInHTTPLatencyHistId
+		mm = MetricMeta{
+			Description: "Latency Histogram",
+			Type:        HistogramMetricType,
+			Units:       StringPointer("msec"),
 		}
+		lh := latencyHist(data.DurationHistogram)
+		in.updateMetric(m, mm, 0, lh)
 	}
 	return nil
 }
diff --git a/base/util.go b/base/util.go
index 323447f8..b5b19653 100644
--- a/base/util.go
+++ b/base/util.go
@@ -11,7 +11,7 @@ var MajorMinor = "v0.10"
 
 // Version is the semantic version of Iter8 (with the `v` prefix)
 // Version is intended to be set using LDFLAGS at build time
-var Version = "v0.10.1"
+var Version = "v0.10.3"
 
 // int64Pointer takes an int64 as input, creates a new variable with the input value, and returns a pointer to the variable
 func int64Pointer(i int64) *int64 {
diff --git a/charts/iter8lib/Chart.yaml b/charts/iter8lib/Chart.yaml
index a3095cbf..7053cd7c 100644
--- a/charts/iter8lib/Chart.yaml
+++ b/charts/iter8lib/Chart.yaml
@@ -1,6 +1,6 @@
 apiVersion: v2
 name: iter8lib
-version: 0.9.1
+version: 0.10.0
 description: Iter8 library chart
 type: library
 home: https://iter8.tools
@@ -11,4 +11,3 @@ maintainers:
   email: spartha@us.ibm.com
   url: https://researcher.watson.ibm.com/researcher/view.php?person=us-spartha
 icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png
-appVersion: v0.9
diff --git a/charts/iter8lib/README.md b/charts/iter8lib/README.md
index a65a4c17..b2a3fdfa 100644
--- a/charts/iter8lib/README.md
+++ b/charts/iter8lib/README.md
@@ -1,18 +1,3 @@
 # Iter8 library chart
 
 > This chart provides reusable templates that are used by Iter8 experiment charts. Experiments can be composed by using this chart as a dependency, and by including templates available in this library chart.
-
-**Templates for Iter8 tasks:** 
-
-- `task.http`: The `gen-load-and-collect-metrics-http` task
-- `task.grpc`: The `gen-load-and-collect-metrics-grpc` task
-- `task.assess`: The `assess-app-versions` task
-
-**Templates for Kubernetes experiments:**
-
-- `k.job`: Kubernetes experiment job
-- `k.spec.secret`: Kubernetes secret containing experiment spec
-- `k.spec.role`: Role for Kubernetes spec secret
-- `k.spec.rolebinding`: Role binding for Kubernetes spec secret
-- `k.result.role`: Role for Kubernetes result secret
-- `k.result.rolebinding`: Role binding for Kubernetes result secret
diff --git a/charts/iter8lib/templates/_k-job.tpl b/charts/iter8lib/templates/_k-job.tpl
index b610e975..c41e2ce9 100644
--- a/charts/iter8lib/templates/_k-job.tpl
+++ b/charts/iter8lib/templates/_k-job.tpl
@@ -9,7 +9,7 @@ spec:
     spec:
       containers:
       - name: iter8
-        image: iter8-tools/iter8:{{  trimPrefix "v" .Chart.AppVersion }}
+        image: {{  .Values.iter8lib.global.iter8Image }}
         imagePullPolicy: Always
         command:
         - "/bin/sh"
diff --git a/charts/iter8lib/templates/_task-grpc.tpl b/charts/iter8lib/templates/_task-grpc.tpl
index e48ee493..579a3060 100644
--- a/charts/iter8lib/templates/_task-grpc.tpl
+++ b/charts/iter8lib/templates/_task-grpc.tpl
@@ -1,7 +1,45 @@
 {{- define "task.grpc" -}}
-# task: generate gRPC requests for the given gRPC method
+{{/* Validate values */}}
+{{- if not .Values.host }}
+{{- fail "Please set a value for the host parameter." }}
+{{- end }}
+{{- if not .Values.call }}
+{{- fail "Please set a value for the call parameter." }}
+{{- end }}
+{{/* Perform the various setup steps before the main task */}}
+{{- $vals := mustDeepCopy .Values }}
+{{- if .Values.protoURL }}
+# task: download proto file from URL
+- run: |
+    curl -o ghz.proto {{ $vals.protoURL }}
+{{- $pf := dict "proto" "ghz.proto" }}
+{{- $vals = mustMerge $pf .Values }}
+{{- end }}
+{{- if .Values.dataURL }}
+# task: download JSON data file from URL
+- run: |
+    curl -o data.json {{ $vals.dataURL }}
+{{- $pf := dict "data-file" "data.json" }}
+{{- $vals = mustMerge $pf $vals }}
+{{- end }}
+{{- if .Values.binaryDataURL }}
+# task: download binary data file from URL
+- run: |
+    curl -o data.bin {{ $vals.binaryDataURL }}
+{{- $pf := dict "binary-file" "data.bin" }}
+{{- $vals = mustMerge $pf $vals }}
+{{- end }}
+{{- if .Values.metadataURL }}
+# task: download metadata JSON file from URL
+- run: |
+    curl -o metadata.json {{ $vals.metadataURL }}
+{{- $pf := dict "metadata-file" "metadata.json" }}
+{{- $vals = mustMerge $pf $vals }}
+{{- end }}
+{{/* Write the main task */}}
+# task: generate gRPC requests for app
 # collect Iter8's built-in gRPC latency and error-related metrics
 - task: gen-load-and-collect-metrics-grpc
   with:
-{{ toYaml .Values | indent 4 }}
-{{- end }}    
+{{ toYaml $vals | indent 4 }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/iter8lib/templates/_task-http.tpl b/charts/iter8lib/templates/_task-http.tpl
index d3c6d2ea..317ecc19 100644
--- a/charts/iter8lib/templates/_task-http.tpl
+++ b/charts/iter8lib/templates/_task-http.tpl
@@ -1,55 +1,22 @@
 {{- define "task.http" -}}
-# task: generate HTTP requests for application URL
+{{/* Validate values */}}
+{{- if not .Values.url }}
+{{- fail "Please set a value for the url parameter." }}
+{{- end }}
+{{/* Perform the various setup steps before the main task */}}
+{{- $vals := mustDeepCopy .Values }}
+{{- if .Values.payloadURL }}
+# task: download payload from payload URL
+- run: |
+    curl -o payload.dat {{ $vals.payloadURL }}
+{{- $pf := dict "payloadFile" "payload.dat" }}
+{{- $vals = mustMerge $pf .Values }}
+{{- end }}
+{{/* Write the main task */}}
+# task: generate HTTP requests for app
 # collect Iter8's built-in HTTP latency and error-related metrics
 - task: gen-load-and-collect-metrics-http
   with:
-
-    {{- if .Values.numQueries }}
-    numRequests: {{ .Values.numQueries | int }}
-    {{- end }}
-
-    {{- if .Values.duration }}
-    duration: {{ .Values.duration | toString }}
-    {{- end }}
-
-    {{- if .Values.qps }}
-    qps: {{ .Values.qps | float64 }}
-    {{- end }}
-
-    {{- if .Values.connections }}
-    connections: {{ .Values.connections | int }}
-    {{- end }}
-
-    {{- if .Values.payloadStr }}
-    payloadStr: {{ .Values.payloadStr | quote }}
-    {{- end }}
-
-    {{- if .Values.contentType }}
-    contentType: {{ .Values.contentType | quote }}
-    {{- end }}
-
-    {{- if .Values.errorsAbove }}
-    errorRanges:
-    - lower: {{ .Values.errorsAbove | int }}
-    {{- end }}
-
-    {{- $percentiles := list }}
-    {{- range $key, $value := .Values.SLOs }}
-    {{- if (regexMatch "http/latency-p\\d+(?:\\.\\d)?$" $key) }}
-    {{- $percentiles = append $percentiles (trimPrefix "http/latency-p" $key | float64 ) }}
-    {{- end }}
-    {{- end }}
-    {{- if $percentiles }}
-    percentiles:
-{{ toYaml ($percentiles | uniq) | indent 4 }}
-    {{- end }}
-
-    {{- ""}}
-    versionInfo:
-    - url: {{ required "A valid url value is required!" .Values.url | toString }}
-    {{- if .Values.headers }}
-      headers:
-{{ toYaml .Values.headers | indent 8 }}
-    {{- end }}
-{{- end }}    
+{{ toYaml $vals | indent 4 }}
+{{- end }}
 
diff --git a/charts/iter8lib/values.yaml b/charts/iter8lib/values.yaml
new file mode 100644
index 00000000..e0035858
--- /dev/null
+++ b/charts/iter8lib/values.yaml
@@ -0,0 +1,2 @@
+global:
+  iter8Image: iter8/iter8:0.10
\ No newline at end of file
diff --git a/cmd/gen.go b/cmd/gen.go
index e4f53e27..a8ba61cc 100644
--- a/cmd/gen.go
+++ b/cmd/gen.go
@@ -12,7 +12,7 @@ Generate an experiment.yaml file by combining an experiment chart with values.
 
     $ iter8 gen --sourceDir /path/to/load-test-http --set url=https://httpbin.org/get
 
-This command is intended for development and testing of experiment charts. For production usage, the launch subcommand is recommended.
+This command is intended for development and testing of experiment charts. For production usage, the launch command is recommended.
 `
 
 // newGenCmd creates the gen command
@@ -36,12 +36,12 @@ func newGenCmd() *cobra.Command {
 
 // addChartsParentDirFlag to the command
 func addChartsParentDirFlag(cmd *cobra.Command, chartsParentDirPtr *string) {
-	cmd.Flags().StringVar(chartsParentDirPtr, "chartsParentDir", ".", "path to experiment chart directory")
+	cmd.Flags().StringVar(chartsParentDirPtr, "chartsParentDir", ".", "directory under which the charts folder is located")
 }
 
 // addChartNameFlag to the command
 func addChartNameFlag(cmd *cobra.Command, chartNamePtr *string) {
-	cmd.Flags().StringVarP(chartNamePtr, "chartName", "c", "", "path to experiment chart directory")
+	cmd.Flags().StringVarP(chartNamePtr, "chartName", "c", "", "name of the experiment chart")
 	cmd.MarkFlagRequired("chartName")
 }
 
diff --git a/driver/kubedriver.go b/driver/kubedriver.go
index 8dfcce81..ac5f77e2 100644
--- a/driver/kubedriver.go
+++ b/driver/kubedriver.go
@@ -32,7 +32,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/yaml"
 
-	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -186,26 +185,6 @@ func (driver *KubeDriver) getSecretWithRetry(name string) (s *corev1.Secret, err
 	return nil, e
 }
 
-// getJobWithRetry attempts to get a Kubernetes job with retry
-func (driver *KubeDriver) getJobWithRetry(name string) (*batchv1.Job, error) {
-
-	jobsClient := driver.Clientset.BatchV1().Jobs(driver.Namespace())
-
-	for i := 0; i < maxGetRetries; i++ {
-		j, err := jobsClient.Get(context.Background(), name, metav1.GetOptions{})
-		if err == nil {
-			return j, err
-		}
-		if !k8serrors.IsNotFound(err) {
-			log.Logger.Warningf("unable to get job: %s; %s\n", name, err.Error())
-		}
-		time.Sleep(getRetryInterval)
-	}
-	e := fmt.Errorf("unable to get job %v", name)
-	log.Logger.Error(e)
-	return nil, e
-}
-
 // getExperimentSpecSecret gets the Kubernetes experiment spec secret
 func (driver *KubeDriver) getExperimentSpecSecret() (s *corev1.Secret, err error) {
 	return driver.getSecretWithRetry(driver.getSpecSecretName())
@@ -216,11 +195,6 @@ func (driver *KubeDriver) getExperimentResultSecret() (s *corev1.Secret, err err
 	return driver.getSecretWithRetry(driver.getResultSecretName())
 }
 
-// getExperimentJob gets the Kubernetes experiment job
-func (driver *KubeDriver) getExperimentJob() (j *batchv1.Job, err error) {
-	return driver.getJobWithRetry(driver.getExperimentJobName())
-}
-
 // ReadSpec creates an ExperimentSpec struct for a Kubernetes experiment
 func (driver *KubeDriver) ReadSpec() (base.ExperimentSpec, error) {
 	s, err := driver.getExperimentSpecSecret()
@@ -267,21 +241,22 @@ type PayloadValue struct {
 
 // formResultSecret creates the result secret using the result
 func (driver *KubeDriver) formResultSecret(r *base.ExperimentResult) (*corev1.Secret, error) {
-	job, err := driver.getExperimentJob()
+	spec, err := driver.getExperimentSpecSecret()
 	if err != nil {
 		return nil, err
 	}
-	// got job ...
+	// got spec secret ...
+	log.Logger.Trace("spec secret typemeta", spec.TypeMeta)
 
 	byteArray, _ := yaml.Marshal(r)
 	sec := corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: driver.getResultSecretName(),
 			OwnerReferences: []metav1.OwnerReference{{
-				APIVersion: job.APIVersion,
-				Kind:       job.Kind,
-				Name:       job.Name,
-				UID:        job.UID,
+				APIVersion: "v1",
+				Kind:       "secret",
+				Name:       spec.Name,
+				UID:        spec.UID,
 			}},
 		},
 		StringData: map[string]string{"result.yaml": string(byteArray)},
@@ -292,12 +267,16 @@ func (driver *KubeDriver) formResultSecret(r *base.ExperimentResult) (*corev1.Se
 
 // createExperimentResultSecret creates the experiment result secret
 func (driver *KubeDriver) createExperimentResultSecret(r *base.ExperimentResult) error {
+	log.Logger.Trace("forming result secret...")
 	if sec, err := driver.formResultSecret(r); err == nil {
+		log.Logger.Trace("result secret formed")
 		secretsClient := driver.Clientset.CoreV1().Secrets(driver.Namespace())
-		_, e := secretsClient.Create(context.Background(), sec, metav1.CreateOptions{})
-		if e != nil {
+		log.Logger.Trace("creating result secret using client and sec", secretsClient, sec)
+		_, err2 := secretsClient.Create(context.Background(), sec, metav1.CreateOptions{})
+		log.Logger.Trace("create result secret returned")
+		if err2 != nil {
 			e := errors.New("unable to create result secret")
-			log.Logger.WithStackTrace(err.Error()).Error(e)
+			log.Logger.WithStackTrace(err2.Error()).Error(e)
 			return e
 		} else {
 			return nil


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 675b9ab4..5522feb0 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -26,6 +26,87 @@ jobs:
           exit 1
         fi
 
+  load-test-http:
+    name: http load test with payloadURL
+    runs-on: ubuntu-latest
+    steps:
+    - name: Install Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: 1.17
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+    - name: Build and install Iter8
+      run: make install
+    - name: run httpbin
+      run: |
+        set -e
+        docker pull kennethreitz/httpbin
+        docker run -p 80:80 kennethreitz/httpbin &
+        HOST_IP=$(ip -f inet addr show docker0 | grep -Po 'inet \K[\d.]+')
+        echo "HOST_IP=$HOST_IP" >> $GITHUB_ENV
+        while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://$HOST_IP/get)" != "200" ]]; do
+          sleep 5; 
+        done;
+    - name: launch load-test-http
+      run: |
+        iter8 launch -c load-test-http --noDownload \
+        --set url="http://$HOST_IP/post" \
+        --set payloadURL="https://httpbin.org/stream/1"
+    - name: assert experiment completed without failures
+      run: |
+        iter8 assert -c completed -c nofailure
+
+  load-test-grpc:
+    name: gRPC load test with various URLs
+    runs-on: ubuntu-latest
+    steps:
+    - name: Install Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: 1.17
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+    - name: Build and install Iter8
+      run: make install
+    - name: run greeter
+      run: |
+        git clone -b v1.43.0 https://github.com/grpc/grpc-go
+        cd grpc-go/examples/helloworld
+        go build greeter_server/main.go
+        ./main &
+
+    - name: load test grpc service with protoURL
+      run: |
+        set -e
+        export LOG_LEVEL=trace
+        iter8 launch -c load-test-grpc --noDownload \
+          --set host="127.0.0.1:50051" \
+          --set call="helloworld.Greeter.SayHello" \
+          --set protoURL="https://raw.githubusercontent.com/grpc/grpc-go/master/examples/helloworld/helloworld/helloworld.proto" \
+          --set data.name="frodo" \
+          --set SLOs.grpc/error-rate=0 \
+          --set SLOs.grpc/latency/mean=50 \
+          --set SLOs.grpc/latency/p90=100 \
+          --set SLOs.grpc/latency/p'97\.5'=200
+        iter8 assert -c completed -c nofailure -c slos
+        
+    - name: load test grpc service with proto/data/metadata URLs
+      run: |
+        set -e
+        export LOG_LEVEL=trace
+        iter8 launch -c load-test-grpc --noDownload \
+          --set host="127.0.0.1:50051" \
+          --set call="helloworld.Greeter.SayHello" \
+          --set protoURL="https://raw.githubusercontent.com/grpc/grpc-go/master/examples/helloworld/helloworld/helloworld.proto" \
+          --set dataURL="https://gist.githubusercontent.com/sriumcp/3f3178f4b698af6696c925832e51b0ba/raw/d02aa698d34aa2067f7a2f6afb4ceb616b0db822/name.json" \
+          --set metadataURL="https://gist.githubusercontent.com/sriumcp/3f3178f4b698af6696c925832e51b0ba/raw/d02aa698d34aa2067f7a2f6afb4ceb616b0db822/name.json" \
+          --set SLOs.grpc/error-rate=0 \
+          --set SLOs.grpc/latency/mean=50 \
+          --set SLOs.grpc/latency/p90=100 \
+          --set SLOs.grpc/latency/p'97\.5'=200
+        iter8 assert -c completed -c nofailure -c slos
+
   static-check:
     runs-on: ubuntu-latest
     steps:
diff --git a/action/gen_test.go b/action/gen_test.go
index 9cbf497b..2dddc7a8 100644
--- a/action/gen_test.go
+++ b/action/gen_test.go
@@ -44,5 +44,4 @@ func TestGenGRPC(t *testing.T) {
 	m = m["with"].(map[string]interface{})
 	s := m["proto"].(string)
 	assert.Equal(t, "helloworld.proto", s)
-
 }
diff --git a/base/collect_http_test.go b/base/collect_http_test.go
index 33d6ca20..08828144 100644
--- a/base/collect_http_test.go
+++ b/base/collect_http_test.go
@@ -25,10 +25,8 @@ func TestRunCollectHTTP(t *testing.T) {
 		With: collectHTTPInputs{
 			Duration:    StringPointer("1s"),
 			PayloadFile: StringPointer(CompletePath("../", "testdata/payload/ukpolice.json")),
-			VersionInfo: []*versionHTTP{{
-				Headers: map[string]string{},
-				URL:     "https://something.com",
-			}},
+			Headers:     map[string]string{},
+			URL:         "https://something.com",
 		},
 	}
 
diff --git a/base/experiment_test.go b/base/experiment_test.go
index a34ee92e..f72fc061 100644
--- a/base/experiment_test.go
+++ b/base/experiment_test.go
@@ -33,8 +33,9 @@ func TestRunTask(t *testing.T) {
 			Task: StringPointer(CollectHTTPTaskName),
 		},
 		With: collectHTTPInputs{
-			Duration:    StringPointer("1s"),
-			VersionInfo: []*versionHTTP{{Headers: map[string]string{}, URL: "https://httpbin.org/get"}},
+			Duration: StringPointer("1s"),
+			Headers:  map[string]string{},
+			URL:      "https://httpbin.org/get",
 		},
 	}
 
diff --git a/base/mock_qs_test.go b/base/mock_qs_test.go
index 3fa4f1b6..96afb73d 100644
--- a/base/mock_qs_test.go
+++ b/base/mock_qs_test.go
@@ -20,10 +20,8 @@ func TestMockQuickStartWithSLOs(t *testing.T) {
 		},
 		With: collectHTTPInputs{
 			Duration: StringPointer("2s"),
-			VersionInfo: []*versionHTTP{{
-				Headers: map[string]string{},
-				URL:     testURL,
-			}},
+			Headers:  map[string]string{},
+			URL:      testURL,
 		},
 	}
 
@@ -68,10 +66,8 @@ func TestMockQuickStartWithSLOsAndPercentiles(t *testing.T) {
 		},
 		With: collectHTTPInputs{
 			Duration: StringPointer("1s"),
-			VersionInfo: []*versionHTTP{{
-				Headers: map[string]string{},
-				URL:     testURL,
-			}},
+			Headers:  map[string]string{},
+			URL:      testURL,
 		},
 	}
 
diff --git a/charts/load-test-grpc/Chart.yaml b/charts/load-test-grpc/Chart.yaml
index 7c950ce5..b13545ee 100644
--- a/charts/load-test-grpc/Chart.yaml
+++ b/charts/load-test-grpc/Chart.yaml
@@ -1,6 +1,6 @@
 apiVersion: v2
 name: load-test-grpc
-version: 0.9.2
+version: 0.10.0
 description: Load test, benchmark, and validate gRPC services
 type: application
 keywords:
@@ -22,6 +22,5 @@ maintainers:
 icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png
 dependencies:
 - name: iter8lib
-  version: 0.9.x
+  version: 0.10.x
   repository: file://../iter8lib
-appVersion: v0.9
diff --git a/charts/load-test-http/Chart.yaml b/charts/load-test-http/Chart.yaml
index 32edd377..62597b15 100644
--- a/charts/load-test-http/Chart.yaml
+++ b/charts/load-test-http/Chart.yaml
@@ -1,6 +1,6 @@
 apiVersion: v2
 name: load-test-http
-version: 0.9.1
+version: 0.10.0
 description: Load test, benchmark, and validate HTTP services
 type: application
 keywords:
@@ -22,6 +22,5 @@ maintainers:
 icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png
 dependencies:
 - name: iter8lib
-  version: 0.9.x
+  version: 0.10.x
   repository: file://../iter8lib
-appVersion: v0.9
diff --git a/charts/load-test-http/values.yaml b/charts/load-test-http/values.yaml
index d4b15ec2..ff5ae45a 100644
--- a/charts/load-test-http/values.yaml
+++ b/charts/load-test-http/values.yaml
@@ -7,47 +7,47 @@
 
 ### url   HTTP(S) URL where the app receives GET or POST requests.
 ### This parameter is required.
-url: null
+# url: null
 
 ### headers   HTTP headers to be used in requests sent to the app.
 ### Specified as map[string]string
-headers: null
+# headers: null
 
 ### numQueries    Number of requests sent to the app. 
-numQueries: null
+# numQueries: null
 
 ### duration    Duration for which requests are sent to the app.
 ### Value can be any Go duration string (https://pkg.go.dev/maze.io/x/duration#ParseDuration)
 ### This field is ignored if `numQueries` is specified.
-duration: null
+# duration: null
 
 ### qps   Number of requests per second sent to each version.
-qps: null
+# qps: null
 
 ### connections   Number of parallel connections used to send requests.
-connections: null
+# connections: null
 
 ### payloadStr    String data to be sent as payload. 
 ### If this field is specified, Iter8 will send HTTP POST requests.
 ### with this string as the payload.
 ### This field is ignored if `payloadURL` is specified.
-payloadStr: null
+# payloadStr: null
 
 ### payloadUrl    URL of payload. If this field is specified, 
 ### Iter8 will send HTTP POST requests to versions with 
 ### data downloaded from this URL as the payload.
-payloadURL: null
+# payloadURL: null
 
 ### contentType   The type of the payload. Indicated using the Content-Type HTTP header value. 
 ### This is intended to be used in conjunction with one of the `payload*` fields above. 
 ### If this field is specified, Iter8 will send HTTP POST requests to versions 
 ### with this content type header value. If payload is supplied, and this field is 
 ### defaulted to "application/octet-stream".
-contentType: null
+# contentType: null
 
 ### errorsAbove   Any HTTP response code above this value is considered an error.
 ### If left unspecified, this parameter is defaulted to 400.
-errorsAbove: null
+# errorsAbove: null
 
 ### SLOs    A map of service level objectives (SLOs) that the app needs to satisfy.
 ### Metrics collected during the load test are used to verify if the app satisfies SLOs.
@@ -61,4 +61,4 @@ errorsAbove: null
 ###   http/error-rate: 0
 ###   http/latency-mean: 50
 ###   http/latency-p99: 200
-SLOs: null
+# SLOs: null
diff --git a/testdata/assertinputs/result.yaml b/testdata/assertinputs/result.yaml
index 383ccd69..36064c5f 100644
--- a/testdata/assertinputs/result.yaml
+++ b/testdata/assertinputs/result.yaml
@@ -125,6 +125,6 @@ insights:
     http/request-count:
     - 16
   numVersions: 1
-iter8Version: v0.9
+iter8Version: v0.10
 numCompletedTasks: 4
 startTime: "2022-03-16T10:22:58.540897-04:00"
diff --git a/testdata/assertinputsfail/result.yaml b/testdata/assertinputsfail/result.yaml
index 86bae128..ef15c39d 100644
--- a/testdata/assertinputsfail/result.yaml
+++ b/testdata/assertinputsfail/result.yaml
@@ -131,6 +131,6 @@ insights:
     http/request-count:
     - 16
   numVersions: 1
-iter8Version: v0.9
+iter8Version: v0.10
 numCompletedTasks: 4
 startTime: "2022-03-16T10:22:37.110424-04:00"
diff --git a/testdata/drivertests/experiment.yaml b/testdata/drivertests/experiment.yaml
index d741f034..fe26c44e 100644
--- a/testdata/drivertests/experiment.yaml
+++ b/testdata/drivertests/experiment.yaml
@@ -13,8 +13,7 @@
     - 97.5
     - 99
     - 99.9
-    versionInfo:
-    - url: https://httpbin.org/get
+    url: https://httpbin.org/get
 # task 2: validate service level objectives for app using
 # the metrics collected in the above task
 - task: assess-app-versions
diff --git a/testdata/experiment.yaml b/testdata/experiment.yaml
index d741f034..fe26c44e 100644
--- a/testdata/experiment.yaml
+++ b/testdata/experiment.yaml
@@ -13,8 +13,7 @@
     - 97.5
     - 99
     - 99.9
-    versionInfo:
-    - url: https://httpbin.org/get
+    url: https://httpbin.org/get
 # task 2: validate service level objectives for app using
 # the metrics collected in the above task
 - task: assess-app-versions