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 }