istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/bug-report/pkg/bugreport/flags.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 bugreport
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"os"
    21  	"time"
    22  
    23  	jsonpatch "github.com/evanphx/json-patch/v5"
    24  	"github.com/spf13/cobra"
    25  	"sigs.k8s.io/yaml"
    26  
    27  	"istio.io/istio/pkg/kube/inject"
    28  	"istio.io/istio/pkg/slices"
    29  	config2 "istio.io/istio/tools/bug-report/pkg/config"
    30  )
    31  
    32  var (
    33  	startTime, endTime, configFile, tempDir, outputDir string
    34  	included, excluded                                 []string
    35  	commandTimeout, since                              time.Duration
    36  	gConfig                                            = &config2.BugReportConfig{}
    37  )
    38  
    39  func addFlags(cmd *cobra.Command, args *config2.BugReportConfig) {
    40  	// k8s client config
    41  	cmd.PersistentFlags().StringVarP(&args.KubeConfigPath, "kubeconfig", "c", "",
    42  		"Path to kube config.")
    43  	cmd.PersistentFlags().StringVar(&args.Context, "context", "",
    44  		"Name of the kubeconfig Context to use.")
    45  
    46  	// input config
    47  	cmd.PersistentFlags().StringVarP(&configFile, "filename", "f", "",
    48  		"Path to a file containing configuration in YAML format. The file contents are applied over the default "+
    49  			"values and flag settings, with lists being replaced per JSON merge semantics.")
    50  
    51  	// dry run
    52  	cmd.PersistentFlags().BoolVarP(&args.DryRun, "dry-run", "", false,
    53  		"Only log commands that would be run, don't fetch or write.")
    54  
    55  	// full secrets
    56  	cmd.PersistentFlags().BoolVarP(&args.FullSecrets, "full-secrets", "", false,
    57  		"If set, secret contents are included in output.")
    58  
    59  	// istio namespaces
    60  	cmd.PersistentFlags().StringVar(&args.IstioNamespace, "istio-namespace", bugReportDefaultIstioNamespace,
    61  		"Namespace where Istio control plane is installed.")
    62  
    63  	// timeouts and max sizes
    64  	cmd.PersistentFlags().DurationVar(&commandTimeout, "timeout", bugReportDefaultTimeout,
    65  		"Maximum amount of time to spend fetching logs. When timeout is reached "+
    66  			"only the logs captured so far are saved to the archive.")
    67  	// include / exclude specs
    68  	cmd.PersistentFlags().StringSliceVar(&included, "include", bugReportDefaultInclude,
    69  		"Spec for which pod's proxy logs to include in the archive. See above for format and examples.")
    70  	cmd.PersistentFlags().StringSliceVar(&excluded, "exclude", bugReportDefaultExclude,
    71  		"Spec for which pod's proxy logs to exclude from the archive, after the include spec "+
    72  			"is processed. See above for format and examples.")
    73  
    74  	// log time ranges
    75  	cmd.PersistentFlags().StringVar(&startTime, "start-time", "",
    76  		"Start time for the range of log entries to include in the archive. "+
    77  			"Default is the infinite past. If set, --duration must be unset.")
    78  	cmd.PersistentFlags().StringVar(&endTime, "end-time", "",
    79  		"End time for the range of log entries to include in the archive. Default is now.")
    80  	cmd.PersistentFlags().DurationVar(&since, "duration", 0,
    81  		"How far to go back in time from end-time for log entries to include in the archive. "+
    82  			"Default is infinity. If set, --start-time must be unset.")
    83  
    84  	// log error control
    85  	cmd.PersistentFlags().StringSliceVar(&args.CriticalErrors, "critical-errs", nil,
    86  		"List of comma separated glob patterns to match against log error strings. "+
    87  			"If any pattern matches an error in the log, the logs is given the highest priority for archive inclusion.")
    88  	cmd.PersistentFlags().StringSliceVar(&args.IgnoredErrors, "ignore-errs", nil,
    89  		"List of comma separated glob patterns to match against log error strings. "+
    90  			"Any error matching these patterns is ignored when calculating the log importance heuristic.")
    91  
    92  	// working dir to store temporary artifacts
    93  	cmd.PersistentFlags().StringVar(&tempDir, "dir", "",
    94  		"Set a specific directory for temporary artifact storage.")
    95  
    96  	cmd.PersistentFlags().StringVar(&outputDir, "output-dir", "",
    97  		"Set a specific directory for output archive file.")
    98  
    99  	// in-flight request limit
   100  	cmd.PersistentFlags().IntVar(&args.RequestConcurrency, "rq-concurrency", 0,
   101  		"Set the concurrency limit of requests to the Kubernetes API server, defaults to 32.")
   102  }
   103  
   104  func parseConfig() (*config2.BugReportConfig, error) {
   105  	fileConfig := &config2.BugReportConfig{}
   106  	if configFile != "" {
   107  		b, err := os.ReadFile(configFile)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  		if err := yaml.Unmarshal(b, fileConfig); err != nil {
   112  			return nil, err
   113  		}
   114  	}
   115  
   116  	if err := parseTimes(gConfig, startTime, endTime, since); err != nil {
   117  		return nil, err
   118  	}
   119  	gConfig.CommandTimeout = config2.Duration(commandTimeout)
   120  	for _, s := range included {
   121  		ss := &config2.SelectionSpec{}
   122  		if err := ss.UnmarshalJSON([]byte(s)); err != nil {
   123  			return nil, err
   124  		}
   125  		gConfig.Include = append(gConfig.Include, ss)
   126  	}
   127  	// Exclude default system namespaces eg: bugReportDefaultExclude
   128  	dss := &config2.SelectionSpec{}
   129  	if err := dss.UnmarshalJSON([]byte(bugReportDefaultExclude[0])); err != nil {
   130  		return nil, err
   131  	}
   132  	gConfig.Exclude = append(gConfig.Exclude, dss)
   133  	// Exclude namespace provided by --exclude flag
   134  	for _, s := range excluded {
   135  		ess := &config2.SelectionSpec{}
   136  		if err := ess.UnmarshalJSON([]byte(s)); err != nil {
   137  			return nil, err
   138  		}
   139  		ess.Namespaces = slices.FilterInPlace(ess.Namespaces, func(ns string) bool { return !inject.IgnoredNamespaces.Contains(ns) })
   140  		if len(ess.Namespaces) > 0 {
   141  			gConfig.Exclude = append(gConfig.Exclude, ess)
   142  		}
   143  	}
   144  	return overlayConfig(fileConfig, gConfig)
   145  }
   146  
   147  func parseTimes(config *config2.BugReportConfig, startTime, endTime string, duration time.Duration) error {
   148  	if startTime == "" && endTime == "" {
   149  		config.TimeFilterApplied = false
   150  	} else {
   151  		config.TimeFilterApplied = true
   152  	}
   153  
   154  	config.EndTime = time.Now()
   155  	config.Since = config2.Duration(duration)
   156  	if endTime != "" {
   157  		var err error
   158  		config.EndTime, err = time.Parse(time.RFC3339, endTime)
   159  		if err != nil {
   160  			return fmt.Errorf("bad format for end-time: %s, expect RFC3339 e.g. %s", endTime, time.RFC3339)
   161  		}
   162  	}
   163  	if config.Since != 0 {
   164  		if startTime != "" {
   165  			return fmt.Errorf("only one --start-time or --duration may be set")
   166  		}
   167  		config.StartTime = config.EndTime.Add(-1 * time.Duration(config.Since))
   168  	} else {
   169  		var err error
   170  		if startTime == "" {
   171  			config.StartTime = time.Time{}
   172  		} else {
   173  			config.StartTime, err = time.Parse(time.RFC3339, startTime)
   174  			if err != nil {
   175  				return fmt.Errorf("bad format for start-time: %s, expect RFC3339 e.g. %s", startTime, time.RFC3339)
   176  			}
   177  			if config.StartTime.After(config.EndTime) {
   178  				return fmt.Errorf("bad format for start-time and end-time: start-time is after end-time")
   179  			}
   180  		}
   181  	}
   182  	return nil
   183  }
   184  
   185  func overlayConfig(base, overlay *config2.BugReportConfig) (*config2.BugReportConfig, error) {
   186  	bj, err := json.Marshal(base)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	oj, err := json.Marshal(overlay)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	mj, err := jsonpatch.MergePatch(bj, oj)
   196  	if err != nil {
   197  		return nil, fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
   198  	}
   199  
   200  	out := &config2.BugReportConfig{}
   201  	err = json.Unmarshal(mj, out)
   202  	return out, err
   203  }