Propagate environment variables from user command to parent process (#31)

And some other enhancements:

- Replace linter `golint` to `revive` as the former is deprecated, and fix code styles found by `revive`.
- Polish the logs to make it not too lengthy.
- Add log level configuration.
- Bump up Go version to 1.16.
- Propagate environment variables from user command (sub-process) to parent process to make it available to other sub-processes.
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index d411fa9..ce03dd8 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -29,10 +29,10 @@
     name: Build
     runs-on: ubuntu-latest
     steps:
-      - name: Set up Go 1.14
+      - name: Set up Go 1.16
         uses: actions/setup-go@v2
         with:
-          go-version: 1.14
+          go-version: 1.16
         id: go
 
       - name: Check out code into the Go module directory
diff --git a/.golangci.yml b/.golangci.yml
index 617deaf..f3d33b5 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -24,7 +24,7 @@
 linters-settings:
   govet:
     check-shadowing: true
-  golint:
+  revive:
     min-confidence: 0
   gocyclo:
     min-complexity: 15
@@ -108,12 +108,11 @@
     - gocyclo
     - gofmt
     - goimports
-    - golint
+    - revive
     - gosec
     - gosimple
     - govet
     - ineffassign
-    - interfacer
     - lll
     - misspell
     - nakedret
diff --git a/Makefile b/Makefile
index 1eee4be..e0f675b 100644
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,7 @@
 
 .PHONY: lint
 lint:
-	$(GO_LINT) version || curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GO_PATH)/bin
+	$(GO_LINT) version || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GO_PATH)/bin
 	$(GO_LINT) run -v --timeout 5m ./...
 
 .PHONY: fix-lint
diff --git a/commands/root.go b/commands/root.go
index 8b5fe6c..ff625db 100644
--- a/commands/root.go
+++ b/commands/root.go
@@ -18,9 +18,12 @@
 package commands
 
 import (
+	"os"
+
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
-	"github.com/apache/skywalking-infra-e2e/internal/util"
+	"github.com/apache/skywalking-infra-e2e/internal/logger"
 
 	"github.com/apache/skywalking-infra-e2e/commands/cleanup"
 	"github.com/apache/skywalking-infra-e2e/commands/run"
@@ -29,6 +32,11 @@
 	"github.com/apache/skywalking-infra-e2e/commands/verify"
 	"github.com/apache/skywalking-infra-e2e/internal/config"
 	"github.com/apache/skywalking-infra-e2e/internal/constant"
+	"github.com/apache/skywalking-infra-e2e/internal/util"
+)
+
+var (
+	verbosity string
 )
 
 // Root represents the base command when called without any subcommands
@@ -38,8 +46,24 @@
 	Version:       version,
 	SilenceErrors: true,
 	SilenceUsage:  true,
-	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		config.ReadGlobalConfigFile()
+
+		level, err := logrus.ParseLevel(verbosity)
+		if err != nil {
+			return err
+		}
+		logger.Log.SetLevel(level)
+
+		util.WorkDir = util.ExpandFilePath(util.WorkDir)
+		if _, err := os.Stat(util.WorkDir); os.IsNotExist(err) {
+			if err := os.MkdirAll(util.WorkDir, os.ModePerm); err != nil {
+				logger.Log.Warnf("failed to create working directory %v", util.WorkDir)
+				return err
+			}
+		}
+
+		return nil
 	},
 }
 
@@ -52,6 +76,8 @@
 	Root.AddCommand(verify.Verify)
 	Root.AddCommand(cleanup.Cleanup)
 
+	Root.PersistentFlags().StringVarP(&verbosity, "verbosity", "v", logrus.InfoLevel.String(), "log level (debug, info, warn, error, fatal, panic")
+	Root.PersistentFlags().StringVarP(&util.WorkDir, "work-dir", "w", "~/.skywalking-infra-e2e", "the working directory for skywalking-infra-e2e")
 	Root.PersistentFlags().StringVarP(&util.CfgFile, "config", "c", constant.E2EDefaultFile, "the config file")
 
 	return Root.Execute()
diff --git a/commands/verify/verify.go b/commands/verify/verify.go
index e979b41..776510b 100644
--- a/commands/verify/verify.go
+++ b/commands/verify/verify.go
@@ -59,7 +59,7 @@
 		return fmt.Errorf("failed to read the expected data file: %v", err)
 	}
 
