Skip to content
Closed
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
17 changes: 10 additions & 7 deletions third_party/agfs/agfs-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ The server is configured using a YAML file (default: `config.yaml`).
server:
address: ":8080"
log_level: info # debug, info, warn, error
management_api_enabled: false # Enable only on trusted/admin-only networks

# External plugins configuration
external_plugins:
Expand Down Expand Up @@ -191,6 +192,8 @@ AGFS Server comes with a rich set of built-in plugins.

You can mount, unmount, and manage plugins at runtime using the API.

This management surface is disabled by default. To enable it, set `server.management_api_enabled: true` in `config.yaml` and only expose the server on a trusted admin network.

**Mount a plugin**:
```bash
curl -X POST http://localhost:8080/api/v1/mount \
Expand Down Expand Up @@ -245,12 +248,12 @@ All API endpoints are prefixed with `/api/v1/`.
| | `GET` | `/stat` | Get file metadata |
| **Directories** | `GET` | `/directories` | List directory contents |
| | `POST` | `/directories` | Create directory |
| **Management** | `GET` | `/mounts` | List active mounts |
| | `POST` | `/mount` | Mount a plugin |
| | `POST` | `/unmount` | Unmount a plugin |
| | `GET` | `/plugins` | List loaded external plugins |
| | `POST` | `/plugins/load` | Load an external plugin |
| | `POST` | `/plugins/unload` | Unload an external plugin |
| **Management** | `GET` | `/mounts` | List active mounts, when `server.management_api_enabled=true` |
| | `POST` | `/mount` | Mount a plugin, when `server.management_api_enabled=true` |
| | `POST` | `/unmount` | Unmount a plugin, when `server.management_api_enabled=true` |
| | `GET` | `/plugins` | List loaded external plugins, when `server.management_api_enabled=true` |
| | `POST` | `/plugins/load` | Load an external plugin, when `server.management_api_enabled=true` |
| | `POST` | `/plugins/unload` | Unload an external plugin, when `server.management_api_enabled=true` |
| **System** | `GET` | `/health` | Server health check |

## Development
Expand All @@ -267,4 +270,4 @@ All API endpoints are prefixed with `/api/v1/`.

## License

Apache License 2.0
Apache License 2.0
10 changes: 10 additions & 0 deletions third_party/agfs/agfs-server/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ curl -X POST "http://localhost:8080/api/v1/chmod?path=/memfs/data.txt" \
### List Mounts
List all currently mounted plugins.

This endpoint is available only when `server.management_api_enabled=true`.

**Endpoint:** `GET /api/v1/mounts`

**Response:**
Expand All @@ -346,6 +348,8 @@ curl "http://localhost:8080/api/v1/mounts"
### Mount Plugin
Mount a new plugin instance.

This endpoint is available only when `server.management_api_enabled=true`.

**Endpoint:** `POST /api/v1/mount`

**Body:**
Expand Down Expand Up @@ -388,6 +392,8 @@ curl -X POST "http://localhost:8080/api/v1/unmount" \
### List Plugins
List all available (loaded) plugins, including external ones.

This endpoint is available only when `server.management_api_enabled=true`.

**Endpoint:** `GET /api/v1/plugins`

**Response:**
Expand Down Expand Up @@ -416,6 +422,8 @@ curl "http://localhost:8080/api/v1/plugins"
### Load External Plugin
Load a dynamic library plugin (.so/.dylib/.dll) or WASM plugin.

This endpoint is available only when `server.management_api_enabled=true`.

**Endpoint:** `POST /api/v1/plugins/load`

**Body:**
Expand All @@ -436,6 +444,8 @@ curl -X POST "http://localhost:8080/api/v1/plugins/load" \
### Unload External Plugin
Unload a previously loaded external plugin.

This endpoint is available only when `server.management_api_enabled=true`.

**Endpoint:** `POST /api/v1/plugins/unload`

**Body:**
Expand Down
9 changes: 8 additions & 1 deletion third_party/agfs/agfs-server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const sampleConfig = `# AGFS Server Configuration File
server:
address: ":8080" # Server listen address
log_level: "info" # Log level: debug, info, warn, error
management_api_enabled: false # Set true to allow runtime mount/plugin changes over HTTP

