You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

doctor.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package cmd
  5. import (
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "fmt"
  10. "io/ioutil"
  11. golog "log"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strings"
  16. "text/tabwriter"
  17. "code.gitea.io/gitea/models"
  18. "code.gitea.io/gitea/models/migrations"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/options"
  22. "code.gitea.io/gitea/modules/setting"
  23. "xorm.io/builder"
  24. "github.com/urfave/cli"
  25. )
  26. // CmdDoctor represents the available doctor sub-command.
  27. var CmdDoctor = cli.Command{
  28. Name: "doctor",
  29. Usage: "Diagnose problems",
  30. Description: "A command to diagnose problems with the current Gitea instance according to the given configuration.",
  31. Action: runDoctor,
  32. Flags: []cli.Flag{
  33. cli.BoolFlag{
  34. Name: "list",
  35. Usage: "List the available checks",
  36. },
  37. cli.BoolFlag{
  38. Name: "default",
  39. Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
  40. },
  41. cli.StringSliceFlag{
  42. Name: "run",
  43. Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
  44. },
  45. cli.BoolFlag{
  46. Name: "all",
  47. Usage: "Run all the available checks",
  48. },
  49. cli.BoolFlag{
  50. Name: "fix",
  51. Usage: "Automatically fix what we can",
  52. },
  53. cli.StringFlag{
  54. Name: "log-file",
  55. Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
  56. },
  57. },
  58. }
  59. type check struct {
  60. title string
  61. name string
  62. isDefault bool
  63. f func(ctx *cli.Context) ([]string, error)
  64. abortIfFailed bool
  65. skipDatabaseInit bool
  66. }
  67. // checklist represents list for all checks
  68. var checklist = []check{
  69. {
  70. // NOTE: this check should be the first in the list
  71. title: "Check paths and basic configuration",
  72. name: "paths",
  73. isDefault: true,
  74. f: runDoctorPathInfo,
  75. abortIfFailed: true,
  76. skipDatabaseInit: true,
  77. },
  78. {
  79. title: "Check Database Version",
  80. name: "check-db",
  81. isDefault: true,
  82. f: runDoctorCheckDBVersion,
  83. abortIfFailed: true,
  84. },
  85. {
  86. title: "Check if OpenSSH authorized_keys file is up-to-date",
  87. name: "authorized_keys",
  88. isDefault: true,
  89. f: runDoctorAuthorizedKeys,
  90. },
  91. {
  92. title: "Check if SCRIPT_TYPE is available",
  93. name: "script-type",
  94. isDefault: false,
  95. f: runDoctorScriptType,
  96. },
  97. {
  98. title: "Check if hook files are up-to-date and executable",
  99. name: "hooks",
  100. isDefault: false,
  101. f: runDoctorHooks,
  102. },
  103. {
  104. title: "Recalculate merge bases",
  105. name: "recalculate_merge_bases",
  106. isDefault: false,
  107. f: runDoctorPRMergeBase,
  108. },
  109. // more checks please append here
  110. }
  111. func runDoctor(ctx *cli.Context) error {
  112. // Silence the default loggers
  113. log.DelNamedLogger("console")
  114. log.DelNamedLogger(log.DEFAULT)
  115. // Now setup our own
  116. logFile := ctx.String("log-file")
  117. if !ctx.IsSet("log-file") {
  118. logFile = "doctor.log"
  119. }
  120. if len(logFile) == 0 {
  121. log.NewLogger(1000, "doctor", "console", `{"level":"NONE","stacktracelevel":"NONE","colorize":"%t"}`)
  122. } else if logFile == "-" {
  123. log.NewLogger(1000, "doctor", "console", `{"level":"trace","stacktracelevel":"NONE"}`)
  124. } else {
  125. log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
  126. }
  127. // Finally redirect the default golog to here
  128. golog.SetFlags(0)
  129. golog.SetPrefix("")
  130. golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
  131. if ctx.IsSet("list") {
  132. w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
  133. _, _ = w.Write([]byte("Default\tName\tTitle\n"))
  134. for _, check := range checklist {
  135. if check.isDefault {
  136. _, _ = w.Write([]byte{'*'})
  137. }
  138. _, _ = w.Write([]byte{'\t'})
  139. _, _ = w.Write([]byte(check.name))
  140. _, _ = w.Write([]byte{'\t'})
  141. _, _ = w.Write([]byte(check.title))
  142. _, _ = w.Write([]byte{'\n'})
  143. }
  144. return w.Flush()
  145. }
  146. var checks []check
  147. if ctx.Bool("all") {
  148. checks = checklist
  149. } else if ctx.IsSet("run") {
  150. addDefault := ctx.Bool("default")
  151. names := ctx.StringSlice("run")
  152. for i, name := range names {
  153. names[i] = strings.ToLower(strings.TrimSpace(name))
  154. }
  155. for _, check := range checklist {
  156. if addDefault && check.isDefault {
  157. checks = append(checks, check)
  158. continue
  159. }
  160. for _, name := range names {
  161. if name == check.name {
  162. checks = append(checks, check)
  163. break
  164. }
  165. }
  166. }
  167. } else {
  168. for _, check := range checklist {
  169. if check.isDefault {
  170. checks = append(checks, check)
  171. }
  172. }
  173. }
  174. dbIsInit := false
  175. for i, check := range checks {
  176. if !dbIsInit && !check.skipDatabaseInit {
  177. // Only open database after the most basic configuration check
  178. setting.EnableXORMLog = false
  179. if err := initDBDisableConsole(true); err != nil {
  180. fmt.Println(err)
  181. fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
  182. return nil
  183. }
  184. dbIsInit = true
  185. }
  186. fmt.Println("[", i+1, "]", check.title)
  187. messages, err := check.f(ctx)
  188. for _, message := range messages {
  189. fmt.Println("-", message)
  190. }
  191. if err != nil {
  192. fmt.Println("Error:", err)
  193. if check.abortIfFailed {
  194. return nil
  195. }
  196. } else {
  197. fmt.Println("OK.")
  198. }
  199. fmt.Println()
  200. }
  201. return nil
  202. }
  203. func runDoctorPathInfo(ctx *cli.Context) ([]string, error) {
  204. res := make([]string, 0, 10)
  205. if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() {
  206. res = append(res, fmt.Sprintf("Failed to find configuration file at '%s'.", setting.CustomConf))
  207. res = append(res, fmt.Sprintf("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf))
  208. res = append(res, "Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.")
  209. return res, fmt.Errorf("can't proceed without a configuration file")
  210. }
  211. setting.NewContext()
  212. fail := false
  213. check := func(name, path string, is_dir, required, is_write bool) {
  214. res = append(res, fmt.Sprintf("%-25s '%s'", name+":", path))
  215. fi, err := os.Stat(path)
  216. if err != nil {
  217. if os.IsNotExist(err) && ctx.Bool("fix") && is_dir {
  218. if err := os.MkdirAll(path, 0777); err != nil {
  219. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  220. fail = true
  221. return
  222. }
  223. fi, err = os.Stat(path)
  224. }
  225. }
  226. if err != nil {
  227. if required {
  228. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  229. fail = true
  230. return
  231. }
  232. res = append(res, fmt.Sprintf(" NOTICE: not accessible (%v)", err))
  233. return
  234. }
  235. if is_dir && !fi.IsDir() {
  236. res = append(res, " ERROR: not a directory")
  237. fail = true
  238. return
  239. } else if !is_dir && !fi.Mode().IsRegular() {
  240. res = append(res, " ERROR: not a regular file")
  241. fail = true
  242. } else if is_write {
  243. if err := runDoctorWritableDir(path); err != nil {
  244. res = append(res, fmt.Sprintf(" ERROR: not writable: %v", err))
  245. fail = true
  246. }
  247. }
  248. }
  249. // Note print paths inside quotes to make any leading/trailing spaces evident
  250. check("Configuration File Path", setting.CustomConf, false, true, false)
  251. check("Repository Root Path", setting.RepoRootPath, true, true, true)
  252. check("Data Root Path", setting.AppDataPath, true, true, true)
  253. check("Custom File Root Path", setting.CustomPath, true, false, false)
  254. check("Work directory", setting.AppWorkPath, true, true, false)
  255. check("Log Root Path", setting.LogRootPath, true, true, true)
  256. if options.IsDynamic() {
  257. // Do not check/report on StaticRootPath if data is embedded in Gitea (-tags bindata)
  258. check("Static File Root Path", setting.StaticRootPath, true, true, false)
  259. }
  260. if fail {
  261. return res, fmt.Errorf("please check your configuration file and try again")
  262. }
  263. return res, nil
  264. }
  265. func runDoctorWritableDir(path string) error {
  266. // There's no platform-independent way of checking if a directory is writable
  267. // https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
  268. tmpFile, err := ioutil.TempFile(path, "doctors-order")
  269. if err != nil {
  270. return err
  271. }
  272. if err := os.Remove(tmpFile.Name()); err != nil {
  273. fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name())
  274. }
  275. tmpFile.Close()
  276. return nil
  277. }
  278. const tplCommentPrefix = `# gitea public key`
  279. func runDoctorAuthorizedKeys(ctx *cli.Context) ([]string, error) {
  280. if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
  281. return nil, nil
  282. }
  283. fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
  284. f, err := os.Open(fPath)
  285. if err != nil {
  286. if ctx.Bool("fix") {
  287. return []string{fmt.Sprintf("Error whilst opening authorized_keys: %v. Attempting regeneration", err)}, models.RewriteAllPublicKeys()
  288. }
  289. return nil, err
  290. }
  291. defer f.Close()
  292. linesInAuthorizedKeys := map[string]bool{}
  293. scanner := bufio.NewScanner(f)
  294. for scanner.Scan() {
  295. line := scanner.Text()
  296. if strings.HasPrefix(line, tplCommentPrefix) {
  297. continue
  298. }
  299. linesInAuthorizedKeys[line] = true
  300. }
  301. f.Close()
  302. // now we regenerate and check if there are any lines missing
  303. regenerated := &bytes.Buffer{}
  304. if err := models.RegeneratePublicKeys(regenerated); err != nil {
  305. return nil, err
  306. }
  307. scanner = bufio.NewScanner(regenerated)
  308. for scanner.Scan() {
  309. line := scanner.Text()
  310. if strings.HasPrefix(line, tplCommentPrefix) {
  311. continue
  312. }
  313. if ok := linesInAuthorizedKeys[line]; ok {
  314. continue
  315. }
  316. if ctx.Bool("fix") {
  317. return []string{"authorized_keys is out of date, attempting regeneration"}, models.RewriteAllPublicKeys()
  318. }
  319. return nil, fmt.Errorf("authorized_keys is out of date and should be regenerated with gitea admin regenerate keys")
  320. }
  321. return nil, nil
  322. }
  323. func runDoctorCheckDBVersion(ctx *cli.Context) ([]string, error) {
  324. if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
  325. if ctx.Bool("fix") {
  326. return []string{fmt.Sprintf("WARN: Got Error %v during ensure up to date", err), "Attempting to migrate to the latest DB version to fix this."}, models.NewEngine(context.Background(), migrations.Migrate)
  327. }
  328. return nil, err
  329. }
  330. return nil, nil
  331. }
  332. func iterateRepositories(each func(*models.Repository) ([]string, error)) ([]string, error) {
  333. results := []string{}
  334. err := models.Iterate(
  335. models.DefaultDBContext(),
  336. new(models.Repository),
  337. builder.Gt{"id": 0},
  338. func(idx int, bean interface{}) error {
  339. res, err := each(bean.(*models.Repository))
  340. results = append(results, res...)
  341. return err
  342. },
  343. )
  344. return results, err
  345. }
  346. func iteratePRs(repo *models.Repository, each func(*models.Repository, *models.PullRequest) ([]string, error)) ([]string, error) {
  347. results := []string{}
  348. err := models.Iterate(
  349. models.DefaultDBContext(),
  350. new(models.PullRequest),
  351. builder.Eq{"base_repo_id": repo.ID},
  352. func(idx int, bean interface{}) error {
  353. res, err := each(repo, bean.(*models.PullRequest))
  354. results = append(results, res...)
  355. return err
  356. },
  357. )
  358. return results, err
  359. }
  360. func runDoctorHooks(ctx *cli.Context) ([]string, error) {
  361. // Need to iterate across all of the repositories
  362. return iterateRepositories(func(repo *models.Repository) ([]string, error) {
  363. results, err := models.CheckDelegateHooks(repo.RepoPath())
  364. if err != nil {
  365. return nil, err
  366. }
  367. if len(results) > 0 && ctx.Bool("fix") {
  368. return []string{fmt.Sprintf("regenerated hooks for %s", repo.FullName())}, models.CreateDelegateHooks(repo.RepoPath())
  369. }
  370. return results, nil
  371. })
  372. }
  373. func runDoctorPRMergeBase(ctx *cli.Context) ([]string, error) {
  374. numRepos := 0
  375. numPRs := 0
  376. numPRsUpdated := 0
  377. results, err := iterateRepositories(func(repo *models.Repository) ([]string, error) {
  378. numRepos++
  379. return iteratePRs(repo, func(repo *models.Repository, pr *models.PullRequest) ([]string, error) {
  380. numPRs++
  381. results := []string{}
  382. pr.BaseRepo = repo
  383. repoPath := repo.RepoPath()
  384. oldMergeBase := pr.MergeBase
  385. if !pr.HasMerged {
  386. var err error
  387. pr.MergeBase, err = git.NewCommand("merge-base", "--", pr.BaseBranch, pr.GetGitRefName()).RunInDir(repoPath)
  388. if err != nil {
  389. var err2 error
  390. pr.MergeBase, err2 = git.NewCommand("rev-parse", git.BranchPrefix+pr.BaseBranch).RunInDir(repoPath)
  391. if err2 != nil {
  392. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  393. log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2)
  394. return results, nil
  395. }
  396. }
  397. } else {
  398. parentsString, err := git.NewCommand("rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunInDir(repoPath)
  399. if err != nil {
  400. results = append(results, fmt.Sprintf("WARN: Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  401. log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  402. return results, nil
  403. }
  404. parents := strings.Split(strings.TrimSpace(parentsString), " ")
  405. if len(parents) < 2 {
  406. return results, nil
  407. }
  408. args := append([]string{"merge-base", "--"}, parents[1:]...)
  409. args = append(args, pr.GetGitRefName())
  410. pr.MergeBase, err = git.NewCommand(args...).RunInDir(repoPath)
  411. if err != nil {
  412. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  413. log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  414. return results, nil
  415. }
  416. }
  417. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  418. if pr.MergeBase != oldMergeBase {
  419. if ctx.Bool("fix") {
  420. if err := pr.UpdateCols("merge_base"); err != nil {
  421. return results, err
  422. }
  423. } else {
  424. results = append(results, fmt.Sprintf("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase))
  425. }
  426. numPRsUpdated++
  427. }
  428. return results, nil
  429. })
  430. })
  431. if ctx.Bool("fix") {
  432. results = append(results, fmt.Sprintf("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  433. } else {
  434. if numPRsUpdated > 0 && err == nil {
  435. return results, fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
  436. }
  437. results = append(results, fmt.Sprintf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  438. }
  439. return results, err
  440. }
  441. func runDoctorScriptType(ctx *cli.Context) ([]string, error) {
  442. path, err := exec.LookPath(setting.ScriptType)
  443. if err != nil {
  444. return []string{fmt.Sprintf("ScriptType %s is not on the current PATH", setting.ScriptType)}, err
  445. }
  446. return []string{fmt.Sprintf("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)}, nil
  447. }