-	var actualData, sourceName string
+	var actualData, sourceName, stderr string
 	if actualFile != "" {
 		sourceName = actualFile
 		actualData, err = util.ReadFileContent(actualFile)
@@ -68,19 +68,19 @@
 		}
 	} else if query != "" {
 		sourceName = query
-		actualData, err = util.ExecuteCommand(query)
+		actualData, stderr, err = util.ExecuteCommand(query)
 		if err != nil {
-			return fmt.Errorf("failed to execute the query: %s, output: %s, error: %v", query, actualData, err)
+			return fmt.Errorf("failed to execute the query: %s, output: %s, error: %v", query, actualData, stderr)
 		}
 	}
 
 	if err = verifier.Verify(actualData, expectedData); err != nil {
 		if me, ok := err.(*verifier.MismatchError); ok {
-			return fmt.Errorf("failed to verify the output: %s, error: %v", sourceName, me.Error())
+			return fmt.Errorf("failed to verify the output: %s, error:\n%v", sourceName, me.Error())
 		}
-		return fmt.Errorf("failed to verify the output: %s, error: %v", sourceName, err)
+		return fmt.Errorf("failed to verify the output: %s, error:\n%v", sourceName, err)
 	}
-	logger.Log.Infof("verified the output: %s\n", sourceName)
+	logger.Log.Infof("verified the output: %s", sourceName)
 	return nil
 }
 
diff --git a/go.mod b/go.mod
index ce51fdd..004f535 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/apache/skywalking-infra-e2e
 
-go 1.13
+go 1.16
 
 require (
 	github.com/docker/docker v20.10.7+incompatible
diff --git a/internal/components/cleanup/kind.go b/internal/components/cleanup/kind.go
index 1e19072..3e5f8f1 100644
--- a/internal/components/cleanup/kind.go
+++ b/internal/components/cleanup/kind.go
@@ -86,9 +86,5 @@
 	args := []string{"delete", "cluster", "--name", clusterName}
 
 	logger.Log.Debugf("cluster delete commands: %s %s", constant.KindCommand, strings.Join(args, " "))
-	if err := kind.Run(kindcmd.NewLogger(), kindcmd.StandardIOStreams(), args); err != nil {
-		return err
-	}
-
-	return nil
+	return kind.Run(kindcmd.NewLogger(), kindcmd.StandardIOStreams(), args)
 }
diff --git a/internal/components/setup/common.go b/internal/components/setup/common.go
index 1e93b9d..e220fa8 100644
--- a/internal/components/setup/common.go
+++ b/internal/components/setup/common.go
@@ -22,6 +22,7 @@
 	"fmt"
 	"io/ioutil"
 	"os"
+	"strings"
 	"time"
 
 	"k8s.io/client-go/dynamic"
@@ -170,13 +171,13 @@
 	defer waitSet.WaitGroup.Done()
 
 	// executes commands
-	logger.Log.Infof("executing commands [%s]", commands)
-	result, err := util.ExecuteCommand(commands)
+	logger.Log.Infof("executing commands [%s]", strings.ReplaceAll(commands, "\n", "\\n"))
+	result, stderr, err := util.ExecuteCommand(commands)
 	if err != nil {
-		err = fmt.Errorf("commands: [%s] runs error: %s", commands, err)
+		err = fmt.Errorf("commands: [%s] runs error: %s", strings.ReplaceAll(commands, "\n", "\\n"), stderr)
 		waitSet.ErrChan <- err
 	}
-	logger.Log.Infof("executed commands [%s], result: %s", commands, result)
+	logger.Log.Infof("executed commands [%s], result: %s", strings.ReplaceAll(commands, "\n", "\\n"), result)
 
 	// waits for conditions meet
 	for idx := range waits {
diff --git a/internal/components/trigger/http.go b/internal/components/trigger/http.go
index ce4cbe2..2af36a3 100644
--- a/internal/components/trigger/http.go
+++ b/internal/components/trigger/http.go
@@ -113,9 +113,9 @@
 		logger.Log.Errorf("do request error %v", err)
 		return err
 	}
-	response.Body.Close()
+	_ = response.Body.Close()
 
