Skip to content

Commit afe4787

Browse files
author
mirkobrombin
committed
feat: add API and Dashboard servers
1 parent f07e306 commit afe4787

35 files changed

+1344
-4
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.22
44

55
require (
66
github.com/armon/go-radix v1.0.0
7+
github.com/gorilla/mux v1.8.1
78
github.com/muesli/termenv v0.15.2
89
github.com/quic-go/quic-go v0.48.2
910
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE
3030
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
3131
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
3232
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
33+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
34+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
3335
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
3436
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3537
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=

internal/api/config.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
"github.com/mirkobrombin/goup/internal/config"
8+
)
9+
10+
func getConfigHandler(w http.ResponseWriter, r *http.Request) {
11+
if config.GlobalConf == nil {
12+
http.Error(w, "Global config not loaded", http.StatusInternalServerError)
13+
return
14+
}
15+
jsonResponse(w, config.GlobalConf)
16+
}
17+
18+
func updateConfigHandler(w http.ResponseWriter, r *http.Request) {
19+
if config.GlobalConf == nil {
20+
http.Error(w, "Global config not loaded", http.StatusInternalServerError)
21+
return
22+
}
23+
var newConf config.GlobalConfig
24+
if err := json.NewDecoder(r.Body).Decode(&newConf); err != nil {
25+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
26+
return
27+
}
28+
config.GlobalConf = &newConf
29+
if err := config.SaveGlobalConfig(); err != nil {
30+
http.Error(w, "Failed to save config", http.StatusInternalServerError)
31+
return
32+
}
33+
jsonResponse(w, config.GlobalConf)
34+
}

internal/api/logs_metrics_status.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package api
2+
3+
import (
4+
"io/fs"
5+
"io/ioutil"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/mirkobrombin/goup/internal/config"
11+
)
12+
13+
func getLogsHandler(w http.ResponseWriter, r *http.Request) {
14+
logFile := os.Getenv("GOUP_LOG_FILE")
15+
if logFile == "" {
16+
http.Error(w, "Log file not set", http.StatusNotFound)
17+
return
18+
}
19+
data, err := ioutil.ReadFile(logFile)
20+
if err != nil {
21+
http.Error(w, "Unable to read log file", http.StatusInternalServerError)
22+
return
23+
}
24+
w.Header().Set("Content-Type", "text/plain")
25+
w.Write(data)
26+
}
27+
28+
func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
29+
metrics := map[string]interface{}{
30+
"requests_total": 1234,
31+
"latency_avg_ms": 45.6,
32+
"cpu_usage": 23.4,
33+
"ram_usage_mb": 512,
34+
"active_sites": len(config.SiteConfigs),
35+
"active_plugins": len(config.GlobalConf.EnabledPlugins),
36+
}
37+
jsonResponse(w, metrics)
38+
}
39+
40+
func getStatusHandler(w http.ResponseWriter, r *http.Request) {
41+
status := map[string]interface{}{
42+
"uptime": "72h",
43+
"sites": len(config.SiteConfigs),
44+
"plugins": config.GlobalConf.EnabledPlugins,
45+
"apiAlive": true,
46+
}
47+
jsonResponse(w, status)
48+
}
49+
50+
func getLogWeightHandler(w http.ResponseWriter, r *http.Request) {
51+
logDir := config.GetLogDir()
52+
var totalSize int64 = 0
53+
err := filepath.Walk(logDir, func(_ string, info fs.FileInfo, err error) error {
54+
if err != nil {
55+
return err
56+
}
57+
if !info.IsDir() {
58+
totalSize += info.Size()
59+
}
60+
return nil
61+
})
62+
if err != nil {
63+
http.Error(w, "Error calculating log weight", http.StatusInternalServerError)
64+
return
65+
}
66+
jsonResponse(w, map[string]interface{}{
67+
"log_weight_bytes": totalSize,
68+
})
69+
}
70+
71+
func getPluginUsageHandler(w http.ResponseWriter, r *http.Request) {
72+
usage := make(map[string]int)
73+
for _, site := range config.SiteConfigs {
74+
for pluginName := range site.PluginConfigs {
75+
usage[pluginName]++
76+
}
77+
}
78+
jsonResponse(w, usage)
79+
}

