Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ type ServiceActionRequest struct {
Action string `json:"action"` // start, stop, restart, enable, disable
}

type UninstallServiceRequest struct {
ServiceName string `json:"service_name"`
RemoveVolumes bool `json:"remove_volumes"`
}

type SetupConfigureRequest struct {
Domain string `json:"domain"`
}
Expand Down Expand Up @@ -252,8 +257,12 @@ func main() {

// Service management endpoints
api.HandleFunc("/services/{name}/status", getServiceStatusHandler).Methods("GET")
api.HandleFunc("/services/{name}/logs", getServiceLogsHandler).Methods("GET")
api.HandleFunc("/services/{name}/stats", getServiceStatsHandler).Methods("GET")
api.HandleFunc("/services/{name}/config", getServiceConfigHandler).Methods("GET")
api.HandleFunc("/services/action", serviceActionHandler).Methods("POST")
api.HandleFunc("/services/install", installServiceHandler).Methods("POST")
api.HandleFunc("/services/uninstall", uninstallServiceHandler).Methods("POST")

// Package management endpoints
api.HandleFunc("/packages", listPackagesHandler).Methods("GET")
Expand Down Expand Up @@ -382,7 +391,7 @@ func serviceActionHandler(w http.ResponseWriter, r *http.Request) {
return
}

output, err := privOps.SystemctlCommand(operation, req.ServiceName)
output, err := privOps.ServiceCommand(operation, req.ServiceName)

response := ServiceStatusResponse{
ServiceName: req.ServiceName,
Expand All @@ -398,6 +407,79 @@ func serviceActionHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}

// Handler for service logs
func getServiceLogsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["name"]
tail := 200
if t := r.URL.Query().Get("tail"); t != "" {
if n, err := strconv.Atoi(t); err == nil && n > 0 {
tail = n
}
}
logs, err := privOps.GetServiceLogs(serviceName, tail)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"logs": logs})
}

// Handler for service stats (CPU, memory, uptime)
func getServiceStatsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["name"]
stats, err := privOps.GetServiceStats(serviceName)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stats)
}

// Handler for service config (domain, port from Caddy)
func getServiceConfigHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["name"]
config, err := privOps.GetServiceConfig(serviceName)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(config)
}

// Handler for service uninstall
func uninstallServiceHandler(w http.ResponseWriter, r *http.Request) {
var req UninstallServiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.ServiceName == "" {
http.Error(w, "service_name is required", http.StatusBadRequest)
return
}
if err := privOps.UninstallService(req.ServiceName, req.RemoveVolumes); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// Handler for listing installed packages
func listPackagesHandler(w http.ResponseWriter, r *http.Request) {
packages, err := privOps.ListInstalledPackages()
Expand Down Expand Up @@ -488,6 +570,9 @@ func installServiceHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
return
}
if err := SetInstalledStateByServiceName(req.Service, true); err != nil {
log.Printf("could not update installed state for service %q: %v", req.Service, err)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Expand Down
21 changes: 21 additions & 0 deletions api/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
)

func TestHealthHandler(t *testing.T) {
Expand Down Expand Up @@ -33,3 +35,22 @@ func TestCORSHeaders(t *testing.T) {
t.Errorf("expected status 200 for OPTIONS, got %d", resp.StatusCode)
}
}

// TestGetServiceConfigHandler_InvalidServiceName ensures invalid service name returns 400.
func TestGetServiceConfigHandler_InvalidServiceName(t *testing.T) {
// privOps must be set for the handler to run
privOps = NewPrivilegedOps()
defer func() { privOps = nil }()

r := mux.NewRouter()
r.HandleFunc("/api/services/{name}/config", getServiceConfigHandler).Methods("GET")

req := httptest.NewRequest("GET", "/api/services/invalid%20name/config", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("expected status 400 for invalid service name, got %d", resp.StatusCode)
}
}
Loading
Loading