Files
micro--go-micro/cmd/micro/api/api.go
T
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

326 lines
8.8 KiB
Go

// Package api implements the 'micro api' command — a lightweight
// HTTP-to-RPC gateway that proxies JSON requests to go-micro services.
//
// Usage:
//
// micro api # listen on :8080
// micro api --address :3000 # custom port
//
// Requests:
//
// POST /service/endpoint → RPC call to service.endpoint
// GET /health → {"status":"ok"}
//
// The request body is forwarded as-is (JSON). The Micro-Endpoint
// header can also be used to specify the endpoint.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"sort"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v6/broker"
"go-micro.dev/v6/client"
"go-micro.dev/v6/cmd"
codecBytes "go-micro.dev/v6/codec/bytes"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/store"
)
func init() {
cmd.Register(&cli.Command{
Name: "api",
Usage: "Run a lightweight HTTP-to-RPC API gateway",
Description: `Start an HTTP gateway that proxies JSON requests to go-micro services.
Requests are routed by URL path:
POST /service/endpoint → calls service.endpoint via RPC
GET / → lists available services and endpoints
Examples:
# Start on default port
micro api
# Custom port
micro api --address :3000
# Call a service through the gateway
curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello
# Or use the Micro-Endpoint header
curl -XPOST -H 'Micro-Endpoint: Greeter.Hello' \
-d '{"name":"Alice"}' http://localhost:8080/greeter`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":8080",
EnvVars: []string{"MICRO_API_ADDRESS"},
},
},
Action: run,
})
}
func run(c *cli.Context) error {
addr := c.String("address")
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Framework primitives under /micro/
registerFrameworkRoutes(mux)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
path := strings.TrimPrefix(r.URL.Path, "/")
path = strings.TrimSuffix(path, "/")
// Root: list services
if path == "" {
listServices(w)
return
}
// Parse service/endpoint from path
parts := strings.SplitN(path, "/", 2)
serviceName := parts[0]
endpoint := ""
if len(parts) > 1 {
endpoint = parts[1]
}
// Allow Micro-Endpoint header to override
if h := r.Header.Get("Micro-Endpoint"); h != "" {
endpoint = h
}
if endpoint == "" {
describeService(w, serviceName)
return
}
// Proxy RPC call
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "failed to read body: "+err.Error())
return
}
if len(body) == 0 {
body = []byte("{}")
}
req := client.DefaultClient.NewRequest(serviceName, endpoint, &codecBytes.Frame{Data: body})
var rsp codecBytes.Frame
if err := client.DefaultClient.Call(r.Context(), req, &rsp); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(rsp.Data)
})
fmt.Println()
fmt.Println(" \033[1mmicro api\033[0m")
fmt.Println()
fmt.Printf(" Listening \033[36m%s\033[0m\n", addr)
fmt.Println()
fmt.Println(" Routes:")
fmt.Println(" \033[32mGET\033[0m / List services")
fmt.Println(" \033[32mGET\033[0m /{service} Describe a service")
fmt.Println(" \033[33mPOST\033[0m /{service}/{endpoint} Call an endpoint")
fmt.Println(" \033[32mGET\033[0m /health Health check")
fmt.Println()
fmt.Println(" Framework:")
fmt.Println(" \033[32mGET\033[0m /micro/registry List registered services")
fmt.Println(" \033[32mGET\033[0m /micro/registry/{name} Describe a service")
fmt.Println(" \033[32mGET\033[0m /micro/store List store keys")
fmt.Println(" \033[32mGET\033[0m /micro/store/{key} Read a record")
fmt.Println(" \033[33mPOST\033[0m /micro/store/{key} Write a record")
fmt.Println(" \033[33mPOST\033[0m /micro/broker/{topic} Publish a message")
fmt.Println()
server := &http.Server{Addr: addr, Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "server error: %v\n", err)
os.Exit(1)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println("\nShutting down...")
return server.Close()
}
func listServices(w http.ResponseWriter) {
services, err := registry.ListServices()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
type svcInfo struct {
Name string `json:"name"`
Endpoints []string `json:"endpoints,omitempty"`
}
var result []svcInfo
for _, svc := range services {
info := svcInfo{Name: svc.Name}
full, err := registry.GetService(svc.Name)
if err == nil && len(full) > 0 {
for _, ep := range full[0].Endpoints {
info.Endpoints = append(info.Endpoints, ep.Name)
}
}
result = append(result, info)
}
json.NewEncoder(w).Encode(result)
}
func describeService(w http.ResponseWriter, name string) {
services, err := registry.GetService(name)
if err != nil || len(services) == 0 {
writeError(w, http.StatusNotFound, "service not found: "+name)
return
}
type epInfo struct {
Name string `json:"name"`
Metadata map[string]string `json:"metadata,omitempty"`
}
svc := services[0]
var endpoints []epInfo
for _, ep := range svc.Endpoints {
endpoints = append(endpoints, epInfo{
Name: ep.Name,
Metadata: ep.Metadata,
})
}
json.NewEncoder(w).Encode(map[string]any{
"name": svc.Name,
"version": svc.Version,
"endpoints": endpoints,
"nodes": len(svc.Nodes),
})
}
func writeError(w http.ResponseWriter, code int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
// registerFrameworkRoutes adds /micro/* routes for registry, broker, and store.
func registerFrameworkRoutes(mux *http.ServeMux) {
// Registry
mux.HandleFunc("/micro/registry", func(w http.ResponseWriter, r *http.Request) {
listServices(w)
})
mux.HandleFunc("/micro/registry/", func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/micro/registry/")
if name == "" {
listServices(w)
return
}
describeService(w, name)
})
// Store
mux.HandleFunc("/micro/store", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
keys, err := store.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(keys)
})
mux.HandleFunc("/micro/store/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/micro/store/")
if key == "" {
w.Header().Set("Content-Type", "application/json")
keys, _ := store.List()
json.NewEncoder(w).Encode(keys)
return
}
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
records, err := store.Read(key)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if len(records) == 0 {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.Write(records[0].Value)
case http.MethodPost:
body, _ := io.ReadAll(r.Body)
if err := store.Write(&store.Record{Key: key, Value: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "key": key})
default:
writeError(w, http.StatusMethodNotAllowed, "use GET or POST")
}
})
// Broker
mux.HandleFunc("/micro/broker/", func(w http.ResponseWriter, r *http.Request) {
topic := strings.TrimPrefix(r.URL.Path, "/micro/broker/")
if topic == "" {
writeError(w, http.StatusBadRequest, "topic required: /micro/broker/{topic}")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "use POST to publish")
return
}
body, _ := io.ReadAll(r.Body)
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
writeError(w, http.StatusInternalServerError, "broker connect: "+err.Error())
return
}
if err := b.Publish(topic, &broker.Message{Body: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "topic": topic})
})
}