internal/api/plugins.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gorilla/mux"
7+
"github.com/mirkobrombin/goup/internal/config"
8+
"github.com/mirkobrombin/goup/internal/plugin"
9+
"github.com/mirkobrombin/goup/internal/restart"
10+
)
11+
12+
type PluginResponse struct {
13+
Name string `json:"name"`
14+
Enabled bool `json:"enabled"`
15+
}
16+
17+
func getPluginsHandler(w http.ResponseWriter, r *http.Request) {
18+
pm := plugin.GetPluginManagerInstance()
19+
all := pm.GetRegisteredPlugins()
20+
var out []PluginResponse
21+
for _, name := range all {
22+
out = append(out, PluginResponse{
23+
Name: name,
24+
Enabled: isPluginEnabled(name),
25+
})
26+
}
27+
jsonResponse(w, out)
28+
}
29+
30+
func togglePluginHandler(w http.ResponseWriter, r *http.Request) {
31+
vars := mux.Vars(r)
32+
pName := vars["pluginName"]
33+
34+
if config.GlobalConf == nil {
35+
http.Error(w, "Global config not loaded", http.StatusInternalServerError)
36+
return
37+
}
38+
idx := -1
39+
for i, n := range config.GlobalConf.EnabledPlugins {
40+
if n == pName {
41+
idx = i
42+
break
43+
}
44+
}
45+
if idx >= 0 {
46+
config.GlobalConf.EnabledPlugins = append(
47+
config.GlobalConf.EnabledPlugins[:idx],
48+
config.GlobalConf.EnabledPlugins[idx+1:]...,
49+
)
50+
} else {
51+
config.GlobalConf.EnabledPlugins = append(config.GlobalConf.EnabledPlugins, pName)
52+
}
53+
if err := config.SaveGlobalConfig(); err != nil {
54+
http.Error(w, "Failed to save config", http.StatusInternalServerError)
55+
return
56+
}
57+
58+
jsonResponse(w, map[string]interface{}{
59+
"name": pName,
60+
"enabled": isPluginEnabled(pName),
61+
})
62+
63+
restart.ScheduleRestart(5)
64+
}
65+
66+
func isPluginEnabled(name string) bool {
67+
if config.GlobalConf == nil || len(config.GlobalConf.EnabledPlugins) == 0 {
68+
return true
69+
}
70+
for _, p := range config.GlobalConf.EnabledPlugins {
71+
if p == name {
72+
return true
73+
}
74+
}
75+
return false
76+
}

internal/api/restart.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/mirkobrombin/goup/internal/restart"
7+
)
8+
9+
func restartHandler(w http.ResponseWriter, r *http.Request) {
10+
restart.RestartHandler(w, r)
11+
}

internal/api/router.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gorilla/mux"
7+
)
8+
9+
// SetupRoutes returns a configured router.
10+
func SetupRoutes() *mux.Router {
11+
r := mux.NewRouter()
12+
13+
// Ping
14+
r.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
15+
jsonResponse(w, map[string]string{"message": "pong"})
16+
}).Methods("GET")
17+
18+
// Plugins
19+
r.HandleFunc("/api/plugins", getPluginsHandler).Methods("GET")
20+
r.HandleFunc("/api/plugins/{pluginName}/toggle", togglePluginHandler).Methods("POST")
21+
22+
// Global config
23+
r.HandleFunc("/api/config", getConfigHandler).Methods("GET")
24+
r.HandleFunc("/api/config", updateConfigHandler).Methods("PUT")
25+
26+
// Logs, metrics, status
27+
r.HandleFunc("/api/logs", getLogsHandler).Methods("GET")
28+
r.HandleFunc("/api/metrics", getMetricsHandler).Methods("GET")
29+
r.HandleFunc("/api/status", getStatusHandler).Methods("GET")
30+
r.HandleFunc("/api/logweight", getLogWeightHandler).Methods("GET")
31+
r.HandleFunc("/api/pluginusage", getPluginUsageHandler).Methods("GET")
32+
33+
// Restart
34+
r.HandleFunc("/api/restart", restartHandler).Methods("POST")
35+
36+
// Sites
37+
r.HandleFunc("/api/sites", listSitesHandler).Methods("GET")
38+
r.HandleFunc("/api/sites", createSiteHandler).Methods("POST")
39+
r.HandleFunc("/api/sites/{domain}", getSiteHandler).Methods("GET")
40+
r.HandleFunc("/api/sites/{domain}", updateSiteHandler).Methods("PUT")
41+
r.HandleFunc("/api/sites/{domain}", deleteSiteHandler).Methods("DELETE")
42+
r.HandleFunc("/api/sites/{domain}/validate", validateSiteHandler).Methods("GET")
43+
44+
return r
45+
}

internal/api/server.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/mirkobrombin/goup/internal/config"
8+
)
9+
10+
// StartAPIServer starts the GoUp API server.
11+
func StartAPIServer() {
12+
if config.GlobalConf == nil || !config.GlobalConf.EnableAPI {
13+
return
14+
}
15+
16+
router := SetupRoutes()
17+
port := config.GlobalConf.APIPort
18+
19+
go func() {
20+
fmt.Printf("[API] Listening on :%d\n", port)
21+
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), router); err != nil {
22+
fmt.Printf("[API] Error: %v\n", err)
23+
}
24+
}()
25+
}

0 commit comments

Comments
 (0)