feat: add WebRTC feature management commands and public API endpoints for enabling, disabling, and checking status

This commit is contained in:
anonpenguin23 2026-02-21 13:14:46 +02:00
parent e6f828d6f1
commit e4d51676cc
4 changed files with 207 additions and 12 deletions

View File

@ -45,10 +45,59 @@ var repairCmd = &cobra.Command{
},
}
var enableCmd = &cobra.Command{
Use: "enable <feature>",
Short: "Enable a feature for a namespace",
Long: "Enable a feature for a namespace. Supported features: webrtc",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ns, _ := cmd.Flags().GetString("namespace")
cliArgs := []string{"enable", args[0]}
if ns != "" {
cliArgs = append(cliArgs, "--namespace", ns)
}
cli.HandleNamespaceCommand(cliArgs)
},
}
var disableCmd = &cobra.Command{
Use: "disable <feature>",
Short: "Disable a feature for a namespace",
Long: "Disable a feature for a namespace. Supported features: webrtc",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ns, _ := cmd.Flags().GetString("namespace")
cliArgs := []string{"disable", args[0]}
if ns != "" {
cliArgs = append(cliArgs, "--namespace", ns)
}
cli.HandleNamespaceCommand(cliArgs)
},
}
var webrtcStatusCmd = &cobra.Command{
Use: "webrtc-status",
Short: "Show WebRTC service status for a namespace",
Run: func(cmd *cobra.Command, args []string) {
ns, _ := cmd.Flags().GetString("namespace")
cliArgs := []string{"webrtc-status"}
if ns != "" {
cliArgs = append(cliArgs, "--namespace", ns)
}
cli.HandleNamespaceCommand(cliArgs)
},
}
func init() {
deleteCmd.Flags().Bool("force", false, "Skip confirmation prompt")
enableCmd.Flags().String("namespace", "", "Namespace name")
disableCmd.Flags().String("namespace", "", "Namespace name")
webrtcStatusCmd.Flags().String("namespace", "", "Namespace name")
Cmd.AddCommand(listCmd)
Cmd.AddCommand(deleteCmd)
Cmd.AddCommand(repairCmd)
Cmd.AddCommand(enableCmd)
Cmd.AddCommand(disableCmd)
Cmd.AddCommand(webrtcStatusCmd)
}

View File

