mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-02-01 14:44:05 +00:00
feat: add vikunja doctor command for diagnostic checks (#2165)
Add a new `vikunja doctor` CLI command that performs diagnostic checks. Checks performed: - **System**: Version, Go version, OS/arch, running user, working directory - **Configuration**: Config file path, public URL, JWT secret, CORS origins - **Database**: Connection test, server version (SQLite/MySQL/PostgreSQL) - **Files**: Storage path, writability, disk space (Unix only) - **Optional services** (when enabled): - Redis: Connection ping - Typesense: Health endpoint - Mailer: SMTP connection - LDAP: Bind authentication test - OpenID Connect: Discovery endpoint for each configured provider
This commit is contained in:
2
go.mod
2
go.mod
@@ -33,6 +33,7 @@ require (
|
|||||||
github.com/d4l3k/messagediff v1.2.1
|
github.com/d4l3k/messagediff v1.2.1
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
|
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
|
||||||
|
github.com/fatih/color v1.18.0
|
||||||
github.com/fclairamb/afero-s3 v0.4.0
|
github.com/fclairamb/afero-s3 v0.4.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12
|
github.com/gabriel-vasile/mimetype v1.4.12
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0
|
github.com/ganigeorgiev/fexpr v0.5.0
|
||||||
@@ -121,7 +122,6 @@ require (
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
github.com/go-chi/chi/v5 v5.2.2 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -423,8 +423,6 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
|
|||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
|
||||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
|||||||
68
pkg/cmd/doctor.go
Normal file
68
pkg/cmd/doctor.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"code.vikunja.io/api/pkg/doctor"
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(doctorCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var doctorCmd = &cobra.Command{
|
||||||
|
Use: "doctor",
|
||||||
|
Short: "Run diagnostic checks on your Vikunja installation",
|
||||||
|
Long: `The doctor command runs a series of diagnostic checks to help troubleshoot
|
||||||
|
issues with your Vikunja installation. It checks:
|
||||||
|
|
||||||
|
- System information (version, user, working directory)
|
||||||
|
- Configuration (config file, public URL, JWT secret, CORS)
|
||||||
|
- Database connectivity and version
|
||||||
|
- File storage (local or S3)
|
||||||
|
- Optional services (Redis, Typesense, Mailer, LDAP, OpenID)
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - All checks passed
|
||||||
|
1 - One or more checks failed`,
|
||||||
|
PreRun: func(_ *cobra.Command, _ []string) {
|
||||||
|
// Minimal init - just config and logger
|
||||||
|
// Each check will initialize and test its own components
|
||||||
|
log.InitLogger()
|
||||||
|
config.InitConfig()
|
||||||
|
},
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
results := doctor.Run()
|
||||||
|
|
||||||
|
doctor.PrintResults(os.Stdout, results)
|
||||||
|
|
||||||
|
failed := doctor.CountFailed(results)
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf("%d check(s) failed\n", failed)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("All checks passed")
|
||||||
|
},
|
||||||
|
}
|
||||||
120
pkg/doctor/config.go
Normal file
120
pkg/doctor/config.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckConfig returns configuration checks.
|
||||||
|
func CheckConfig() CheckGroup {
|
||||||
|
results := []CheckResult{
|
||||||
|
checkConfigFile(),
|
||||||
|
checkPublicURL(),
|
||||||
|
checkJWTSecret(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show CORS details if CORS is enabled
|
||||||
|
if config.CorsEnable.GetBool() {
|
||||||
|
results = append(results, checkCORS())
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Configuration",
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigFile() CheckResult {
|
||||||
|
configFile := viper.ConfigFileUsed()
|
||||||
|
if configFile == "" {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Config file",
|
||||||
|
Passed: true,
|
||||||
|
Value: "none (using defaults/environment)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Config file",
|
||||||
|
Passed: true,
|
||||||
|
Value: configFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPublicURL() CheckResult {
|
||||||
|
publicURL := config.ServicePublicURL.GetString()
|
||||||
|
if publicURL == "" {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Public URL",
|
||||||
|
Passed: false,
|
||||||
|
Error: "not configured (required for many features)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Public URL",
|
||||||
|
Passed: true,
|
||||||
|
Value: publicURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkJWTSecret() CheckResult {
|
||||||
|
// We can't check the actual value, but we can check if it's the default length
|
||||||
|
// which would indicate it was auto-generated
|
||||||
|
secret := config.ServiceJWTSecret.GetString()
|
||||||
|
|
||||||
|
// Auto-generated secrets are 64 hex characters (32 bytes)
|
||||||
|
if len(secret) == 64 {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "JWT secret",
|
||||||
|
Passed: true,
|
||||||
|
Value: "configured (auto-generated)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "JWT secret",
|
||||||
|
Passed: true,
|
||||||
|
Value: "configured",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCORS() CheckResult {
|
||||||
|
origins := config.CorsOrigins.GetStringSlice()
|
||||||
|
|
||||||
|
result := CheckResult{
|
||||||
|
Name: "CORS origins",
|
||||||
|
Passed: true,
|
||||||
|
Value: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(origins) == 0 {
|
||||||
|
result.Value = "none configured"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show first origin in the value, rest as additional lines
|
||||||
|
result.Value = origins[0]
|
||||||
|
if len(origins) > 1 {
|
||||||
|
result.Lines = append(result.Lines, origins[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
124
pkg/doctor/database.go
Normal file
124
pkg/doctor/database.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"code.vikunja.io/api/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckDatabase returns database connectivity checks.
|
||||||
|
func CheckDatabase() CheckGroup {
|
||||||
|
dbType := config.DatabaseType.GetString()
|
||||||
|
|
||||||
|
// Initialize database engine
|
||||||
|
_, err := db.CreateDBEngine()
|
||||||
|
if err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: fmt.Sprintf("Database (%s)", dbType),
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []CheckResult{
|
||||||
|
checkDatabaseConnection(),
|
||||||
|
checkDatabaseVersion(dbType),
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: fmt.Sprintf("Database (%s)", dbType),
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDatabaseConnection() CheckResult {
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if err := s.Ping(); err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: true,
|
||||||
|
Value: "OK",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDatabaseVersion(dbType string) CheckResult {
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
var versionQuery string
|
||||||
|
switch dbType {
|
||||||
|
case "sqlite":
|
||||||
|
versionQuery = "SELECT sqlite_version()"
|
||||||
|
case "mysql":
|
||||||
|
versionQuery = "SELECT version()"
|
||||||
|
case "postgres":
|
||||||
|
versionQuery = "SELECT version()"
|
||||||
|
default:
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Server version",
|
||||||
|
Passed: false,
|
||||||
|
Error: fmt.Sprintf("unknown database type: %s", dbType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.QueryString(versionQuery)
|
||||||
|
if err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Server version",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 || len(results[0]) == 0 {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Server version",
|
||||||
|
Passed: false,
|
||||||
|
Error: "could not retrieve version",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first value from the result map
|
||||||
|
var version string
|
||||||
|
for _, v := range results[0] {
|
||||||
|
version = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Server version",
|
||||||
|
Passed: true,
|
||||||
|
Value: version,
|
||||||
|
}
|
||||||
|
}
|
||||||
33
pkg/doctor/doctor.go
Normal file
33
pkg/doctor/doctor.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Package doctor provides diagnostic checks for Vikunja installations.
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
// Run executes all diagnostic checks and returns the results.
|
||||||
|
func Run() []CheckGroup {
|
||||||
|
groups := []CheckGroup{
|
||||||
|
CheckSystem(),
|
||||||
|
CheckConfig(),
|
||||||
|
CheckDatabase(),
|
||||||
|
CheckFiles(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add optional service checks
|
||||||
|
groups = append(groups, CheckOptionalServices()...)
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
132
pkg/doctor/files.go
Normal file
132
pkg/doctor/files.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"code.vikunja.io/api/pkg/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckFiles returns file storage checks.
|
||||||
|
func CheckFiles() CheckGroup {
|
||||||
|
fileType := config.FilesType.GetString()
|
||||||
|
|
||||||
|
// Initialize file handler
|
||||||
|
if err := files.InitFileHandler(); err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: fmt.Sprintf("Files (%s)", fileType),
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Initialization",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []CheckResult
|
||||||
|
|
||||||
|
switch fileType {
|
||||||
|
case "local":
|
||||||
|
results = checkLocalStorage()
|
||||||
|
case "s3":
|
||||||
|
results = checkS3Storage()
|
||||||
|
default:
|
||||||
|
results = []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Type",
|
||||||
|
Passed: false,
|
||||||
|
Error: fmt.Sprintf("unknown storage type: %s", fileType),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: fmt.Sprintf("Files (%s)", fileType),
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLocalStorage() []CheckResult {
|
||||||
|
basePath := config.FilesBasePath.GetString()
|
||||||
|
|
||||||
|
results := []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Path",
|
||||||
|
Passed: true,
|
||||||
|
Value: basePath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check writable using the existing ValidateFileStorage function
|
||||||
|
if err := files.ValidateFileStorage(); err != nil {
|
||||||
|
results = append(results, CheckResult{
|
||||||
|
Name: "Writable",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results = append(results, CheckResult{
|
||||||
|
Name: "Writable",
|
||||||
|
Passed: true,
|
||||||
|
Value: "yes",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check disk space (platform-specific)
|
||||||
|
results = append(results, checkDiskSpace(basePath))
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkS3Storage() []CheckResult {
|
||||||
|
endpoint := config.FilesS3Endpoint.GetString()
|
||||||
|
bucket := config.FilesS3Bucket.GetString()
|
||||||
|
|
||||||
|
results := []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Endpoint",
|
||||||
|
Passed: true,
|
||||||
|
Value: endpoint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Bucket",
|
||||||
|
Passed: true,
|
||||||
|
Value: bucket,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check writable using the existing ValidateFileStorage function
|
||||||
|
if err := files.ValidateFileStorage(); err != nil {
|
||||||
|
results = append(results, CheckResult{
|
||||||
|
Name: "Writable",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results = append(results, CheckResult{
|
||||||
|
Name: "Writable",
|
||||||
|
Passed: true,
|
||||||
|
Value: "yes",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
46
pkg/doctor/files_unix.go
Normal file
46
pkg/doctor/files_unix.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkDiskSpace(path string) CheckResult {
|
||||||
|
var stat unix.Statfs_t
|
||||||
|
if err := unix.Statfs(path, &stat); err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Disk space",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available space in bytes
|
||||||
|
availableBytes := stat.Bavail * uint64(stat.Bsize)
|
||||||
|
availableGB := float64(availableBytes) / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Disk space",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("%.1f GB available", availableGB),
|
||||||
|
}
|
||||||
|
}
|
||||||
27
pkg/doctor/files_windows.go
Normal file
27
pkg/doctor/files_windows.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
func checkDiskSpace(_ string) CheckResult {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Disk space",
|
||||||
|
Passed: true,
|
||||||
|
Value: "check not available on Windows",
|
||||||
|
}
|
||||||
|
}
|
||||||
75
pkg/doctor/output.go
Normal file
75
pkg/doctor/output.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
greenCheck = color.New(color.FgGreen).SprintFunc()
|
||||||
|
redCross = color.New(color.FgRed).SprintFunc()
|
||||||
|
bold = color.New(color.Bold).SprintFunc()
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintResults writes all check groups to the given writer with colored output.
|
||||||
|
func PrintResults(w io.Writer, groups []CheckGroup) {
|
||||||
|
fmt.Fprintln(w, bold("Vikunja Doctor"))
|
||||||
|
fmt.Fprintln(w, "==============")
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
fmt.Fprintln(w, bold(group.Name))
|
||||||
|
for _, result := range group.Results {
|
||||||
|
printResult(w, result)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResult(w io.Writer, result CheckResult) {
|
||||||
|
marker := greenCheck("✓")
|
||||||
|
value := result.Value
|
||||||
|
if !result.Passed {
|
||||||
|
marker = redCross("✗")
|
||||||
|
if result.Error != "" {
|
||||||
|
value = result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, " %s %s: %s\n", marker, result.Name, value)
|
||||||
|
|
||||||
|
for _, line := range result.Lines {
|
||||||
|
fmt.Fprintf(w, " %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountFailed returns the number of failed checks across all groups.
|
||||||
|
func CountFailed(groups []CheckGroup) int {
|
||||||
|
count := 0
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, result := range group.Results {
|
||||||
|
if !result.Passed {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
406
pkg/doctor/services.go
Normal file
406
pkg/doctor/services.go
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"code.vikunja.io/api/pkg/modules/auth/ldap"
|
||||||
|
"code.vikunja.io/api/pkg/red"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckOptionalServices returns check groups for all enabled optional services.
|
||||||
|
func CheckOptionalServices() []CheckGroup {
|
||||||
|
var groups []CheckGroup
|
||||||
|
|
||||||
|
if config.RedisEnabled.GetBool() {
|
||||||
|
groups = append(groups, checkRedis())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.TypesenseEnabled.GetBool() {
|
||||||
|
groups = append(groups, checkTypesense())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MailerEnabled.GetBool() {
|
||||||
|
groups = append(groups, checkMailer())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AuthLdapEnabled.GetBool() {
|
||||||
|
groups = append(groups, checkLDAP())
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AuthOpenIDEnabled.GetBool() {
|
||||||
|
groups = append(groups, checkOpenID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRedis() CheckGroup {
|
||||||
|
// Initialize Redis
|
||||||
|
red.InitRedis()
|
||||||
|
|
||||||
|
r := red.GetRedis()
|
||||||
|
if r == nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Redis",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: "Redis client not initialized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := r.Ping(ctx).Err(); err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Redis",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Redis",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("OK (%s)", config.RedisHost.GetString()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTypesense() CheckGroup {
|
||||||
|
url := config.TypesenseURL.GetString()
|
||||||
|
healthURL := url + "/health"
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Typesense",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-TYPESENSE-API-KEY", config.TypesenseAPIKey.GetString())
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Typesense",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Typesense",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: fmt.Sprintf("health check returned status %d", resp.StatusCode),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Typesense",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("OK (%s)", url),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMailer() CheckGroup {
|
||||||
|
host := config.MailerHost.GetString()
|
||||||
|
port := config.MailerPort.GetInt()
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Mailer",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "SMTP connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: "mailer host not configured",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||||
|
|
||||||
|
// Simple TCP dial test with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Mailer",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "SMTP connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "Mailer",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "SMTP connection",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("OK (%s)", address),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLDAP() CheckGroup {
|
||||||
|
host := config.AuthLdapHost.GetString()
|
||||||
|
port := config.AuthLdapPort.GetInt()
|
||||||
|
useTLS := config.AuthLdapUseTLS.GetBool()
|
||||||
|
|
||||||
|
address := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||||
|
|
||||||
|
protocol := "ldap"
|
||||||
|
if useTLS {
|
||||||
|
protocol = "ldaps"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the actual LDAP connection function which tests bind credentials
|
||||||
|
l, err := ldap.ConnectAndBindToLDAPDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "LDAP",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "LDAP",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Connection",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("OK (%s://%s)", protocol, address),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpenID() CheckGroup {
|
||||||
|
// Parse raw config to get all providers (including ones that fail to connect)
|
||||||
|
rawProviders := config.AuthOpenIDProviders.Get()
|
||||||
|
if rawProviders == nil {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "OpenID Connect",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Providers",
|
||||||
|
Passed: true,
|
||||||
|
Value: "none configured",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to map[string]interface{}
|
||||||
|
var providerMap map[string]interface{}
|
||||||
|
switch p := rawProviders.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
providerMap = p
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
providerMap = make(map[string]interface{}, len(p))
|
||||||
|
for k, v := range p {
|
||||||
|
if key, ok := k.(string); ok {
|
||||||
|
providerMap[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "OpenID Connect",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Configuration",
|
||||||
|
Passed: false,
|
||||||
|
Error: "invalid provider configuration format",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(providerMap) == 0 {
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "OpenID Connect",
|
||||||
|
Results: []CheckResult{
|
||||||
|
{
|
||||||
|
Name: "Providers",
|
||||||
|
Passed: true,
|
||||||
|
Value: "none configured",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []CheckResult
|
||||||
|
for key, p := range providerMap {
|
||||||
|
result := checkOpenIDProvider(key, p)
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "OpenID Connect",
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpenIDProvider(key string, rawProvider interface{}) CheckResult {
|
||||||
|
// Extract provider config
|
||||||
|
var pi map[string]interface{}
|
||||||
|
switch p := rawProvider.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
pi = p
|
||||||
|
case map[interface{}]interface{}:
|
||||||
|
pi = make(map[string]interface{}, len(p))
|
||||||
|
for k, v := range p {
|
||||||
|
if kStr, ok := k.(string); ok {
|
||||||
|
pi[kStr] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", key),
|
||||||
|
Passed: false,
|
||||||
|
Error: "invalid configuration format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider name
|
||||||
|
name := key
|
||||||
|
if n, ok := pi["name"].(string); ok {
|
||||||
|
name = n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get auth URL
|
||||||
|
authURL, ok := pi["authurl"].(string)
|
||||||
|
if !ok || authURL == "" {
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", name),
|
||||||
|
Passed: false,
|
||||||
|
Error: "authurl not configured",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the provider's discovery endpoint is reachable
|
||||||
|
// OpenID Connect discovery is at /.well-known/openid-configuration
|
||||||
|
discoveryURL := authURL
|
||||||
|
if authURL[len(authURL)-1] != '/' {
|
||||||
|
discoveryURL += "/"
|
||||||
|
}
|
||||||
|
discoveryURL += ".well-known/openid-configuration"
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", name),
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", name),
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", name),
|
||||||
|
Passed: false,
|
||||||
|
Error: fmt.Sprintf("discovery endpoint returned status %d", resp.StatusCode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: fmt.Sprintf("Provider: %s", name),
|
||||||
|
Passed: true,
|
||||||
|
Value: "OK",
|
||||||
|
}
|
||||||
|
}
|
||||||
82
pkg/doctor/system.go
Normal file
82
pkg/doctor/system.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckSystem returns system information checks.
|
||||||
|
func CheckSystem() CheckGroup {
|
||||||
|
results := []CheckResult{
|
||||||
|
checkVersion(),
|
||||||
|
checkGoVersion(),
|
||||||
|
checkOS(),
|
||||||
|
checkUser(),
|
||||||
|
checkWorkingDirectory(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckGroup{
|
||||||
|
Name: "System",
|
||||||
|
Results: results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVersion() CheckResult {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Version",
|
||||||
|
Passed: true,
|
||||||
|
Value: version.Version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGoVersion() CheckResult {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Go",
|
||||||
|
Passed: true,
|
||||||
|
Value: runtime.Version(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOS() CheckResult {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "OS",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkWorkingDirectory() CheckResult {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Working directory",
|
||||||
|
Passed: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "Working directory",
|
||||||
|
Passed: true,
|
||||||
|
Value: wd,
|
||||||
|
}
|
||||||
|
}
|
||||||
40
pkg/doctor/system_unix.go
Normal file
40
pkg/doctor/system_unix.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
osuser "os/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkUser() CheckResult {
|
||||||
|
uid := os.Getuid()
|
||||||
|
username := "unknown"
|
||||||
|
|
||||||
|
if u, err := osuser.Current(); err == nil {
|
||||||
|
username = u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "User",
|
||||||
|
Passed: true,
|
||||||
|
Value: fmt.Sprintf("%s (uid=%d)", username, uid),
|
||||||
|
}
|
||||||
|
}
|
||||||
37
pkg/doctor/system_windows.go
Normal file
37
pkg/doctor/system_windows.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
osuser "os/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkUser() CheckResult {
|
||||||
|
username := "unknown"
|
||||||
|
|
||||||
|
if u, err := osuser.Current(); err == nil {
|
||||||
|
username = u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: "User",
|
||||||
|
Passed: true,
|
||||||
|
Value: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
32
pkg/doctor/types.go
Normal file
32
pkg/doctor/types.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
// CheckResult represents the result of a single diagnostic check.
|
||||||
|
type CheckResult struct {
|
||||||
|
Name string
|
||||||
|
Passed bool
|
||||||
|
Value string // e.g., "vikunja (uid=1000)" or "OK"
|
||||||
|
Error string // only populated if Passed is false
|
||||||
|
Lines []string // additional lines to display (e.g., list of CORS origins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckGroup represents a category of checks with a header.
|
||||||
|
type CheckGroup struct {
|
||||||
|
Name string // e.g., "Database (sqlite)"
|
||||||
|
Results []CheckResult
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user