diff --git a/README.md b/README.md index e1d6e53..bbd63c3 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,17 @@ server.Set("maintenance_mode", "true") server.Commit() ``` +### Calling API Functions + +```go +// Call a remote API function by group and function name +result, err := adminapi.CallAPI("ip", "get_free", map[string]any{"network": "internal"}) +if err != nil { + panic(err) +} +fmt.Printf("Free IP: %s\n", result) +``` + ## Building ```bash diff --git a/adminapi/call.go b/adminapi/call.go new file mode 100644 index 0000000..4aea539 --- /dev/null +++ b/adminapi/call.go @@ -0,0 +1,49 @@ +package adminapi + +import ( + "encoding/json" + "fmt" +) + +const apiEndpointCall = "/call" + +type callRequest struct { + Group string `json:"group"` + Name string `json:"name"` + Args []any `json:"args"` + Kwargs map[string]any `json:"kwargs"` +} + +type callResponse struct { + Status string `json:"status"` + RetVal any `json:"retval"` + Message string `json:"message"` +} + +// CallAPI calls a remote API function on the Serveradmin server. +// It takes a function group, function name, and keyword arguments as a map. +func CallAPI(group, function string, args map[string]any) (any, error) { + req := callRequest{ + Group: group, + Name: function, + Args: []any{}, + Kwargs: args, + } + + resp, err := sendRequest(apiEndpointCall, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result callResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode call response: %w", err) + } + + if result.Status == "error" { + return nil, fmt.Errorf("API call %s.%s failed: %s", group, function, result.Message) + } + + return result.RetVal, nil +} diff --git a/adminapi/call_test.go b/adminapi/call_test.go new file mode 100644 index 0000000..528b069 --- /dev/null +++ b/adminapi/call_test.go @@ -0,0 +1,101 @@ +package adminapi + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCallAPISuccess(t *testing.T) { + var receivedBody callRequest + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &receivedBody) + + w.WriteHeader(200) + w.Write([]byte(`{"status": "success", "retval": "10.0.0.1"}`)) + })) + defer server.Close() + + resetConfig() + t.Setenv("SERVERADMIN_TOKEN", "testtoken") + t.Setenv("SERVERADMIN_BASE_URL", server.URL) + + result, err := CallAPI("ip", "get_free", map[string]any{"network": "internal"}) + require.NoError(t, err) + assert.Equal(t, "10.0.0.1", result) + + // Verify request structure + assert.Equal(t, "ip", receivedBody.Group) + assert.Equal(t, "get_free", receivedBody.Name) + assert.Empty(t, receivedBody.Args) + assert.Equal(t, map[string]any{"network": "internal"}, receivedBody.Kwargs) +} + +func TestCallAPIError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(200) + w.Write([]byte(`{"status": "error", "message": "function not found"}`)) + })) + defer server.Close() + + resetConfig() + t.Setenv("SERVERADMIN_TOKEN", "testtoken") + t.Setenv("SERVERADMIN_BASE_URL", server.URL) + + result, err := CallAPI("ip", "nonexistent", map[string]any{}) + assert.Nil(t, result) + require.Error(t, err) + assert.Contains(t, err.Error(), "ip.nonexistent") + assert.Contains(t, err.Error(), "function not found") +} + +func TestCallAPIComplexReturnValue(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(200) + w.Write([]byte(`{"status": "success", "retval": {"ip": "10.0.0.1", "network": "internal"}}`)) + })) + defer server.Close() + + resetConfig() + t.Setenv("SERVERADMIN_TOKEN", "testtoken") + t.Setenv("SERVERADMIN_BASE_URL", server.URL) + + result, err := CallAPI("ip", "get_details", map[string]any{"ip": "10.0.0.1"}) + require.NoError(t, err) + + resultMap, ok := result.(map[string]any) + require.True(t, ok, "expected map return value") + assert.Equal(t, "10.0.0.1", resultMap["ip"]) + assert.Equal(t, "internal", resultMap["network"]) +} + +func TestCallAPINilArgs(t *testing.T) { + var receivedBody callRequest + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + json.Unmarshal(body, &receivedBody) + + w.WriteHeader(200) + w.Write([]byte(`{"status": "success", "retval": null}`)) + })) + defer server.Close() + + resetConfig() + t.Setenv("SERVERADMIN_TOKEN", "testtoken") + t.Setenv("SERVERADMIN_BASE_URL", server.URL) + + result, err := CallAPI("system", "ping", nil) + require.NoError(t, err) + assert.Nil(t, result) + + assert.Equal(t, "system", receivedBody.Group) + assert.Equal(t, "ping", receivedBody.Name) +} diff --git a/examples/update_example.go b/examples/update_example.go index c2fd3b0..850f74e 100644 --- a/examples/update_example.go +++ b/examples/update_example.go @@ -94,6 +94,14 @@ func batchDeleteExample() { fmt.Printf("Deleted %d servers (commit %d)\n", len(servers), commitID) } +func callAPIExample() { + // Call a remote API function + result, err := api.CallAPI("ip", "get_free", map[string]any{"network": "internal"}) + checkErr(err) + + fmt.Printf("Free IP: %s\n", result) +} + func rollbackExample() { q, err := api.FromQuery("hostname=webserver01") checkErr(err)