@ -241,21 +241,27 @@ func handleNamespaceEnable(args []string) {
os.Exit(1)
}
gatewayURL, apiKey := loadAuthForNamespace(ns)
fmt.Printf("Enabling WebRTC for namespace '%s'...\n", ns)
fmt.Printf("This will provision SFU (3 nodes) and TURN (2 nodes) services.\n")
url := fmt.Sprintf("http://localhost:%d/v1/internal/namespace/webrtc/enable?namespace=%s", constants.GatewayAPIPort, ns)
url := fmt.Sprintf("%s/v1/namespace/webrtc/enable", gatewayURL)
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
os.Exit(1)
}
req.Header.Set("X-Orama-Internal-Auth", "namespace-coordination")
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to local gateway (is the node running?): %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to connect to gateway: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
@ -294,20 +300,26 @@ func handleNamespaceDisable(args []string) {
os.Exit(1)
}
gatewayURL, apiKey := loadAuthForNamespace(ns)
fmt.Printf("Disabling WebRTC for namespace '%s'...\n", ns)
url := fmt.Sprintf("http://localhost:%d/v1/internal/namespace/webrtc/disable?namespace=%s", constants.GatewayAPIPort, ns)
url := fmt.Sprintf("%s/v1/namespace/webrtc/disable", gatewayURL)
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
os.Exit(1)
}
req.Header.Set("X-Orama-Internal-Auth", "namespace-coordination")
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to local gateway (is the node running?): %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to connect to gateway: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
@ -329,18 +341,24 @@ func handleNamespaceDisable(args []string) {
}
func handleNamespaceWebRTCStatus(ns string) {
url := fmt.Sprintf("http://localhost:%d/v1/internal/namespace/webrtc/status?namespace=%s", constants.GatewayAPIPort, ns)
gatewayURL, apiKey := loadAuthForNamespace(ns)
url := fmt.Sprintf("%s/v1/namespace/webrtc/status", gatewayURL)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create request: %v\n", err)
os.Exit(1)
}
req.Header.Set("X-Orama-Internal-Auth", "namespace-coordination")
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to local gateway (is the node running?): %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to connect to gateway: %v\n", err)
os.Exit(1)
}
defer resp.Body.Close()
@ -383,6 +401,26 @@ func handleNamespaceWebRTCStatus(ns string) {
}
}
// loadAuthForNamespace loads credentials and returns the gateway URL and API key.
// Exits with an error message if not authenticated.
func loadAuthForNamespace(ns string) (gatewayURL, apiKey string) {
store, err := auth.LoadEnhancedCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load credentials: %v\n", err)
os.Exit(1)
}
gatewayURL = getGatewayURL()
creds := store.GetDefaultCredential(gatewayURL)
if creds == nil || !creds.IsValid() {
fmt.Fprintf(os.Stderr, "Not authenticated. Run 'orama auth login' first.\n")
os.Exit(1)
}
return gatewayURL, creds.APIKey
}
func handleNamespaceList() {
// Load credentials
store, err := auth.LoadEnhancedCredentials()

View File

@ -871,6 +871,109 @@ func (g *Gateway) namespaceClusterRepairHandler(w http.ResponseWriter, r *http.R
})
}
// namespaceWebRTCEnablePublicHandler handles POST /v1/namespace/webrtc/enable
// Public: authenticated by JWT/API key via auth middleware. Namespace from context.
func (g *Gateway) namespaceWebRTCEnablePublicHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
namespaceName, _ := r.Context().Value(CtxKeyNamespaceOverride).(string)
if namespaceName == "" {
writeError(w, http.StatusForbidden, "namespace not resolved")
return
}
if g.webrtcManager == nil {
writeError(w, http.StatusServiceUnavailable, "WebRTC management not enabled")
return
}
if err := g.webrtcManager.EnableWebRTC(r.Context(), namespaceName, "api"); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"namespace": namespaceName,
"message": "WebRTC enabled successfully",
})
}
// namespaceWebRTCDisablePublicHandler handles POST /v1/namespace/webrtc/disable
// Public: authenticated by JWT/API key via auth middleware. Namespace from context.
func (g *Gateway) namespaceWebRTCDisablePublicHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
namespaceName, _ := r.Context().Value(CtxKeyNamespaceOverride).(string)
if namespaceName == "" {
writeError(w, http.StatusForbidden, "namespace not resolved")
return
}
if g.webrtcManager == nil {
writeError(w, http.StatusServiceUnavailable, "WebRTC management not enabled")
return
}
if err := g.webrtcManager.DisableWebRTC(r.Context(), namespaceName); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"namespace": namespaceName,
"message": "WebRTC disabled successfully",
})
}
// namespaceWebRTCStatusPublicHandler handles GET /v1/namespace/webrtc/status
// Public: authenticated by JWT/API key via auth middleware. Namespace from context.
func (g *Gateway) namespaceWebRTCStatusPublicHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
namespaceName, _ := r.Context().Value(CtxKeyNamespaceOverride).(string)
if namespaceName == "" {
writeError(w, http.StatusForbidden, "namespace not resolved")
return
}
if g.webrtcManager == nil {
writeError(w, http.StatusServiceUnavailable, "WebRTC management not enabled")
return
}
config, err := g.webrtcManager.GetWebRTCStatus(r.Context(), namespaceName)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if config == nil {
json.NewEncoder(w).Encode(map[string]interface{}{
"namespace": namespaceName,
"enabled": false,
})
} else {
json.NewEncoder(w).Encode(config)
}
}
// namespaceWebRTCEnableHandler handles POST /v1/internal/namespace/webrtc/enable?namespace={name}
// Internal-only: authenticated by X-Orama-Internal-Auth header + WireGuard subnet.
func (g *Gateway) namespaceWebRTCEnableHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -52,6 +52,11 @@ func (g *Gateway) Routes() http.Handler {
mux.HandleFunc("/v1/internal/namespace/webrtc/disable", g.namespaceWebRTCDisableHandler)
mux.HandleFunc("/v1/internal/namespace/webrtc/status", g.namespaceWebRTCStatusHandler)
// Namespace WebRTC enable/disable/status (public, JWT/API key auth via middleware)
mux.HandleFunc("/v1/namespace/webrtc/enable", g.namespaceWebRTCEnablePublicHandler)
mux.HandleFunc("/v1/namespace/webrtc/disable", g.namespaceWebRTCDisablePublicHandler)
mux.HandleFunc("/v1/namespace/webrtc/status", g.namespaceWebRTCStatusPublicHandler)
// auth endpoints
mux.HandleFunc("/v1/auth/jwks", g.authService.JWKSHandler)
mux.HandleFunc("/.well-known/jwks.json", g.authService.JWKSHandler)