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 }