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

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