Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6be7e942e | |||
| 680de3a536 | |||
| 4aaba87ec3 |
+20
-1
@@ -71,6 +71,18 @@ func init() {
|
||||
{
|
||||
Name: "call",
|
||||
Usage: "Call a service",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "header",
|
||||
Aliases: []string{"H"},
|
||||
Usage: "Set request headers (can be used multiple times): --header 'Key:Value'",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "metadata",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Set request metadata (can be used multiple times): --metadata 'Key:Value'",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
|
||||
@@ -86,9 +98,16 @@ func init() {
|
||||
request = args.Get(2)
|
||||
}
|
||||
|
||||
// Create context with metadata if provided
|
||||
// Note: This is for the direct 'micro call' command.
|
||||
// Dynamic service calls (e.g., 'micro helloworld call') are handled in CallService.
|
||||
callCtx := context.TODO()
|
||||
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("metadata"))
|
||||
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("header"))
|
||||
|
||||
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
|
||||
var rsp bytes.Frame
|
||||
err := client.Call(context.TODO(), req, &rsp)
|
||||
err := client.Call(callCtx, req, &rsp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,9 +15,30 @@ import (
|
||||
"github.com/stretchr/objx"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/client"
|
||||
"go-micro.dev/v5/metadata"
|
||||
"go-micro.dev/v5/registry"
|
||||
)
|
||||
|
||||
// AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context
|
||||
func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context {
|
||||
if len(metadataStrings) == 0 {
|
||||
return ctx
|
||||
}
|
||||
|
||||
md := make(metadata.Metadata)
|
||||
for _, m := range metadataStrings {
|
||||
parts := strings.SplitN(m, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
md[key] = value
|
||||
}
|
||||
|
||||
return metadata.MergeContext(ctx, md, true)
|
||||
}
|
||||
|
||||
// LookupService queries the service for a service with the given alias. If
|
||||
// no services are found for a given alias, the registry will return nil and
|
||||
// the error will also be nil. An error is only returned if there was an issue
|
||||
@@ -132,17 +153,27 @@ func CallService(srv *registry.Service, args []string) error {
|
||||
return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
|
||||
}
|
||||
|
||||
// parse the flags
|
||||
// create a context for the call
|
||||
callCtx := context.TODO()
|
||||
|
||||
// parse out --header or --metadata flags before parsing request body
|
||||
// Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y').
|
||||
// Direct 'micro call' commands are handled in cli.go.
|
||||
if headerFlags, ok := flags["header"]; ok {
|
||||
callCtx = AddMetadataToContext(callCtx, headerFlags)
|
||||
delete(flags, "header")
|
||||
}
|
||||
if metadataFlags, ok := flags["metadata"]; ok {
|
||||
callCtx = AddMetadataToContext(callCtx, metadataFlags)
|
||||
delete(flags, "metadata")
|
||||
}
|
||||
|
||||
// parse the flags into request body
|
||||
body, err := FlagsToRequest(flags, ep.Request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create a context for the call based on the cli context
|
||||
callCtx := context.TODO()
|
||||
|
||||
// TODO: parse out --header or --metadata
|
||||
|
||||
// construct and execute the request using the json content type
|
||||
req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json"))
|
||||
var rsp json.RawMessage
|
||||
@@ -387,7 +418,7 @@ func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]
|
||||
// so we do that here
|
||||
if strings.Contains(key, "-") {
|
||||
parts := strings.Split(key, "-")
|
||||
for i, _ := range parts {
|
||||
for i := range parts {
|
||||
pToCreate := strings.Join(parts[0:i], ".")
|
||||
if i > 0 && i < len(parts) && !result.Has(pToCreate) {
|
||||
result.Set(pToCreate, map[string]interface{}{})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"go-micro.dev/v5/metadata"
|
||||
goregistry "go-micro.dev/v5/registry"
|
||||
)
|
||||
|
||||
@@ -377,3 +379,75 @@ func TestDynamicFlagParsing(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddMetadataToContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadataStrs []string
|
||||
expectedKeys []string
|
||||
expectedValues []string
|
||||
}{
|
||||
{
|
||||
name: "Single metadata",
|
||||
metadataStrs: []string{"Key1:Value1"},
|
||||
expectedKeys: []string{"Key1"},
|
||||
expectedValues: []string{"Value1"},
|
||||
},
|
||||
{
|
||||
name: "Multiple metadata",
|
||||
metadataStrs: []string{"Key1:Value1", "Key2:Value2"},
|
||||
expectedKeys: []string{"Key1", "Key2"},
|
||||
expectedValues: []string{"Value1", "Value2"},
|
||||
},
|
||||
{
|
||||
name: "Metadata with spaces",
|
||||
metadataStrs: []string{"Key1: Value1 ", " Key2 : Value2"},
|
||||
expectedKeys: []string{"Key1", "Key2"},
|
||||
expectedValues: []string{"Value1", "Value2"},
|
||||
},
|
||||
{
|
||||
name: "Metadata with colon in value",
|
||||
metadataStrs: []string{"Authorization:Bearer token:123"},
|
||||
expectedKeys: []string{"Authorization"},
|
||||
expectedValues: []string{"Bearer token:123"},
|
||||
},
|
||||
{
|
||||
name: "Empty metadata",
|
||||
metadataStrs: []string{},
|
||||
expectedKeys: []string{},
|
||||
expectedValues: []string{},
|
||||
},
|
||||
{
|
||||
name: "Invalid metadata format",
|
||||
metadataStrs: []string{"InvalidFormat"},
|
||||
expectedKeys: []string{},
|
||||
expectedValues: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = AddMetadataToContext(ctx, tt.metadataStrs)
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if len(tt.expectedKeys) == 0 && !ok {
|
||||
return // Expected no metadata
|
||||
}
|
||||
|
||||
if !ok && len(tt.expectedKeys) > 0 {
|
||||
t.Fatal("Expected metadata in context but got none")
|
||||
}
|
||||
|
||||
for i, key := range tt.expectedKeys {
|
||||
value, found := md.Get(key)
|
||||
if !found {
|
||||
t.Fatalf("Expected key %s not found in metadata", key)
|
||||
}
|
||||
if value != tt.expectedValues[i] {
|
||||
t.Fatalf("Expected value %s for key %s, got %s", tt.expectedValues[i], key, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ type templates struct {
|
||||
authLogin *template.Template
|
||||
authUsers *template.Template
|
||||
playground *template.Template
|
||||
scopes *template.Template
|
||||
scopes *template.Template
|
||||
}
|
||||
type TemplateUser struct {
|
||||
ID string
|
||||
@@ -82,7 +82,7 @@ func parseTemplates() *templates {
|
||||
authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")),
|
||||
authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")),
|
||||
playground: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/playground.html")),
|
||||
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
|
||||
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,8 +657,8 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
|
||||
// Discover tools from registry
|
||||
services, _ := registry.ListServices()
|
||||
type toolInfo struct {
|
||||
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
|
||||
SafeName string // LLM-safe name (dots replaced with underscores)
|
||||
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
|
||||
SafeName string // LLM-safe name (dots replaced with underscores)
|
||||
Description string
|
||||
Properties map[string]any
|
||||
}
|
||||
@@ -897,9 +897,9 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
|
||||
"result": rpcResult,
|
||||
})
|
||||
toolResultBlocks = append(toolResultBlocks, map[string]any{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tu.ID,
|
||||
"content": rpcContent,
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tu.ID,
|
||||
"content": rpcContent,
|
||||
})
|
||||
}
|
||||
result["tool_calls"] = toolCalls
|
||||
|
||||
@@ -63,22 +63,22 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func usersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
|
||||
// Return all users
|
||||
userList := make([]*User, 0, len(users))
|
||||
for _, user := range users {
|
||||
userList = append(userList, user)
|
||||
}
|
||||
|
||||
|
||||
json.NewEncoder(w).Encode(userList)
|
||||
}
|
||||
|
||||
func userHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
|
||||
// Extract user ID from path
|
||||
id := r.URL.Path[len("/users/"):]
|
||||
|
||||
|
||||
user, exists := users[id]
|
||||
if !exists {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
@@ -87,7 +87,7 @@ func userHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ type mockAuth struct {
|
||||
accounts map[string]*auth.Account // token -> account
|
||||
}
|
||||
|
||||
func (m *mockAuth) Init(...auth.Option) {}
|
||||
func (m *mockAuth) Options() auth.Options { return auth.Options{} }
|
||||
func (m *mockAuth) Init(...auth.Option) {}
|
||||
func (m *mockAuth) Options() auth.Options { return auth.Options{} }
|
||||
func (m *mockAuth) Generate(string, ...auth.GenerateOption) (*auth.Account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -540,7 +540,7 @@ func TestScopesFromGatewayOptions(t *testing.T) {
|
||||
s := newTestServer(Options{
|
||||
Registry: reg,
|
||||
Scopes: map[string][]string{
|
||||
"blog.Blog.Create": {"blog:admin"}, // override service scope
|
||||
"blog.Blog.Create": {"blog:admin"}, // override service scope
|
||||
"blog.Blog.Delete": {"blog:admin", "sudo"}, // add scope to tool without service scope
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user