istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/bug-report/pkg/kubectlcmd/kubectlcmd.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kubectlcmd
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"os/exec"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	"istio.io/istio/operator/pkg/util"
    27  	"istio.io/istio/pkg/kube"
    28  	"istio.io/istio/pkg/log"
    29  	"istio.io/istio/pkg/util/sets"
    30  	"istio.io/istio/tools/bug-report/pkg/common"
    31  )
    32  
    33  const (
    34  	// The default number of in-flight requests allowed for the runner.
    35  	defaultActiveRequestLimit = 32
    36  
    37  	// reportInterval controls how frequently to output progress reports on running tasks.
    38  	reportInterval = 30 * time.Second
    39  )
    40  
    41  type Runner struct {
    42  	Client kube.CLIClient
    43  
    44  	// Used to limit the number of concurrent tasks.
    45  	taskSem chan struct{}
    46  
    47  	// runningTasks tracks the in-flight fetch operations for user feedback.
    48  	runningTasks   sets.String
    49  	runningTasksMu sync.RWMutex
    50  
    51  	// runningTasksTicker is the report interval for running tasks.
    52  	runningTasksTicker *time.Ticker
    53  }
    54  
    55  func NewRunner(activeRqLimit int) *Runner {
    56  	if activeRqLimit <= 0 {
    57  		activeRqLimit = defaultActiveRequestLimit
    58  	}
    59  	return &Runner{
    60  		taskSem:            make(chan struct{}, activeRqLimit),
    61  		runningTasks:       sets.New[string](),
    62  		runningTasksMu:     sync.RWMutex{},
    63  		runningTasksTicker: time.NewTicker(reportInterval),
    64  	}
    65  }
    66  
    67  func (r *Runner) SetClient(client kube.CLIClient) {
    68  	r.Client = client
    69  }
    70  
    71  func (r *Runner) ReportRunningTasks() {
    72  	go func() {
    73  		time.Sleep(reportInterval)
    74  		for range r.runningTasksTicker.C {
    75  			r.printRunningTasks()
    76  		}
    77  	}()
    78  }
    79  
    80  // Options contains the Run options.
    81  type Options struct {
    82  	// Path to the kubeconfig file.
    83  	Kubeconfig string
    84  	// ComponentName of the kubeconfig context to use.
    85  	Context string
    86  
    87  	// namespace - k8s namespace for Run command
    88  	Namespace string
    89  
    90  	// DryRun performs all steps but only logs the Run command without running it.
    91  	DryRun bool
    92  	// Maximum amount of time to wait for resources to be ready after install when Wait=true.
    93  	WaitTimeout time.Duration
    94  
    95  	// output - output mode for Run i.e. --output.
    96  	Output string
    97  
    98  	// extraArgs - more args to be added to the Run command, which are appended to
    99  	// the end of the Run command.
   100  	ExtraArgs []string
   101  }
   102  
   103  // Logs returns the logs for the given namespace/pod/container.
   104  func (r *Runner) Logs(namespace, pod, container string, previous, dryRun bool) (string, error) {
   105  	if dryRun {
   106  		return fmt.Sprintf("Dry run: would be running client.PodLogs(%s, %s, %s)", pod, namespace, container), nil
   107  	}
   108  	// ignore cancellation errors since this is subject to global timeout.
   109  	task := fmt.Sprintf("PodLogs %s/%s/%s", namespace, pod, container)
   110  	r.addRunningTask(task)
   111  	defer r.removeRunningTask(task)
   112  	return r.Client.PodLogs(context.TODO(), pod, namespace, container, previous)
   113  }
   114  
   115  // EnvoyGet sends a GET request for the URL in the Envoy container in the given namespace/pod and returns the result.
   116  func (r *Runner) EnvoyGet(namespace, pod, url string, dryRun bool) (string, error) {
   117  	if dryRun {
   118  		return fmt.Sprintf("Dry run: would be running client.EnvoyDo(%s, %s, %s)", pod, namespace, url), nil
   119  	}
   120  	task := fmt.Sprintf("ProxyGet %s/%s:%s", namespace, pod, url)
   121  	r.addRunningTask(task)
   122  	defer r.removeRunningTask(task)
   123  	out, err := r.Client.EnvoyDo(context.TODO(), pod, namespace, "GET", url)
   124  	return string(out), err
   125  }
   126  
   127  // Cat runs the cat command for the given path in the given namespace/pod/container.
   128  func (r *Runner) Cat(namespace, pod, container, path string, dryRun bool) (string, error) {
   129  	cmdStr := "cat " + path
   130  	if dryRun {
   131  		return fmt.Sprintf("Dry run: would be running podExec %s/%s/%s:%s", pod, namespace, container, cmdStr), nil
   132  	}
   133  	task := fmt.Sprintf("PodExec %s/%s/%s:%s", namespace, pod, container, cmdStr)
   134  	r.addRunningTask(task)
   135  	defer r.removeRunningTask(task)
   136  	stdout, stderr, err := r.Client.PodExec(pod, namespace, container, cmdStr)
   137  	if err != nil {
   138  		return "", fmt.Errorf("podExec error: %s\n\nstderr:\n%s\n\nstdout:\n%s",
   139  			err, util.ConsolidateLog(stderr), stdout)
   140  	}
   141  	return stdout, nil
   142  }
   143  
   144  // Exec runs exec for the given command in the given namespace/pod/container.
   145  func (r *Runner) Exec(namespace, pod, container, cmdStr string, dryRun bool) (string, error) {
   146  	if dryRun {
   147  		return fmt.Sprintf("Dry run: would be running podExec %s/%s/%s:%s", pod, namespace, container, cmdStr), nil
   148  	}
   149  	task := fmt.Sprintf("PodExec %s/%s/%s:%s", namespace, pod, container, cmdStr)
   150  	r.addRunningTask(task)
   151  	defer r.removeRunningTask(task)
   152  	stdout, stderr, err := r.Client.PodExec(pod, namespace, container, cmdStr)
   153  	if err != nil {
   154  		return "", fmt.Errorf("podExec error: %s\n\nstderr:\n%s\n\nstdout:\n%s",
   155  			err, util.ConsolidateLog(stderr), stdout)
   156  	}
   157  	return stdout, nil
   158  }
   159  
   160  // RunCmd runs the given command in kubectl, adding -n namespace if namespace is not empty.
   161  func (r *Runner) RunCmd(command, namespace, kubeConfig, kubeContext string, dryRun bool) (string, error) {
   162  	return r.Run(strings.Split(command, " "),
   163  		&Options{
   164  			Namespace:  namespace,
   165  			DryRun:     dryRun,
   166  			Kubeconfig: kubeConfig,
   167  			Context:    kubeContext,
   168  		})
   169  }
   170  
   171  // Run runs the kubectl command by specifying subcommands in subcmds with opts.
   172  func (r *Runner) Run(subcmds []string, opts *Options) (string, error) {
   173  	args := subcmds
   174  	if opts.Kubeconfig != "" {
   175  		args = append(args, "--kubeconfig", opts.Kubeconfig)
   176  	}
   177  	if opts.Context != "" {
   178  		args = append(args, "--context", opts.Context)
   179  	}
   180  	if opts.Namespace != "" {
   181  		args = append(args, "-n", opts.Namespace)
   182  	}
   183  	if opts.Output != "" {
   184  		args = append(args, "-o", opts.Output)
   185  	}
   186  	args = append(args, opts.ExtraArgs...)
   187  
   188  	cmd := exec.Command("kubectl", args...)
   189  	var stdout, stderr bytes.Buffer
   190  	cmd.Stdout = &stdout
   191  	cmd.Stderr = &stderr
   192  
   193  	cmdStr := strings.Join(args, " ")
   194  
   195  	if opts.DryRun {
   196  		log.Infof("dry run mode: would be running this cmd:\nkubectl %s\n", cmdStr)
   197  		return "", nil
   198  	}
   199  
   200  	task := fmt.Sprintf("kubectl %s", cmdStr)
   201  	r.addRunningTask(task)
   202  	defer r.removeRunningTask(task)
   203  	if err := cmd.Run(); err != nil {
   204  		return "", fmt.Errorf("kubectl error: %s\n\nstderr:\n%s\n\nstdout:\n%s",
   205  			err, util.ConsolidateLog(stderr.String()), stdout.String())
   206  	}
   207  
   208  	return stdout.String(), nil
   209  }
   210  
   211  func (r *Runner) printRunningTasks() {
   212  	r.runningTasksMu.RLock()
   213  	defer r.runningTasksMu.RUnlock()
   214  	if r.runningTasks.IsEmpty() {
   215  		return
   216  	}
   217  	common.LogAndPrintf("The following fetches are still running: \n")
   218  	for t := range r.runningTasks {
   219  		common.LogAndPrintf("  %s\n", t)
   220  	}
   221  	common.LogAndPrintf("\n")
   222  }
   223  
   224  func (r *Runner) addRunningTask(task string) {
   225  	// Limit the concurrency of running tasks.
   226  	r.taskSem <- struct{}{}
   227  
   228  	r.runningTasksMu.Lock()
   229  	defer r.runningTasksMu.Unlock()
   230  	log.Infof("STARTING %s", task)
   231  	r.runningTasks.Insert(task)
   232  }
   233  
   234  func (r *Runner) removeRunningTask(task string) {
   235  	defer func() {
   236  		// Free up a slot for another running task.
   237  		<-r.taskSem
   238  	}()
   239  
   240  	r.runningTasksMu.Lock()
   241  	defer r.runningTasksMu.Unlock()
   242  	log.Infof("COMPLETED %s", task)
   243  	r.runningTasks.Delete(task)
   244  }