# Plugin configurations
plugins:
Expand Down Expand Up @@ -360,7 +361,13 @@ func main() {
// Create handlers
handler := handlers.NewHandler(mfs, trafficMonitor)
handler.SetVersionInfo(Version, GitCommit, BuildTime)
pluginHandler := handlers.NewPluginHandler(mfs)
pluginHandler := handlers.NewPluginHandler(mfs, cfg.Server.ManagementAPIEnabled)

if cfg.Server.ManagementAPIEnabled {
log.Warn("AGFS management API enabled: runtime mount and external plugin changes are available over HTTP")
} else {
log.Info("AGFS management API disabled: runtime mount and external plugin endpoints will return 403")
}

// Setup routes
mux := http.NewServeMux()
Expand Down
1 change: 1 addition & 0 deletions third_party/agfs/agfs-server/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
server:
address: ":8080"
log_level: info # Options: debug, info, warn, error
management_api_enabled: false # Set true only on trusted/admin-only networks

plugins:
serverinfofs:
Expand Down
1 change: 1 addition & 0 deletions third_party/agfs/agfs-server/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
server:
address: ":8080"
log_level: info # Options: debug, info, warn, error
management_api_enabled: false # Set true only on trusted/admin-only networks

plugins:
serverinfofs:
Expand Down
5 changes: 3 additions & 2 deletions third_party/agfs/agfs-server/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type Config struct {

// ServerConfig contains server-level configuration
type ServerConfig struct {
Address string `yaml:"address"`
LogLevel string `yaml:"log_level"`
Address string `yaml:"address"`
LogLevel string `yaml:"log_level"`
ManagementAPIEnabled bool `yaml:"management_api_enabled"`
}

// ExternalPluginsConfig contains configuration for external plugins
Expand Down
47 changes: 44 additions & 3 deletions third_party/agfs/agfs-server/pkg/handlers/plugin_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,29 @@ import (

// PluginHandler handles plugin management operations
type PluginHandler struct {
mfs *mountablefs.MountableFS
mfs *mountablefs.MountableFS
managementAPIEnabled bool
}

// NewPluginHandler creates a new plugin handler
func NewPluginHandler(mfs *mountablefs.MountableFS) *PluginHandler {
return &PluginHandler{mfs: mfs}
func NewPluginHandler(mfs *mountablefs.MountableFS, managementAPIEnabled bool) *PluginHandler {
return &PluginHandler{
mfs: mfs,
managementAPIEnabled: managementAPIEnabled,
}
}

func (ph *PluginHandler) requireManagementAPI(w http.ResponseWriter) bool {
if ph.managementAPIEnabled {
return true
}

writeError(
w,
http.StatusForbidden,
"management API is disabled; set server.management_api_enabled=true to allow runtime mount and plugin changes",
)
return false
}

// MountInfo represents information about a mounted plugin
Expand All @@ -42,6 +59,10 @@ type ListMountsResponse struct {

// ListMounts handles GET /mounts
func (ph *PluginHandler) ListMounts(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

mounts := ph.mfs.GetMounts()

var mountInfos []MountInfo
Expand All @@ -63,6 +84,10 @@ type UnmountRequest struct {

// Unmount handles POST /unmount
func (ph *PluginHandler) Unmount(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

var req UnmountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
Expand Down Expand Up @@ -91,6 +116,10 @@ type MountRequest struct {

// Mount handles POST /mount
func (ph *PluginHandler) Mount(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

var req MountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
Expand Down Expand Up @@ -248,6 +277,10 @@ func (ph *PluginHandler) readPluginFromAGFS(agfsPath string) (string, error) {

// LoadPlugin handles POST /plugins/load
func (ph *PluginHandler) LoadPlugin(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

var req LoadPluginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
Expand Down Expand Up @@ -316,6 +349,10 @@ type UnloadPluginRequest struct {

// UnloadPlugin handles POST /plugins/unload
func (ph *PluginHandler) UnloadPlugin(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

var req UnloadPluginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
Expand Down Expand Up @@ -357,6 +394,10 @@ type ListPluginsResponse struct {

// ListPlugins handles GET /plugins
func (ph *PluginHandler) ListPlugins(w http.ResponseWriter, r *http.Request) {
if !ph.requireManagementAPI(w) {
return
}

// Get all mounts
mounts := ph.mfs.GetMounts()

Expand Down
77 changes: 77 additions & 0 deletions third_party/agfs/agfs-server/pkg/handlers/plugin_handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package handlers

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/c4pt0r/agfs/agfs-server/pkg/mountablefs"
"github.com/c4pt0r/agfs/agfs-server/pkg/plugin/api"
)

func TestManagementEndpointsDisabledByDefault(t *testing.T) {
mfs := mountablefs.NewMountableFS(api.PoolConfig{})
handler := NewPluginHandler(mfs, false)
mux := http.NewServeMux()
handler.SetupRoutes(mux)

tests := []struct {
name string
method string
target string
}{
{name: "list mounts", method: http.MethodGet, target: "/api/v1/mounts"},
{name: "mount plugin", method: http.MethodPost, target: "/api/v1/mount"},
{name: "unmount plugin", method: http.MethodPost, target: "/api/v1/unmount"},
{name: "list plugins", method: http.MethodGet, target: "/api/v1/plugins"},
{name: "load plugin", method: http.MethodPost, target: "/api/v1/plugins/load"},
{name: "unload plugin", method: http.MethodPost, target: "/api/v1/plugins/unload"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.target, nil)
rec := httptest.NewRecorder()

mux.ServeHTTP(rec, req)

if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
})
}
}

func TestManagementEndpointsEnabledPreserveExistingValidation(t *testing.T) {
mfs := mountablefs.NewMountableFS(api.PoolConfig{})
handler := NewPluginHandler(mfs, true)
mux := http.NewServeMux()
handler.SetupRoutes(mux)

tests := []struct {
name string
method string
target string
wantStatus int
}{
{name: "list mounts", method: http.MethodGet, target: "/api/v1/mounts", wantStatus: http.StatusOK},
{name: "mount plugin", method: http.MethodPost, target: "/api/v1/mount", wantStatus: http.StatusBadRequest},
{name: "unmount plugin", method: http.MethodPost, target: "/api/v1/unmount", wantStatus: http.StatusBadRequest},
{name: "list plugins", method: http.MethodGet, target: "/api/v1/plugins", wantStatus: http.StatusOK},
{name: "load plugin", method: http.MethodPost, target: "/api/v1/plugins/load", wantStatus: http.StatusBadRequest},
{name: "unload plugin", method: http.MethodPost, target: "/api/v1/plugins/unload", wantStatus: http.StatusBadRequest},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.target, nil)
rec := httptest.NewRecorder()

mux.ServeHTTP(rec, req)

if rec.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d", rec.Code, tt.wantStatus)
}
})
}
}
Loading