-	logger.Log.Infof("do request %v response http code %v", h.url, response.StatusCode)
+	logger.Log.Debugf("do request %v response http code %v", h.url, response.StatusCode)
 	if response.StatusCode == http.StatusOK {
 		logger.Log.Debugf("do http action %+v success.", *h)
 		return nil
diff --git a/internal/logger/log.go b/internal/logger/log.go
index 7fc102e..1c641e8 100644
--- a/internal/logger/log.go
+++ b/internal/logger/log.go
@@ -29,7 +29,7 @@
 	if Log == nil {
 		Log = logrus.New()
 	}
-	Log.Level = logrus.DebugLevel
+	Log.Level = logrus.InfoLevel
 	Log.SetOutput(os.Stdout)
 	Log.SetFormatter(&logrus.TextFormatter{
 		DisableTimestamp:       true,
diff --git a/internal/util/config.go b/internal/util/config.go
index 15e38e1..c34eecb 100644
--- a/internal/util/config.go
+++ b/internal/util/config.go
@@ -25,7 +25,10 @@
 	"github.com/apache/skywalking-infra-e2e/internal/logger"
 )
 
-var CfgFile string
+var (
+	CfgFile string
+	WorkDir string
+)
 
 // ResolveAbs resolves the relative path (relative to CfgFile) to an absolute file path.
 func ResolveAbs(p string) string {
diff --git a/internal/util/hook.sh b/internal/util/hook.sh
new file mode 100644
index 0000000..0a09eda
--- /dev/null
+++ b/internal/util/hook.sh
@@ -0,0 +1,21 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you 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.
+#
+function finish {
+  printenv > {{ .EnvFile }}
+}
+trap finish EXIT
diff --git a/internal/util/io.go b/internal/util/io.go
new file mode 100644
index 0000000..733f552
--- /dev/null
+++ b/internal/util/io.go
@@ -0,0 +1,45 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 util
+
+import (
+	"os/user"
+	"strings"
+
+	"github.com/apache/skywalking-infra-e2e/internal/logger"
+)
+
+// UserHomeDir returns the current user's home directory absolute path,
+// which is usually represented as `~` in most shells
+func UserHomeDir() string {
+	if currentUser, err := user.Current(); err != nil {
+		logger.Log.Warnln("Cannot obtain user home directory")
+	} else {
+		return currentUser.HomeDir
+	}
+	return ""
+}
+
+// ExpandFilePath expands the leading `~` to absolute path
+func ExpandFilePath(path string) string {
+	if strings.HasPrefix(path, "~") {
+		return strings.Replace(path, "~", UserHomeDir(), 1)
+	}
+	return path
+}
diff --git a/internal/util/utils.go b/internal/util/utils.go
index 1e84ea1..5dc7a55 100644
--- a/internal/util/utils.go
+++ b/internal/util/utils.go
@@ -20,10 +20,16 @@
 
 import (
 	"bytes"
+	_ "embed"
 	"errors"
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/apache/skywalking-infra-e2e/internal/logger"
 )
 
 // PathExist checks if a file/directory is exist.
@@ -48,16 +54,72 @@
 }
 
 // ExecuteCommand executes the given command and returns the result.
-func ExecuteCommand(cmd string) (string, error) {
+func ExecuteCommand(cmd string) (stdout, stderr string, err error) {
+	hookScript, err := hookScript()
+	if err != nil {
+		return "", "", err
+	}
+
+	// Propagate the env vars from sub-process back to parent process
+	defer exportEnvVars()
+
+	cmd = hookScript + "\n" + cmd
+
 	command := exec.Command("bash", "-ec", cmd)
-	outinfo := bytes.Buffer{}
-	command.Stdout = &outinfo
+	sout, serr := bytes.Buffer{}, bytes.Buffer{}
+	command.Stdout, command.Stderr = &sout, &serr
 
 	if err := command.Start(); err != nil {
-		return outinfo.String(), err
+		return sout.String(), serr.String(), err
 	}
 	if err := command.Wait(); err != nil {
-		return outinfo.String(), err
+		return sout.String(), serr.String(), err
 	}
-	return outinfo.String(), nil
+	return sout.String(), serr.String(), nil
+}
+
+//go:embed hook.sh
+var hookScriptTemplate string
+
+type HookScriptTemplate struct {
+	EnvFile string
+}
+
+func hookScript() (string, error) {
+	hookScript := bytes.Buffer{}
+
+	parse, err := template.New("hookScriptTemplate").Parse(hookScriptTemplate)
+	if err != nil {
+		return "", err
+	}
+
+	envFile := filepath.Join(WorkDir, ".env")
+	scriptData := HookScriptTemplate{EnvFile: envFile}
+	if err := parse.Execute(&hookScript, scriptData); err != nil {
+		return "", err
+	}
+	return hookScript.String(), nil
+}
+
+func exportEnvVars() {
+	envFile := filepath.Join(WorkDir, ".env")
+	b, err := ioutil.ReadFile(envFile)
+	if err != nil {
+		logger.Log.Warnf("failed to export environment variables, %v", err)
+		return
+	}
+	s := string(b)
+
+	lines := strings.Split(s, "\n")
+	for _, line := range lines {
+		kv := strings.SplitN(line, "=", 2)
+		if len(kv) != 2 {
+			continue
+		}
+		key, val := kv[0], kv[1]
+		// should only export env vars that are not already exist in parent process (Go process)
+		if err := os.Setenv(key, val); err != nil {
+			logger.Log.Warnf("failed to export environment variable %v=%v, %v", key, val, err)
+		}
+	}
 }