diff --git a/cmd/api/mcp/server.go b/cmd/api/mcp/server.go new file mode 100644 index 0000000..36f746c --- /dev/null +++ b/cmd/api/mcp/server.go @@ -0,0 +1,54 @@ +// Package mcp exposes the tuning operation through the Model Context Protocol. +package mcp + +import ( + "context" + "net/http" + + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/pgconfig/api/pkg/rules" + "github.com/pgconfig/api/pkg/version" +) + +const recommendToolName = "recommend_postgres_configuration" + +// NewHandler returns the stateless Streamable HTTP MCP endpoint. +func NewHandler() http.Handler { + server := mcpsdk.NewServer(&mcpsdk.Implementation{ + Name: "pgconfig", + Version: version.Pretty(), + }, nil) + + falseValue := false + mcpsdk.AddTool(server, &mcpsdk.Tool{ + Name: recommendToolName, + Description: "Return deterministic PostgreSQL configuration " + + "recommendations for a complete tuning request.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: &falseValue, + IdempotentHint: true, + OpenWorldHint: &falseValue, + ReadOnlyHint: true, + }, + }, recommendPostgresConfiguration) + + return mcpsdk.NewStreamableHTTPHandler( + func(*http.Request) *mcpsdk.Server { return server }, + &mcpsdk.StreamableHTTPOptions{ + Stateless: true, + JSONResponse: true, + }, + ) +} + +func recommendPostgresConfiguration( + _ context.Context, + _ *mcpsdk.CallToolRequest, + request rules.TuningRequest, +) (*mcpsdk.CallToolResult, rules.TuningResult, error) { + result, err := rules.Tune(request) + if err != nil { + return nil, rules.TuningResult{}, err + } + return nil, *result, nil +} diff --git a/cmd/api/routes/mcp_test.go b/cmd/api/routes/mcp_test.go new file mode 100644 index 0000000..06e9655 --- /dev/null +++ b/cmd/api/routes/mcp_test.go @@ -0,0 +1,129 @@ +package routes_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/gofiber/fiber/v2/middleware/adaptor" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/pgconfig/api/cmd/api/routes" + "github.com/pgconfig/api/pkg/input/bytes" + "github.com/pgconfig/api/pkg/version" + "github.com/stretchr/testify/require" +) + +func TestMCPHappyPath(t *testing.T) { + app := routes.New() + httpServer := httptest.NewServer(adaptor.FiberApp(app)) + t.Cleanup(httpServer.Close) + + ctx := context.Background() + recorder := &responseRecorder{base: httpServer.Client().Transport} + client := mcp.NewClient(&mcp.Implementation{Name: "pgconfig-test", Version: "1.0.0"}, nil) + session, err := client.Connect(ctx, &mcp.StreamableClientTransport{ + Endpoint: httpServer.URL + "/mcp", + HTTPClient: &http.Client{Transport: recorder}, + DisableStandaloneSSE: true, + }, nil) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, session.Close()) }) + + initializeResult := session.InitializeResult() + require.Equal(t, "pgconfig", initializeResult.ServerInfo.Name) + require.Equal(t, version.Pretty(), initializeResult.ServerInfo.Version) + + tools, err := session.ListTools(ctx, nil) + require.NoError(t, err) + require.Len(t, tools.Tools, 1) + + tool := tools.Tools[0] + require.Equal(t, "recommend_postgres_configuration", tool.Name) + require.Contains(t, tool.Description, "deterministic") + require.NotNil(t, tool.OutputSchema) + require.NotNil(t, tool.Annotations) + require.True(t, tool.Annotations.ReadOnlyHint) + require.True(t, tool.Annotations.IdempotentHint) + require.NotNil(t, tool.Annotations.DestructiveHint) + require.False(t, *tool.Annotations.DestructiveHint) + require.NotNil(t, tool.Annotations.OpenWorldHint) + require.False(t, *tool.Annotations.OpenWorldHint) + + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: tool.Name, + Arguments: map[string]any{ + "os": " Linux ", + "arch": " x86-64 ", + "total_ram": int64(16 * bytes.GB), + "profile": " web ", + "disk_type": " ssd ", + "max_connections": 100, + "total_cpu": 8, + "postgres_version": " 18.4 ", + }, + }) + require.NoError(t, err) + require.False(t, callResult.IsError) + require.Len(t, callResult.Content, 1) + + structured, ok := callResult.StructuredContent.(map[string]any) + require.True(t, ok) + request := structured["request"].(map[string]any) + require.Equal(t, "linux", request["os"]) + require.Equal(t, "amd64", request["arch"]) + require.Equal(t, "WEB", request["profile"]) + require.Equal(t, "SSD", request["disk_type"]) + require.Equal(t, "18.4", request["postgres_version"]) + require.Empty(t, structured["assumptions"]) + require.Empty(t, structured["warnings"]) + + recommendations := structured["recommendations"].(map[string]any) + require.NotEmpty(t, recommendations) + require.NotContains(t, recommendations, "listen_addresses") + + text, ok := callResult.Content[0].(*mcp.TextContent) + require.True(t, ok) + structuredJSON, err := json.Marshal(structured) + require.NoError(t, err) + require.JSONEq(t, string(structuredJSON), text.Text) + + contentTypes := recorder.successfulPostContentTypes() + require.Len(t, contentTypes, 3) + for _, contentType := range contentTypes { + require.Equal(t, "application/json", contentType) + } + + requestWithoutSession, err := http.NewRequest(http.MethodGet, httpServer.URL+"/mcp", nil) + require.NoError(t, err) + requestWithoutSession.Header.Set("Accept", "text/event-stream") + response, err := httpServer.Client().Do(requestWithoutSession) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, response.Body.Close()) }) + require.Equal(t, http.StatusMethodNotAllowed, response.StatusCode) + require.Equal(t, "POST", response.Header.Get("Allow")) +} + +type responseRecorder struct { + base http.RoundTripper + mu sync.Mutex + contentTypes []string +} + +func (r *responseRecorder) RoundTrip(request *http.Request) (*http.Response, error) { + response, err := r.base.RoundTrip(request) + if err == nil && request.Method == http.MethodPost && response.StatusCode == http.StatusOK { + r.mu.Lock() + r.contentTypes = append(r.contentTypes, response.Header.Get("Content-Type")) + r.mu.Unlock() + } + return response, err +} + +func (r *responseRecorder) successfulPostContentTypes() []string { + r.mu.Lock() + defer r.mu.Unlock() + return append([]string(nil), r.contentTypes...) +} diff --git a/cmd/api/routes/routes.go b/cmd/api/routes/routes.go index a03dcd3..07f7e0e 100644 --- a/cmd/api/routes/routes.go +++ b/cmd/api/routes/routes.go @@ -4,12 +4,14 @@ import ( "fmt" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/swagger" handV1 "github.com/pgconfig/api/cmd/api/handlers/v1" + mcpHandler "github.com/pgconfig/api/cmd/api/mcp" ) // New create an instance of Book app routes @@ -63,6 +65,7 @@ func New() *fiber.App { }) app.Get("/v1/version", handV1.Version) + app.All("/mcp", adaptor.HTTPHandler(mcpHandler.NewHandler())) v1 := app.Group("/v1/tuning/") diff --git a/go.mod b/go.mod index 1546e88..b93f824 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/gofiber/swagger v1.1.1 github.com/mackerelio/go-osstat v0.2.6 github.com/mitchellh/go-homedir v1.1.0 + github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/smartystreets/goconvey v1.8.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 @@ -44,6 +45,7 @@ require ( github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -58,6 +60,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -68,13 +72,15 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.68.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2a22adc..8eda972 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,13 @@ github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5 github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -100,6 +105,8 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= +github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -113,6 +120,10 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= @@ -148,6 +159,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= @@ -164,8 +177,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -175,8 +188,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -198,8 +213,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -218,16 +233,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/pkg/rules/tuning.go b/pkg/rules/tuning.go index fdf9cec..bf5c168 100644 --- a/pkg/rules/tuning.go +++ b/pkg/rules/tuning.go @@ -34,6 +34,8 @@ type TuningRecommendation struct { // TuningResult is the provenance-aware result returned by Tune. type TuningResult struct { Request TuningRequest `json:"request"` + Assumptions []string `json:"assumptions"` + Warnings []string `json:"warnings"` Recommendations map[string]TuningRecommendation `json:"recommendations"` ApplicationVersion string `json:"application_version"` legacy *category.ExportCfg @@ -93,6 +95,8 @@ func Tune(request TuningRequest) (*TuningResult, error) { return &TuningResult{ Request: normalized, + Assumptions: []string{}, + Warnings: []string{}, Recommendations: recommendations, ApplicationVersion: version.Pretty(), legacy: legacy, @@ -108,6 +112,7 @@ func (r *TuningResult) CompatibilityProjection() *category.ExportCfg { func normalizeRequest(request TuningRequest) TuningRequest { request.OS = strings.ToLower(strings.TrimSpace(request.OS)) request.Arch = strings.ToLower(strings.TrimSpace(request.Arch)) + request.Profile = profile.Profile(strings.ToUpper(strings.TrimSpace(string(request.Profile)))) switch request.Arch { case "i686": request.Arch = "386"