You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2021/11/01 06:32:44 UTC

[skywalking-infra-e2e] branch main updated: Support save pod/container std log on the Environment (#62)

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-infra-e2e.git


The following commit(s) were added to refs/heads/main by this push:
     new 5adf908  Support save pod/container std log on the Environment  (#62)
5adf908 is described below

commit 5adf908561e2c93e8ee0d82e6eb549cd4cb3b234
Author: mrproliu <74...@qq.com>
AuthorDate: Mon Nov 1 14:32:36 2021 +0800

    Support save pod/container std log on the Environment  (#62)
---
 action.yaml                                   |  46 +++++++-
 commands/root.go                              |  27 +++--
 commands/setup/setup.go                       |   4 +
 docs/en/setup/Configuration-File.md           |   8 ++
 docs/en/setup/Run-E2E-Tests.md                |  16 ++-
 internal/components/setup/common.go           |  48 ++++-----
 internal/components/setup/compose.go          | 145 ++++++++++++++++++--------
 internal/components/setup/compose_listener.go | 103 ++++++++++++++++++
 internal/components/setup/kind.go             | 112 ++++++++++++++++----
 internal/components/setup/kind_listener.go    | 135 ++++++++++++++++++++++++
 internal/util/config.go                       |   1 +
 internal/util/env_log.go                      | 112 ++++++++++++++++++++
 internal/util/k8s.go                          |  72 +++++++++++--
 internal/util/restClientGetter.go             |  83 ---------------
 14 files changed, 728 insertions(+), 184 deletions(-)

diff --git a/action.yaml b/action.yaml
index 681b8b8..20f91e9 100644
--- a/action.yaml
+++ b/action.yaml
@@ -21,6 +21,9 @@ inputs:
   e2e-file:
     description: File path of e2e file
     required: true
+  log-dir:
+    description: The container logs directory
+    required: false
 
 runs:
   using: "composite"
@@ -30,5 +33,46 @@ runs:
         make -C $GITHUB_ACTION_PATH docker
         docker run -v $(pwd):/tmp -w /tmp --entrypoint=sh docker.io/apache/e2e:latest -c "cp /usr/local/bin/e2e /tmp/e2e"
         install ./e2e /usr/local/bin
+    - name: E2E Dir Generator
+      id: 'e2e-dir-generator'
+      shell: bash
+      run: |
+        WORK_DIR="${{ runner.temp }}/skywalking-infra-e2e"
+        echo "::set-output name=work::$WORK_DIR"
+
+        LOG_DIR=""
+        LOG_JOB_DIR=""
+        if [[ "${{ inputs.log-dir }}" == "" ]]
+        then
+          matrix='${{ toJSON(matrix) }}'
+          if [[ "$matrix" == "null" ]]
+          then
+            LOG_DIR="$WORK_DIR/logs"
+            LOG_JOB_DIR="$LOG_DIR/${{ github.job }}"
+          else
+            combine_matrix=$(echo $matrix|jq -r 'to_entries|map(.value)|tostring')
+            # remove json syntax
+            combine_matrix=`echo $combine_matrix|sed -e 's/\[\|\]\|\"//g'`
+            combine_matrix=`echo $combine_matrix|sed -e 's/[\{|\}]//g'`
+            # replace to path
+            combine_matrix=`echo $combine_matrix|sed -e 's/[^A-Za-z0-9_-]/_/g'`
+            LOG_DIR="$WORK_DIR/logs"
+            LOG_JOB_DIR="$LOG_DIR/${{ github.job }}_$combine_matrix"
+          fi
+        elif [[ "${{ inputs.log-dir }}" == /* ]]
+        then
+          LOG_DIR="${{ inputs.log-dir }}"
+          LOG_JOB_DIR="${{ inputs.log-dir }}"
+        else
+          LOG_DIR="$WORK_DIR/${{ inputs.log-dir }}"
+          LOG_JOB_DIR="$WORK_DIR/${{ inputs.log-dir }}"
+        fi
+        echo "::set-output name=log::$LOG_DIR"
+        echo "::set-output name=log-case::$LOG_JOB_DIR"
+        echo "SW_INFRA_E2E_LOG_DIR=$LOG_DIR" >> $GITHUB_ENV
     - shell: bash
-      run: e2e run -c "${{ inputs.e2e-file }}"
+      run: |
+        e2e run \
+          -c "${{ inputs.e2e-file }}" \
+          -w "${{ steps.e2e-dir-generator.outputs.work }}" \
+          -l "${{ steps.e2e-dir-generator.outputs.log-case }}"
\ No newline at end of file
diff --git a/commands/root.go b/commands/root.go
index ff625db..29572f7 100644
--- a/commands/root.go
+++ b/commands/root.go
@@ -55,18 +55,32 @@ var Root = &cobra.Command{
 		}
 		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
-			}
+		util.WorkDir, err = ExpandPathAndCreate(util.WorkDir)
+		if err != nil {
+			logger.Log.Warnf("failed to create working directory %v", util.WorkDir)
+			return err
+		}
+
+		util.LogDir, err = ExpandPathAndCreate(util.LogDir)
+		if err != nil {
+			logger.Log.Warnf("failed to create logging directory %v", util.LogDir)
+			return err
 		}
 
 		return nil
 	},
 }
 
+func ExpandPathAndCreate(path string) (string, error) {
+	path = util.ExpandFilePath(path)
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		if err := os.MkdirAll(path, os.ModePerm); err != nil {
+			return path, err
+		}
+	}
+	return path, nil
+}
+
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() error {
@@ -78,6 +92,7 @@ func Execute() error {
 
 	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.LogDir, "log-dir", "l", "~/.skywalking-infra-e2e/logs", "the container logs directory for environment")
 	Root.PersistentFlags().StringVarP(&util.CfgFile, "config", "c", constant.E2EDefaultFile, "the config file")
 
 	return Root.Execute()
diff --git a/commands/setup/setup.go b/commands/setup/setup.go
index 4dc6adf..a932d4d 100644
--- a/commands/setup/setup.go
+++ b/commands/setup/setup.go
@@ -38,6 +38,7 @@ var Setup = &cobra.Command{
 			return err
 		}
 
+		defer setup.CloseLogFollower()
 		if err := DoSetupAccordingE2E(); err != nil {
 			return fmt.Errorf("[Setup] %s", err)
 		}
@@ -61,6 +62,7 @@ func DoSetupAccordingE2E() error {
 
 	e2eConfig := config.GlobalConfig.E2EConfig
 
+	setup.InitLogFollower()
 	if e2eConfig.Setup.Env == constant.Kind {
 		err := setup.KindSetup(&e2eConfig)
 		if err != nil {
@@ -80,6 +82,8 @@ func DoSetupAccordingE2E() error {
 }
 
 func DoStopSetup() {
+	// close log follower
+	setup.CloseLogFollower()
 	// notify clean up
 	setup.KindCleanNotify()
 }
diff --git a/docs/en/setup/Configuration-File.md b/docs/en/setup/Configuration-File.md
index d1c5a6d..969c682 100644
--- a/docs/en/setup/Configuration-File.md
+++ b/docs/en/setup/Configuration-File.md
@@ -85,6 +85,10 @@ If you want to access the resource from host, should follow these steps:
       url: http://${pod_foo_host}:${pod_foo_8080}/
    ```
 
+#### Log
+
+The console output of each pod could be found in `${workDir}/logs/${namespace}/${podName}.log`.
+
 ### Compose
 
 ```yaml
@@ -123,6 +127,10 @@ If you want to get the service host and port mapping, should follow these steps:
       url: http://${oap_host}:${oap_8080}/
    ```
 
+#### Log
+
+The console output of each service could be found in `${workDir}/logs/{serviceName}/std.log`.
+
 ## Trigger
 
 After the `Setup` step is finished, use the `Trigger` step to generate traffic.
diff --git a/docs/en/setup/Run-E2E-Tests.md b/docs/en/setup/Run-E2E-Tests.md
index 682dcd5..28353f8 100644
--- a/docs/en/setup/Run-E2E-Tests.md
+++ b/docs/en/setup/Run-E2E-Tests.md
@@ -31,9 +31,23 @@ e2e cleanup
 
 To use skywalking-infra-e2e in GitHub Actions, add a step in your GitHub workflow.
 
+The working directory could be uploaded to GitHub Action Artifact after the task is completed, which contains environment variables and container logs in the environment.
+
 ```yaml
 - name: Run E2E Test
   uses: apache/skywalking-infra-e2e@main      # always prefer to use a revision instead of `main`.
   with:
-    e2e-file: e2e.yaml                        # need to run E2E file path
+    e2e-file: e2e.yaml                        # (required)need to run E2E file path
+    log-dir: /path/to/log/dir                 # (Optional)Use `<work_dir>/logs/<job_name>_<matrix_value>`(if have GHA matrix) or `<work_dir>/logs/<job_name>` in GHA, and output logs into `<work_dir>/logs` out of GHA env, such as running locally.
+```
+
+If you want to upload the log directory to the GitHub Action Artifact when this E2E test failure, you could define the below content in your GitHub Action Job.
+
+```yaml
+- name: Upload E2E Log
+  uses: actions/upload-artifact@v2
+  if: ${{ failure() }}                      # Only upload the artifact when E2E testing failure
+  with:
+    name: e2e-log
+    path: "${{ env.SW_INFRA_E2E_LOG_DIR }}" # The SkyWalking Infra E2E action sets SW_INFRA_E2E_LOG_DIR automatically. 
 ```
\ No newline at end of file
diff --git a/internal/components/setup/common.go b/internal/components/setup/common.go
index 86fcef6..87039d4 100644
--- a/internal/components/setup/common.go
+++ b/internal/components/setup/common.go
@@ -19,20 +19,21 @@
 package setup
 
 import (
+	"context"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"strings"
 	"time"
 
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/kubernetes"
-
 	"github.com/apache/skywalking-infra-e2e/internal/config"
 	"github.com/apache/skywalking-infra-e2e/internal/logger"
 	"github.com/apache/skywalking-infra-e2e/internal/util"
 )
 
+var (
+	logFollower *util.ResourceLogFollower
+)
+
 func RunStepsAndWait(steps []config.Step, waitTimeout time.Duration, k8sCluster *util.K8sClusterInfo) error {
 	logger.Log.Debugf("wait timeout is %v", waitTimeout.String())
 
@@ -50,7 +51,7 @@ func RunStepsAndWait(steps []config.Step, waitTimeout time.Duration, k8sCluster
 				Path:  step.Path,
 				Waits: step.Waits,
 			}
-			err := createManifestAndWait(k8sCluster.Client, k8sCluster.Interface, manifest, waitTimeout)
+			err := createManifestAndWait(k8sCluster, manifest, waitTimeout)
 			if err != nil {
 				return err
 			}
@@ -60,7 +61,7 @@ func RunStepsAndWait(steps []config.Step, waitTimeout time.Duration, k8sCluster
 				Waits:   step.Waits,
 			}
 
-			err := RunCommandsAndWait(command, waitTimeout)
+			err := RunCommandsAndWait(command, waitTimeout, k8sCluster)
 			if err != nil {
 				return err
 			}
@@ -79,16 +80,11 @@ func RunStepsAndWait(steps []config.Step, waitTimeout time.Duration, k8sCluster
 }
 
 // createManifestAndWait creates manifests in k8s cluster and concurrent waits according to the manifests' wait conditions.
-func createManifestAndWait(c *kubernetes.Clientset, dc dynamic.Interface, manifest config.Manifest, timeout time.Duration) error {
+func createManifestAndWait(c *util.K8sClusterInfo, manifest config.Manifest, timeout time.Duration) error {
 	waitSet := util.NewWaitSet(timeout)
 
-	kubeConfigYaml, err := ioutil.ReadFile(kubeConfigPath)
-	if err != nil {
-		return err
-	}
-
 	waits := manifest.Waits
-	err = createByManifest(c, dc, manifest)
+	err := createByManifest(c, manifest)
 	if err != nil {
 		return err
 	}
@@ -103,7 +99,7 @@ func createManifestAndWait(c *kubernetes.Clientset, dc dynamic.Interface, manife
 		wait := waits[idx]
 		logger.Log.Infof("waiting for %+v", wait)
 
-		options, err := getWaitOptions(kubeConfigYaml, &wait)
+		options, err := getWaitOptions(c, &wait)
 		if err != nil {
 			return err
 		}
@@ -131,7 +127,7 @@ func createManifestAndWait(c *kubernetes.Clientset, dc dynamic.Interface, manife
 }
 
 // RunCommandsAndWait Concurrently run commands and wait for conditions.
-func RunCommandsAndWait(run config.Run, timeout time.Duration) error {
+func RunCommandsAndWait(run config.Run, timeout time.Duration, cluster *util.K8sClusterInfo) error {
 	waitSet := util.NewWaitSet(timeout)
 
 	commands := run.Command
@@ -140,7 +136,7 @@ func RunCommandsAndWait(run config.Run, timeout time.Duration) error {
 	}
 
 	waitSet.WaitGroup.Add(1)
-	go executeCommandsAndWait(commands, run.Waits, waitSet)
+	go executeCommandsAndWait(commands, run.Waits, waitSet, cluster)
 
 	go func() {
 		waitSet.WaitGroup.Wait()
@@ -160,7 +156,7 @@ func RunCommandsAndWait(run config.Run, timeout time.Duration) error {
 	return nil
 }
 
-func executeCommandsAndWait(commands string, waits []config.Wait, waitSet *util.WaitSet) {
+func executeCommandsAndWait(commands string, waits []config.Wait, waitSet *util.WaitSet, cluster *util.K8sClusterInfo) {
 	defer waitSet.WaitGroup.Done()
 
 	// executes commands
@@ -177,13 +173,7 @@ func executeCommandsAndWait(commands string, waits []config.Wait, waitSet *util.
 		wait := waits[idx]
 		logger.Log.Infof("waiting for %+v", wait)
 
-		kubeConfigYaml, err := ioutil.ReadFile(kubeConfigPath)
-		if err != nil {
-			err = fmt.Errorf("read kube config failed: %s", err)
-			waitSet.ErrChan <- err
-		}
-
-		options, err := getWaitOptions(kubeConfigYaml, &wait)
+		options, err := getWaitOptions(cluster, &wait)
 		if err != nil {
 			err = fmt.Errorf("commands: [%s] get wait options error: %s", commands, err)
 			waitSet.ErrChan <- err
@@ -213,3 +203,13 @@ func GetIdentity() string {
 	}
 	return runID
 }
+
+func InitLogFollower() {
+	logFollower = util.NewResourceLogFollower(context.Background(), util.LogDir)
+}
+
+func CloseLogFollower() {
+	if logFollower != nil {
+		logFollower.Close()
+	}
+}
diff --git a/internal/components/setup/compose.go b/internal/components/setup/compose.go
index 0543187..12b2d14 100644
--- a/internal/components/setup/compose.go
+++ b/internal/components/setup/compose.go
@@ -36,6 +36,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/client"
+	"github.com/docker/docker/pkg/stdcopy"
 
 	"github.com/testcontainers/testcontainers-go"
 )
@@ -61,7 +62,7 @@ func ComposeSetup(e2eConfig *config.E2EConfig) error {
 	compose := testcontainers.NewLocalDockerCompose(composeFilePaths, identifier)
 
 	// bind wait port
-	serviceWithPorts, err := bindWaitPort(e2eConfig, compose)
+	services, err := buildComposeServices(e2eConfig, compose)
 	if err != nil {
 		return fmt.Errorf("bind wait ports error: %v", err)
 	}
@@ -75,6 +76,18 @@ func ComposeSetup(e2eConfig *config.E2EConfig) error {
 	}
 	cmd = append(cmd, "up", "-d")
 
+	// Listen container create
+	listener := NewComposeContainerListener(context.Background(), cli, services)
+	defer listener.Stop()
+	err = listener.Listen(func(container *ComposeContainer) {
+		if err = exposeComposeLog(cli, container.Service, container.ID, logFollower); err == nil {
+			container.Service.beenFollowLog = true
+		}
+	})
+	if err != nil {
+		return err
+	}
+
 	// setup
 	execError := compose.WithCommand(cmd).Invoke()
 	if execError.Error != nil {
@@ -82,7 +95,7 @@ func ComposeSetup(e2eConfig *config.E2EConfig) error {
 	}
 
 	// find exported port and build env
-	err = exposeServiceEnv(serviceWithPorts, cli, identifier, e2eConfig)
+	err = exposeComposeService(services, cli, identifier, e2eConfig)
 	if err != nil {
 		return err
 	}
@@ -97,53 +110,103 @@ func ComposeSetup(e2eConfig *config.E2EConfig) error {
 	return nil
 }
 
-func exposeServiceEnv(serviceWithPorts map[string][]*hostPortCachedStrategy, cli *client.Client, identity string, e2eConfig *config.E2EConfig) error {
+type ComposeService struct {
+	Name           string
+	waitStrategies []*hostPortCachedStrategy
+	beenFollowLog  bool
+}
+
+func exposeComposeService(services []*ComposeService, cli *client.Client,
+	identity string, e2eConfig *config.E2EConfig) error {
 	dockerProvider := &DockerProvider{client: cli}
+
 	// find exported port and build env
-	for service, portList := range serviceWithPorts {
-		container, err := findContainer(cli, fmt.Sprintf("%s_%s", identity, getInstanceName(service)))
+	for _, service := range services {
+		container, err := findContainer(cli, fmt.Sprintf("%s_%s", identity, getInstanceName(service.Name)))
 		if err != nil {
 			return err
 		}
-		if len(portList) == 0 {
-			continue
-		}
-
-		containerPorts := container.Ports
 
-		// get real ip address for access and export to env
-		host, err := dockerProvider.daemonHost(context.Background())
-		if err != nil {
+		// expose port
+		if err := exposeComposePort(dockerProvider, service, container, e2eConfig); err != nil {
 			return err
 		}
 
-		// format: <service_name>_host
-		if err := exportComposeEnv(fmt.Sprintf("%s_host", service), host, service); err != nil {
-			return err
+		// if service log not follow, expose log
+		if !service.beenFollowLog {
+			if err := exposeComposeLog(dockerProvider.client, service, container.ID, logFollower); err != nil {
+				return err
+			}
+			service.beenFollowLog = true
 		}
+	}
+	return nil
+}
 
-		for inx := range portList {
-			for _, containerPort := range containerPorts {
-				if int(containerPort.PrivatePort) != portList[inx].expectPort {
-					continue
-				}
-
-				if err := waitPortUntilReady(e2eConfig, container, dockerProvider, portList[inx].expectPort); err != nil {
-					return err
-				}
-
-				// expose env config to env
-				// format: <service_name>_<port>
-				if err := exportComposeEnv(
-					fmt.Sprintf("%s_%d", service, containerPort.PrivatePort),
-					fmt.Sprintf("%d", containerPort.PublicPort),
-					service); err != nil {
-					return err
-				}
-				break
+func exposeComposePort(dockerProvider *DockerProvider, service *ComposeService, container *types.Container,
+	e2eConfig *config.E2EConfig) error {
+	if len(service.waitStrategies) == 0 {
+		return nil
+	}
+
+	// get real ip address for access and export to env
+	host, err := dockerProvider.daemonHost(context.Background())
+	if err != nil {
+		return err
+	}
+
+	// format: <service_name>_host
+	if err := exportComposeEnv(fmt.Sprintf("%s_host", service.Name), host, service.Name); err != nil {
+		return err
+	}
+
+	for inx := range service.waitStrategies {
+		for _, containerPort := range container.Ports {
+			if int(containerPort.PrivatePort) != service.waitStrategies[inx].expectPort {
+				continue
 			}
+
+			if err := waitPortUntilReady(e2eConfig, container, dockerProvider, service.waitStrategies[inx].expectPort); err != nil {
+				return err
+			}
+
+			// expose env config to env
+			// format: <service_name>_<port>
+			if err := exportComposeEnv(
+				fmt.Sprintf("%s_%d", service.Name, containerPort.PrivatePort),
+				fmt.Sprintf("%d", containerPort.PublicPort),
+				service.Name); err != nil {
+				return err
+			}
+			break
 		}
 	}
+
+	return nil
+}
+
+// export container log to local path
+func exposeComposeLog(cli *client.Client, service *ComposeService, containerID string, logFollower *util.ResourceLogFollower) error {
+	logs, err := cli.ContainerLogs(logFollower.Ctx, containerID, types.ContainerLogsOptions{
+		ShowStdout: true,
+		ShowStderr: true,
+		Follow:     true,
+		Details:    false,
+	})
+	if err != nil {
+		return err
+	}
+	writer, err := logFollower.BuildLogWriter(fmt.Sprintf("%s/std.log", service.Name))
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		defer writer.Close()
+		if _, err := stdcopy.StdCopy(writer, writer, logs); err != nil {
+			logger.Log.Warnf("write %s std log error: %v", service.Name, err)
+		}
+	}()
 	return nil
 }
 
@@ -156,16 +219,17 @@ func exportComposeEnv(key, value, service string) error {
 	return nil
 }
 
-func bindWaitPort(e2eConfig *config.E2EConfig, compose *testcontainers.LocalDockerCompose) (map[string][]*hostPortCachedStrategy, error) {
+func buildComposeServices(e2eConfig *config.E2EConfig, compose *testcontainers.LocalDockerCompose) ([]*ComposeService, error) {
 	waitTimeout := e2eConfig.Setup.GetTimeout()
-	serviceWithPorts := make(map[string][]*hostPortCachedStrategy)
+	services := make([]*ComposeService, 0)
 	for service, content := range compose.Services {
 		serviceConfig := content.(map[interface{}]interface{})
 		ports := serviceConfig["ports"]
+		serviceContext := &ComposeService{Name: service}
+		services = append(services, serviceContext)
 		if ports == nil {
 			continue
 		}
-		serviceWithPorts[service] = []*hostPortCachedStrategy{}
 
 		portList := ports.([]interface{})
 		for inx := range portList {
@@ -180,11 +244,10 @@ func bindWaitPort(e2eConfig *config.E2EConfig, compose *testcontainers.LocalDock
 			}
 			// temporary don't use testcontainers-go framework wait strategy until fix docker-in-docker bug
 			// compose.WithExposedService(service, exportPort, strategy)
-
-			serviceWithPorts[service] = append(serviceWithPorts[service], strategy)
+			serviceContext.waitStrategies = append(serviceContext.waitStrategies, strategy)
 		}
 	}
-	return serviceWithPorts, nil
+	return services, nil
 }
 
 func getExpectPort(portConfig interface{}) (int, error) {
diff --git a/internal/components/setup/compose_listener.go b/internal/components/setup/compose_listener.go
new file mode 100644
index 0000000..4f1fef2
--- /dev/null
+++ b/internal/components/setup/compose_listener.go
@@ -0,0 +1,103 @@
+// 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 setup
+
+import (
+	"context"
+
+	"github.com/docker/docker/api/types/events"
+
+	"github.com/apache/skywalking-infra-e2e/internal/logger"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/client"
+)
+
+type ComposeContainerListener struct {
+	client   *client.Client
+	services []*ComposeService
+	ctx      context.Context
+	cancel   context.CancelFunc
+}
+
+type ComposeContainer struct {
+	Service *ComposeService
+	ID      string
+}
+
+func NewComposeContainerListener(ctx context.Context, cli *client.Client, services []*ComposeService) *ComposeContainerListener {
+	childCtx, cancelFunc := context.WithCancel(ctx)
+	return &ComposeContainerListener{
+		client:   cli,
+		services: services,
+		ctx:      childCtx,
+		cancel:   cancelFunc,
+	}
+}
+
+func (c *ComposeContainerListener) Listen(consumer func(container *ComposeContainer)) error {
+	containerEvents, errors := c.client.Events(c.ctx, types.EventsOptions{
+		Filters: filters.NewArgs(
+			filters.Arg("type", "container"),
+			filters.Arg("event", "start"),
+		),
+	})
+
+	if len(errors) > 0 {
+		return <-errors
+	}
+
+	go func() {
+		for {
+			select {
+			case msg := <-containerEvents:
+				container := c.foundMessage(&msg)
+				if container != nil {
+					consumer(container)
+				}
+			case err := <-errors:
+				if err != nil {
+					logger.Log.Warnf("Listen docker container failed, %v", err)
+				}
+			case <-c.ctx.Done():
+				c.cancel()
+				return
+			}
+		}
+	}()
+	return nil
+}
+
+func (c *ComposeContainerListener) Stop() {
+	c.cancel()
+}
+
+func (c *ComposeContainerListener) foundMessage(message *events.Message) *ComposeContainer {
+	serviceName := message.Actor.Attributes["com.docker.compose.service"]
+	for _, service := range c.services {
+		if service.Name == serviceName {
+			return &ComposeContainer{
+				Service: service,
+				ID:      message.ID,
+			}
+		}
+	}
+	return nil
+}
diff --git a/internal/components/setup/kind.go b/internal/components/setup/kind.go
index d7e09ec..8f186be 100644
--- a/internal/components/setup/kind.go
+++ b/internal/components/setup/kind.go
@@ -21,12 +21,14 @@ package setup
 import (
 	"bufio"
 	"bytes"
+	"context"
 	"fmt"
-	"io/ioutil"
 	"net/http"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	apiv1 "k8s.io/api/admission/v1"
@@ -35,8 +37,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/cli-runtime/pkg/resource"
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/portforward"
 	"k8s.io/client-go/transport/spdy"
@@ -117,6 +117,17 @@ func KindSetup(e2eConfig *config.E2EConfig) error {
 		return err
 	}
 
+	listener := NewKindContainerListener(context.Background(), cluster)
+	defer listener.Stop()
+	err = listener.Listen(func(pod *v1.Pod) {
+		if err = exposePerContainerLog(cluster, pod, e2eConfig.Setup.GetTimeout()); err != nil {
+			logger.Log.Warnf("export kubernetes pod log failure: %v", err)
+		}
+	})
+	if err != nil {
+		logger.Log.Warnf("listen kubernetes pod event failure: %v", err)
+	}
+
 	// run steps
 	err = RunStepsAndWait(e2eConfig.Setup.Steps, e2eConfig.Setup.GetTimeout(), cluster)
 	if err != nil {
@@ -124,8 +135,14 @@ func KindSetup(e2eConfig *config.E2EConfig) error {
 		return err
 	}
 
+	// expose logs
+	if err = exposeLogs(cluster, listener, e2eConfig.Setup.GetTimeout()); err != nil {
+		logger.Log.Errorf("export logs error: %v", err)
+		return err
+	}
+
 	// expose ports
-	err = exposeKindService(e2eConfig.Setup.Kind.ExposePorts, e2eConfig.Setup.GetTimeout(), kubeConfigPath)
+	err = exposeKindService(e2eConfig.Setup.Kind.ExposePorts, e2eConfig.Setup.GetTimeout(), cluster)
 	if err != nil {
 		logger.Log.Errorf("export ports error: %v", err)
 		return err
@@ -174,12 +191,12 @@ func createKindCluster(kindConfigPath string, e2eConfig *config.E2EConfig) error
 	return nil
 }
 
-func getWaitOptions(kubeConfigYaml []byte, wait *config.Wait) (options *ctlwait.WaitOptions, err error) {
+func getWaitOptions(cluster *util.K8sClusterInfo, wait *config.Wait) (options *ctlwait.WaitOptions, err error) {
 	if strings.Contains(wait.Resource, "/") && wait.LabelSelector != "" {
 		return nil, fmt.Errorf("when passing resource.group/resource.name in Resource, the labelSelector can not be set at the same time")
 	}
 
-	restClientGetter := util.NewSimpleRESTClientGetter(wait.Namespace, string(kubeConfigYaml))
+	restClientGetter := cluster.CopyClusterToNamespace(wait.Namespace)
 	silenceOutput, _ := os.Open(os.DevNull)
 	ioStreams := genericclioptions.IOStreams{In: os.Stdin, Out: silenceOutput, ErrOut: os.Stderr}
 	waitFlags := ctlwait.NewWaitFlags(restClientGetter, ioStreams)
@@ -209,7 +226,7 @@ func getWaitOptions(kubeConfigYaml []byte, wait *config.Wait) (options *ctlwait.
 	return options, nil
 }
 
-func createByManifest(c *kubernetes.Clientset, dc dynamic.Interface, manifest config.Manifest) error {
+func createByManifest(c *util.K8sClusterInfo, manifest config.Manifest) error {
 	files, err := util.GetManifests(manifest.Path)
 	if err != nil {
 		logger.Log.Error("get manifests failed")
@@ -218,7 +235,7 @@ func createByManifest(c *kubernetes.Clientset, dc dynamic.Interface, manifest co
 
 	for _, f := range files {
 		logger.Log.Infof("creating manifest %s", f)
-		err = util.OperateManifest(c, dc, f, apiv1.Create)
+		err = util.OperateManifest(c.Client, c.Interface, f, apiv1.Create)
 		if err != nil {
 			logger.Log.Errorf("create manifest %s failed", f)
 			return err
@@ -302,10 +319,10 @@ func buildKindPort(port string, ro runtime.Object, pod *v1.Pod) (*kindPort, erro
 	}, nil
 }
 
-func exposePerKindService(port config.KindExposePort, timeout time.Duration, clientGetter *util.SimpleRESTClientGetter,
+func exposePerKindService(port config.KindExposePort, timeout time.Duration, cluster *util.K8sClusterInfo,
 	client *rest.RESTClient, roundTripper http.RoundTripper, upgrader spdy.Upgrader, forward *kindPortForwardContext) error {
 	// find resource
-	builder := resource.NewBuilder(clientGetter).
+	builder := resource.NewBuilder(cluster).
 		WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
 		ContinueOnError().
 		NamespaceParam(port.Namespace).DefaultNamespace()
@@ -314,7 +331,7 @@ func exposePerKindService(port config.KindExposePort, timeout time.Duration, cli
 	if err != nil {
 		return err
 	}
-	forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(clientGetter, obj, timeout)
+	forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(cluster, obj, timeout)
 	if err != nil {
 		return err
 	}
@@ -393,15 +410,8 @@ func exposePerKindService(port config.KindExposePort, timeout time.Duration, cli
 	return nil
 }
 
-func exposeKindService(exports []config.KindExposePort, timeout time.Duration, kubeConfig string) error {
-	// round tripper
-	kubeConfigYaml, err := ioutil.ReadFile(kubeConfig)
-	if err != nil {
-		return err
-	}
-	clientGetter := util.NewSimpleRESTClientGetter("", string(kubeConfigYaml))
-
-	restConf, err := clientGetter.ToRESTConfig()
+func exposeKindService(exports []config.KindExposePort, timeout time.Duration, cluster *util.K8sClusterInfo) error {
+	restConf, err := cluster.ToRESTConfig()
 	if err != nil {
 		return err
 	}
@@ -436,7 +446,7 @@ func exposeKindService(exports []config.KindExposePort, timeout time.Duration, k
 		resourceCount:           len(exports),
 	}
 	for _, p := range exports {
-		if err := exposePerKindService(p, waitTimeout, clientGetter, client, tripperFor, upgrader, forwardContext); err != nil {
+		if err := exposePerKindService(p, waitTimeout, cluster, client, tripperFor, upgrader, forwardContext); err != nil {
 			return err
 		}
 	}
@@ -446,6 +456,66 @@ func exposeKindService(exports []config.KindExposePort, timeout time.Duration, k
 	return nil
 }
 
+func exposePerContainerLog(clientGetter *util.K8sClusterInfo, pod *v1.Pod, timeout time.Duration) error {
+	if pod.Status.Phase != v1.PodRunning {
+		return nil
+	}
+
+	file := filepath.Join(pod.Namespace, fmt.Sprintf("%s.log", pod.Name))
+	// check is followed
+	if logFollower.IsFollowed(file) {
+		return nil
+	}
+
+	logOptions := &v1.PodLogOptions{
+		Follow: true,
+	}
+	data, err := polymorphichelpers.LogsForObjectFn(clientGetter, pod, logOptions, timeout, true)
+	if err != nil {
+		return err
+	}
+
+	writer, err := logFollower.BuildLogWriter(file)
+	if err != nil {
+		return err
+	}
+	wg := &sync.WaitGroup{}
+	wg.Add(len(data))
+	// following each container
+	for _, resp := range data {
+		stream, err := resp.Stream(logFollower.Ctx)
+		if err != nil {
+			return err
+		}
+		go func() {
+			if finish := logFollower.ConsumeLog(writer, stream); finish != nil {
+				<-finish
+			}
+			wg.Done()
+		}()
+	}
+
+	go func() {
+		wg.Wait()
+		writer.Close()
+	}()
+
+	return nil
+}
+
+func exposeLogs(clientGetter *util.K8sClusterInfo, listener *KindContainerListener, timeout time.Duration) error {
+	pods, err := listener.GetAllPods()
+	if err != nil {
+		return err
+	}
+	for _, pod := range pods {
+		if err := exposePerContainerLog(clientGetter, pod, timeout); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func exportKindEnv(key, value, res string) error {
 	err := os.Setenv(key, value)
 	if err != nil {
diff --git a/internal/components/setup/kind_listener.go b/internal/components/setup/kind_listener.go
new file mode 100644
index 0000000..b55da0f
--- /dev/null
+++ b/internal/components/setup/kind_listener.go
@@ -0,0 +1,135 @@
+// 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 setup
+
+import (
+	"context"
+
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/cli-runtime/pkg/resource"
+
+	"github.com/apache/skywalking-infra-e2e/internal/logger"
+	"github.com/apache/skywalking-infra-e2e/internal/util"
+)
+
+// KindContainerListener listen or get all kubernetes pod
+type KindContainerListener struct {
+	clientGetter *util.K8sClusterInfo
+	ctx          context.Context
+	ctxCancel    context.CancelFunc
+}
+
+func NewKindContainerListener(ctx context.Context, clientGetter *util.K8sClusterInfo) *KindContainerListener {
+	childCtx, cancelFunc := context.WithCancel(ctx)
+	return &KindContainerListener{
+		clientGetter: clientGetter,
+		ctx:          childCtx,
+		ctxCancel:    cancelFunc,
+	}
+}
+
+// Listen pod event
+func (c *KindContainerListener) Listen(consumer func(pod *v1.Pod)) error {
+	result := c.buildSearchResult()
+
+	runtimeObject, err := result.Object()
+	if err != nil {
+		return err
+	}
+	watchVersion, err := meta.NewAccessor().ResourceVersion(runtimeObject)
+	if err != nil {
+		return err
+	}
+
+	watcher, err := result.Watch(watchVersion)
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		for {
+			select {
+			case event := <-watcher.ResultChan():
+				switch event.Type {
+				case watch.Added, watch.Modified:
+					pod, err := c.unstructuredToPod(event.Object.(*unstructured.Unstructured))
+					if err != nil {
+						continue
+					}
+					consumer(pod)
+				case watch.Error:
+					errObject := apierrors.FromObject(event.Object)
+					statusErr := errObject.(*apierrors.StatusError)
+					logger.Log.Warnf("watch kubernetes pod error, %v", statusErr)
+				}
+			case <-c.ctx.Done():
+				watcher.Stop()
+				c.ctxCancel()
+				return
+			}
+		}
+	}()
+
+	return nil
+}
+
+func (c *KindContainerListener) GetAllPods() ([]*v1.Pod, error) {
+	result := c.buildSearchResult()
+	infos, err := result.Infos()
+	if err != nil {
+		return nil, err
+	}
+
+	pods := make([]*v1.Pod, 0)
+	for _, info := range infos {
+		pod, err := c.unstructuredToPod(info.Object.(*unstructured.Unstructured))
+		if err != nil {
+			return nil, err
+		}
+		pods = append(pods, pod)
+	}
+	return pods, nil
+}
+
+func (c *KindContainerListener) Stop() {
+	c.ctxCancel()
+}
+
+func (c *KindContainerListener) buildSearchResult() *resource.Result {
+	return resource.NewBuilder(c.clientGetter).
+		Unstructured().
+		AllNamespaces(true).
+		ResourceTypeOrNameArgs(true, "pods").
+		Latest().
+		Flatten().
+		Do()
+}
+
+func (c *KindContainerListener) unstructuredToPod(object *unstructured.Unstructured) (*v1.Pod, error) {
+	var pod v1.Pod
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(object.UnstructuredContent(), &pod); err != nil {
+		return nil, err
+	}
+	return &pod, nil
+}
diff --git a/internal/util/config.go b/internal/util/config.go
index 679235d..ad082b5 100644
--- a/internal/util/config.go
+++ b/internal/util/config.go
@@ -28,6 +28,7 @@ import (
 var (
 	CfgFile string
 	WorkDir string
+	LogDir  string
 )
 
 // ResolveAbs resolves the relative path (relative to CfgFile) to an absolute file path.
diff --git a/internal/util/env_log.go b/internal/util/env_log.go
new file mode 100644
index 0000000..a696c47
--- /dev/null
+++ b/internal/util/env_log.go
@@ -0,0 +1,112 @@
+// 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 (
+	"bufio"
+	"context"
+	"io"
+	"os"
+	"path/filepath"
+	"sync"
+)
+
+type ResourceLogFollower struct {
+	Ctx        context.Context
+	cancelFunc context.CancelFunc
+	basePath   string
+	followLock *sync.RWMutex
+	following  map[string]bool
+}
+
+func NewResourceLogFollower(ctx context.Context, basePath string) *ResourceLogFollower {
+	childCtx, cancelFunc := context.WithCancel(ctx)
+	return &ResourceLogFollower{
+		Ctx:        childCtx,
+		cancelFunc: cancelFunc,
+		basePath:   basePath,
+		followLock: &sync.RWMutex{},
+		following:  make(map[string]bool),
+	}
+}
+
+func (l *ResourceLogFollower) BuildLogWriter(path string) (*os.File, error) {
+	logFile := l.buildLogFilename(path)
+	if err := os.MkdirAll(filepath.Dir(logFile), os.ModePerm); err != nil {
+		return nil, err
+	}
+	if _, err := os.Stat(logFile); os.IsExist(err) {
+		if err := os.Remove(logFile); err != nil {
+			return nil, err
+		}
+	}
+
+	return os.Create(logFile)
+}
+
+func (l *ResourceLogFollower) ConsumeLog(logWriter *os.File, stream io.ReadCloser) <-chan struct{} {
+	if l.IsFollowed(logWriter.Name()) {
+		return nil
+	}
+
+	finished := make(chan struct{}, 1)
+	go func() {
+		defer func() {
+			stream.Close()
+			close(finished)
+		}()
+
+		r := bufio.NewReader(stream)
+		for {
+			bytes, err := r.ReadBytes('\n')
+			if err != nil {
+				if err != io.EOF {
+					return
+				}
+				return
+			}
+
+			l.writeFollowed(logWriter)
+			if _, err := logWriter.Write(bytes); err != nil {
+				return
+			}
+		}
+	}()
+	return finished
+}
+
+func (l *ResourceLogFollower) IsFollowed(path string) bool {
+	l.followLock.RLock()
+	defer l.followLock.RUnlock()
+	return l.following[l.buildLogFilename(path)]
+}
+
+func (l *ResourceLogFollower) Close() {
+	l.cancelFunc()
+}
+
+func (l *ResourceLogFollower) buildLogFilename(path string) string {
+	return filepath.Join(l.basePath, path)
+}
+
+func (l *ResourceLogFollower) writeFollowed(writer *os.File) {
+	l.followLock.Lock()
+	defer l.followLock.Unlock()
+	l.following[writer.Name()] = true
+}
diff --git a/internal/util/k8s.go b/internal/util/k8s.go
index 8c4be1f..2da0496 100644
--- a/internal/util/k8s.go
+++ b/internal/util/k8s.go
@@ -27,16 +27,17 @@ import (
 	"strings"
 
 	apiv1 "k8s.io/api/admission/v1"
-
 	"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/runtime"
-	"k8s.io/client-go/dynamic"
-
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
 	yamlutil "k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/discovery/cached/memory"
+	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/restmapper"
 	"k8s.io/client-go/tools/clientcmd"
 
@@ -45,8 +46,10 @@ import (
 
 // K8sClusterInfo created when connect to cluster
 type K8sClusterInfo struct {
-	Client    *kubernetes.Clientset
-	Interface dynamic.Interface
+	Client     *kubernetes.Clientset
+	Interface  dynamic.Interface
+	restConfig *rest.Config
+	namespace  string
 }
 
 // ConnectToK8sCluster gets clientSet and dynamic client from k8s config file.
@@ -65,9 +68,64 @@ func ConnectToK8sCluster(kubeConfigPath string) (info *K8sClusterInfo, err error
 		return nil, err
 	}
 
+	kubeConfigYaml, err := ioutil.ReadFile(kubeConfigPath)
+	if err != nil {
+		return nil, err
+	}
+	restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
+	if err != nil {
+		return nil, err
+	}
+
 	logger.Log.Info("connect to k8s cluster succeeded")
 
-	return &K8sClusterInfo{c, dc}, nil
+	return &K8sClusterInfo{c, dc, restConfig, ""}, nil
+}
+
+func (c *K8sClusterInfo) CopyClusterToNamespace(namespace string) *K8sClusterInfo {
+	return &K8sClusterInfo{
+		Client:     c.Client,
+		Interface:  c.Interface,
+		restConfig: c.restConfig,
+		namespace:  namespace,
+	}
+}
+
+func (c *K8sClusterInfo) ToRESTConfig() (*rest.Config, error) {
+	return c.restConfig, nil
+}
+
+func (c *K8sClusterInfo) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	config, err := c.ToRESTConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	config.Burst = 100
+
+	discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
+	return memory.NewMemCacheClient(discoveryClient), nil
+}
+
+func (c *K8sClusterInfo) ToRESTMapper() (meta.RESTMapper, error) {
+	discoveryClient, err := c.ToDiscoveryClient()
+	if err != nil {
+		return nil, err
+	}
+
+	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
+	expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
+	return expander, nil
+}
+
+func (c *K8sClusterInfo) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
+
+	overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
+	overrides.Context.Namespace = c.namespace
+
+	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
 }
 
 // GetManifests recursively gets all yml and yaml files from manifests string.
diff --git a/internal/util/restClientGetter.go b/internal/util/restClientGetter.go
deleted file mode 100644
index 09b0236..0000000
--- a/internal/util/restClientGetter.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// 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 (
-	"k8s.io/apimachinery/pkg/api/meta"
-	"k8s.io/client-go/discovery"
-	"k8s.io/client-go/discovery/cached/memory"
-	"k8s.io/client-go/rest"
-	"k8s.io/client-go/restmapper"
-	"k8s.io/client-go/tools/clientcmd"
-)
-
-// SimpleRESTClientGetter implements genericclioptions.RESTClientGetter,
-// so that we can use kubectl's functions just by passing kubeConfig and namespace.
-type SimpleRESTClientGetter struct {
-	Namespace  string
-	KubeConfig string
-}
-
-func NewSimpleRESTClientGetter(namespace, kubeConfig string) *SimpleRESTClientGetter {
-	return &SimpleRESTClientGetter{
-		Namespace:  namespace,
-		KubeConfig: kubeConfig,
-	}
-}
-
-func (c *SimpleRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
-	config, err := clientcmd.RESTConfigFromKubeConfig([]byte(c.KubeConfig))
-	if err != nil {
-		return nil, err
-	}
-	return config, nil
-}
-
-func (c *SimpleRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
-	config, err := c.ToRESTConfig()
-	if err != nil {
-		return nil, err
-	}
-
-	config.Burst = 100
-
-	discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
-	return memory.NewMemCacheClient(discoveryClient), nil
-}
-
-func (c *SimpleRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
-	discoveryClient, err := c.ToDiscoveryClient()
-	if err != nil {
-		return nil, err
-	}
-
-	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
-	expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
-	return expander, nil
-}
-
-func (c *SimpleRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
-	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
-	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
-
-	overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
-	overrides.Context.Namespace = c.Namespace
-
-	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
-}