github.com/verrazzano/verrazzano@v1.7.0/tools/vz/cmd/bugreport/bugreport.go (about)

     1  // Copyright (c) 2022, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package bugreport
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"github.com/spf13/cobra"
    10  	"github.com/verrazzano/verrazzano/tools/vz/cmd/analyze"
    11  	cmdhelpers "github.com/verrazzano/verrazzano/tools/vz/cmd/helpers"
    12  	vzbugreport "github.com/verrazzano/verrazzano/tools/vz/pkg/bugreport"
    13  	"github.com/verrazzano/verrazzano/tools/vz/pkg/constants"
    14  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    15  	"io/fs"
    16  	"os"
    17  	"strings"
    18  	"time"
    19  )
    20  
    21  const (
    22  	flagErrorStr = "error fetching flag: %s"
    23  	CommandName  = "bug-report"
    24  	helpShort    = "Collect information from the cluster to report an issue"
    25  	helpLong     = `Verrazzano command line utility to collect data from the cluster, to report an issue`
    26  	helpExample  = `
    27  # Create a bug report file, bugreport.tar.gz, by collecting data from the cluster:
    28  vz bug-report --report-file bugreport.tar.gz
    29  
    30  When --report-file is not provided, the command creates bug-report.tar.gz in the current directory.
    31  
    32  # Create a bug report file, bugreport.tar.gz, including the additional namespace ns1 from the cluster:
    33  vz bug-report --report-file bugreport.tgz --include-namespaces ns1
    34  
    35  # The flag --include-namespaces accepts comma-separated values and can be specified multiple times. For example, the following commands create a bug report by including additional namespaces ns1, ns2, and ns3:
    36     a. vz bug-report --report-file bugreport.tgz --include-namespaces ns1,ns2,ns3
    37     b. vz bug-report --report-file bugreport.tgz --include-namespaces ns1,ns2 --include-namespaces ns3
    38  
    39  The values specified for the flag --include-namespaces are case-sensitive.
    40  
    41  # Use the --include-logs flag to collect the logs from the pods in one or more namespaces, by specifying the --include-namespaces flag.
    42  vz bug-report --report-file bugreport.tgz --include-namespaces ns1,ns2 --include-logs
    43  
    44  # The flag --duration collects logs for a specific period. The default value is 0, which collects the complete pod log. It supports seconds, minutes, and hours.
    45     a. vz bug-report --report-file bugreport.tgz --include-namespaces ns1 --include-logs --duration 3h
    46     b. vz bug-report --report-file bugreport.tgz --include-namespaces ns1,ns2 --include-logs --duration 5m
    47     c. vz bug-report --report-file bugreport.tgz --include-namespaces ns1,ns2 --include-logs --duration 300s
    48  `
    49  )
    50  
    51  const minLineLength = 100
    52  
    53  var kubeconfigFlagValPointer string
    54  var contextFlagValPointer string
    55  
    56  // NewCmdBugReport - creates cobra command for bug-report
    57  func NewCmdBugReport(vzHelper helpers.VZHelper) *cobra.Command {
    58  	cmd := cmdhelpers.NewCommand(vzHelper, CommandName, helpShort, helpLong)
    59  
    60  	cmd.RunE = func(cmd *cobra.Command, args []string) error {
    61  		return runCmdBugReport(cmd, args, vzHelper)
    62  	}
    63  
    64  	cmd.Example = helpExample
    65  	cmd.PersistentFlags().StringP(constants.BugReportFileFlagName, constants.BugReportFileFlagShort, constants.BugReportFileFlagValue, constants.BugReportFileFlagUsage)
    66  	cmd.PersistentFlags().StringSliceP(constants.BugReportIncludeNSFlagName, constants.BugReportIncludeNSFlagShort, []string{}, constants.BugReportIncludeNSFlagUsage)
    67  	cmd.PersistentFlags().BoolP(constants.VerboseFlag, constants.VerboseFlagShorthand, constants.VerboseFlagDefault, constants.VerboseFlagUsage)
    68  	cmd.PersistentFlags().BoolP(constants.BugReportLogFlagName, constants.BugReportLogFlagNameShort, constants.BugReportLogFlagDefault, constants.BugReportLogFlagNameUsage)
    69  	cmd.PersistentFlags().DurationP(constants.BugReportTimeFlagName, constants.BugReportTimeFlagNameShort, constants.BugReportTimeFlagDefaultTime, constants.BugReportTimeFlagNameUsage)
    70  
    71  	// Verifies that the CLI args are not set at the creation of a command
    72  	vzHelper.VerifyCLIArgsNil(cmd)
    73  
    74  	return cmd
    75  }
    76  
    77  func runCmdBugReport(cmd *cobra.Command, args []string, vzHelper helpers.VZHelper) error {
    78  	newCmd := analyze.NewCmdAnalyze(vzHelper)
    79  	err := setUpFlags(cmd, newCmd)
    80  	if err != nil {
    81  		return fmt.Errorf(flagErrorStr, err.Error())
    82  	}
    83  	analyzeErr := analyze.RunCmdAnalyze(newCmd, vzHelper, false)
    84  	if analyzeErr != nil {
    85  		fmt.Fprintf(vzHelper.GetErrorStream(), "Error calling vz analyze %s \n", analyzeErr.Error())
    86  	}
    87  
    88  	start := time.Now()
    89  	// determines the bug report file
    90  	bugReportFile, err := cmd.PersistentFlags().GetString(constants.BugReportFileFlagName)
    91  	if err != nil {
    92  		return fmt.Errorf(flagErrorStr, err.Error())
    93  	}
    94  	if bugReportFile == "" {
    95  		bugReportFile = constants.BugReportFileDefaultValue
    96  	}
    97  
    98  	// Get the kubernetes clientset, which will validate that the kubeconfigFlagValPointer and contextFlagValPointer are valid.
    99  	kubeClient, err := vzHelper.GetKubeClient(cmd)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	// Get the controller runtime client
   105  	client, err := vzHelper.GetClient(cmd)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	// Get the dynamic client to retrieve OAM resources
   111  	dynamicClient, err := vzHelper.GetDynamicClient(cmd)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	// Create the bug report file
   117  	var bugRepFile *os.File
   118  	if bugReportFile == constants.BugReportFileDefaultValue {
   119  		bugReportFile = strings.Replace(bugReportFile, "dt", start.Format(constants.DatetimeFormat), 1)
   120  		bugRepFile, err = os.CreateTemp(".", bugReportFile)
   121  		if err != nil && (errors.Is(err, fs.ErrPermission) || strings.Contains(err.Error(), constants.ReadOnly)) {
   122  			fmt.Fprintf(vzHelper.GetOutputStream(), "Warning: %s, creating report in current directory, using temp directory instead\n", fs.ErrPermission)
   123  			bugRepFile, err = os.CreateTemp("", bugReportFile)
   124  		}
   125  	} else {
   126  		bugRepFile, err = os.OpenFile(bugReportFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
   127  	}
   128  
   129  	if err != nil {
   130  		return fmt.Errorf("an error occurred while creating %s: %s", bugReportFile, err.Error())
   131  	}
   132  	defer bugRepFile.Close()
   133  
   134  	// Read the additional namespaces provided using flag --include-namespaces
   135  	moreNS, err := cmd.PersistentFlags().GetStringSlice(constants.BugReportIncludeNSFlagName)
   136  	if err != nil {
   137  		return fmt.Errorf("an error occurred while reading values for the flag --include-namespaces: %s", err.Error())
   138  	}
   139  	// If additional namespaces pods logs needs to be capture using flag --include-logs
   140  	isPodLog, err := cmd.PersistentFlags().GetBool(constants.BugReportLogFlagName)
   141  	if err != nil {
   142  		return fmt.Errorf("an error occurred while reading values for the flag --include-logs: %s", err.Error())
   143  	}
   144  
   145  	// If additional namespaces pods logs needs to be capture using flag with duration --duration
   146  	durationString, err := cmd.PersistentFlags().GetDuration(constants.BugReportTimeFlagName)
   147  	if err != nil {
   148  		return fmt.Errorf("an error occurred while reading values for the flag --duration: %s", err.Error())
   149  	}
   150  	durationValue := int64(durationString.Seconds())
   151  	if err != nil {
   152  		return fmt.Errorf("an error occurred,invalid value --duration: %s", err.Error())
   153  	}
   154  	if durationValue < 0 {
   155  		return fmt.Errorf("an error occurred, invalid duration can't be less than 1s: %d", durationValue)
   156  	}
   157  
   158  	// Create a temporary directory to place the cluster data
   159  	bugReportDir, err := os.MkdirTemp("", constants.BugReportDir)
   160  	if err != nil {
   161  		return fmt.Errorf("an error occurred while creating the directory to place cluster resources: %s", err.Error())
   162  	}
   163  	defer os.RemoveAll(bugReportDir)
   164  
   165  	// set the flag to control the display the resources captured
   166  	isVerbose, err := cmd.PersistentFlags().GetBool(constants.VerboseFlag)
   167  	if err != nil {
   168  		return fmt.Errorf("an error occurred while reading value for the flag %s: %s", constants.VerboseFlag, err.Error())
   169  	}
   170  	helpers.SetVerboseOutput(isVerbose)
   171  
   172  	// Capture cluster snapshot
   173  	clusterSnapshotCtx := helpers.ClusterSnapshotCtx{BugReportDir: bugReportDir, MoreNS: moreNS, PrintReportToConsole: false}
   174  	err = vzbugreport.CaptureClusterSnapshot(kubeClient, dynamicClient, client, vzHelper, vzbugreport.PodLogs{IsPodLog: isPodLog, Duration: durationValue}, clusterSnapshotCtx)
   175  	if err != nil {
   176  		os.Remove(bugRepFile.Name())
   177  		return fmt.Errorf(err.Error())
   178  	}
   179  
   180  	// Return an error when the command fails to collect anything from the cluster
   181  	// There will be bug-report.out and bug-report.err in bugReportDir, ignore them
   182  	if isDirEmpty(bugReportDir, 2) {
   183  		return fmt.Errorf("The bug-report command did not collect any file from the cluster. " +
   184  			"Please go through errors (if any), in the standard output.\n")
   185  	}
   186  
   187  	// Generate the bug report
   188  	err = helpers.CreateReportArchive(bugReportDir, bugRepFile)
   189  	if err != nil {
   190  		return fmt.Errorf("there is an error in creating the bug report, %s", err.Error())
   191  	}
   192  
   193  	brf, _ := os.Stat(bugRepFile.Name())
   194  	if brf.Size() > 0 {
   195  		msg := fmt.Sprintf("Created bug report: %s in %s\n", bugRepFile.Name(), time.Since(start))
   196  		fmt.Fprintf(vzHelper.GetOutputStream(), msg)
   197  		// Display a message to check the standard error, if the command reported any error and continued
   198  		if helpers.IsErrorReported() {
   199  			fmt.Fprintf(vzHelper.GetOutputStream(), constants.BugReportError+"\n")
   200  		}
   201  		displayWarning(msg, vzHelper)
   202  	} else {
   203  		// Verrazzano is not installed, remove the empty bug report file
   204  		os.Remove(bugRepFile.Name())
   205  	}
   206  	return nil
   207  }
   208  
   209  // displayWarning logs a warning message to check the contents of the bug report
   210  func displayWarning(successMessage string, helper helpers.VZHelper) {
   211  	// This might be the efficient way, but does the job of displaying a formatted message
   212  
   213  	// Draw a line to differentiate the warning from the info message
   214  	count := len(successMessage)
   215  	if len(successMessage) < minLineLength {
   216  		count = minLineLength
   217  	}
   218  	sep := strings.Repeat(constants.LineSeparator, count)
   219  
   220  	// Any change in BugReportWarning, requires a change here to adjust the whitespace characters before the message
   221  	wsCount := count - len(constants.BugReportWarning)
   222  
   223  	fmt.Fprintf(helper.GetOutputStream(), sep+"\n")
   224  	fmt.Fprintf(helper.GetOutputStream(), strings.Repeat(" ", wsCount/2)+constants.BugReportWarning+"\n")
   225  	fmt.Fprintf(helper.GetOutputStream(), sep+"\n")
   226  }
   227  
   228  // isDirEmpty returns whether the directory is empty or not, ignoring ignoreFilesCount number of files
   229  func isDirEmpty(directory string, ignoreFilesCount int) bool {
   230  	entries, err := os.ReadDir(directory)
   231  	if err != nil {
   232  		return false
   233  	}
   234  	return len(entries) == ignoreFilesCount
   235  }
   236  
   237  // creates a new bug-report cobra command, initailizes and sets the required flags, and runs the new command.
   238  // Returns the original error that's passed in as a parameter to preserve the error received from previous cli command failure.
   239  func CallVzBugReport(cmd *cobra.Command, vzHelper helpers.VZHelper, err error) error {
   240  	newCmd := NewCmdBugReport(vzHelper)
   241  	flagErr := setUpFlags(cmd, newCmd)
   242  	if flagErr != nil {
   243  		return flagErr
   244  	}
   245  	bugReportErr := runCmdBugReport(newCmd, []string{}, vzHelper)
   246  	if bugReportErr != nil {
   247  		fmt.Fprintf(vzHelper.GetErrorStream(), "Error calling vz bug-report %s \n", bugReportErr.Error())
   248  	}
   249  	// return original error from running vz command which was passed into CallVzBugReport as a parameter
   250  	return err
   251  }
   252  
   253  // AutoBugReport checks that AutoBugReportFlag is set and then kicks off vz bugreport CLI command. It returns the same error that is passed in
   254  func AutoBugReport(cmd *cobra.Command, vzHelper helpers.VZHelper, err error) error {
   255  	autoBugReportFlag, errFlag := cmd.Flags().GetBool(constants.AutoBugReportFlag)
   256  	if errFlag != nil {
   257  		fmt.Fprintf(vzHelper.GetOutputStream(), "Error fetching flags: %s", errFlag.Error())
   258  		return err
   259  	}
   260  	if autoBugReportFlag {
   261  		//err returned from CallVzBugReport is the same error that's passed in, the error that was returned from either installVerrazzano() or waitForInstallToComplete()
   262  		err = CallVzBugReport(cmd, vzHelper, err)
   263  	}
   264  	return err
   265  }
   266  
   267  func setUpFlags(cmd *cobra.Command, newCmd *cobra.Command) error {
   268  	kubeconfigFlag, errFlag := cmd.Flags().GetString(constants.GlobalFlagKubeConfig)
   269  	if errFlag != nil {
   270  		return fmt.Errorf(flagErrorStr, errFlag.Error())
   271  	}
   272  	contextFlag, errFlag2 := cmd.Flags().GetString(constants.GlobalFlagContext)
   273  	if errFlag2 != nil {
   274  		return fmt.Errorf(flagErrorStr, errFlag2.Error())
   275  	}
   276  	newCmd.Flags().StringVar(&kubeconfigFlagValPointer, constants.GlobalFlagKubeConfig, "", constants.GlobalFlagKubeConfigHelp)
   277  	newCmd.Flags().StringVar(&contextFlagValPointer, constants.GlobalFlagContext, "", constants.GlobalFlagContextHelp)
   278  	newCmd.Flags().Set(constants.GlobalFlagKubeConfig, kubeconfigFlag)
   279  	newCmd.Flags().Set(constants.GlobalFlagContext, contextFlag)
   280  	return nil
   281  }