-
Notifications
You must be signed in to change notification settings - Fork 169
feat(toolsets): add kiali support #425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aljesusg
wants to merge
8
commits into
containers:main
Choose a base branch
from
aljesusg:kiali_toolset
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6be58fc
Add kiali toolset
aljesusg 64f01f4
Kiali Options in a type and make kiali instance
aljesusg 1d75415
Update docs about Kiali
aljesusg 3a14d5b
Make configuration toolsets and update docs
aljesusg f099e92
Change token get
aljesusg e7f6ff9
Adapt tests to the new toolsetconfig
aljesusg 504303a
review(toolsets): align kiali implementation
manusa 6be0f91
Merge pull request #2 from marcnuri-forks/review/kiali
aljesusg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| ## Kiali integration | ||
|
|
||
| This server can expose Kiali tools so assistants can query mesh information (e.g., mesh status/graph). | ||
|
|
||
| ### Enable the Kiali toolset | ||
|
|
||
| You can enable the Kiali tools via config or flags. | ||
|
|
||
| Config (TOML): | ||
|
|
||
| ```toml | ||
| toolsets = ["core", "kiali"] | ||
|
|
||
| [kiali] | ||
| url = "https://kiali.example" | ||
| # insecure = true # optional: allow insecure TLS | ||
| ``` | ||
|
|
||
| Flags: | ||
|
|
||
| ```bash | ||
| kubernetes-mcp-server \ | ||
| --toolsets core,kiali \ | ||
| --kiali-url https://kiali.example \ | ||
| [--kiali-insecure] | ||
| ``` | ||
|
|
||
| When the `kiali` toolset is enabled, a Kiali URL is required. Without it, the server will refuse to start. | ||
|
|
||
| ### How authentication works | ||
|
|
||
| - The server uses your existing Kubernetes credentials (from kubeconfig or in-cluster) to set a bearer token for Kiali calls. | ||
| - If you pass an HTTP Authorization header to the MCP HTTP endpoint, that is not required for Kiali; Kiali calls use the server's configured token. | ||
|
|
||
| ### Available tools (initial) | ||
|
|
||
| - `mesh_status`: retrieves mesh components status from Kiali’s mesh graph endpoint. | ||
|
|
||
| ### Troubleshooting | ||
|
|
||
| - Error: "kiali-url is required when kiali tools are enabled" → provide `--kiali-url` or set `[kiali].url` in the config TOML. | ||
| - TLS issues against Kiali → try `--kiali-insecure` or `[kiali].insecure = true` for non-production environments. | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package kiali | ||
|
|
||
| // Kiali API endpoint paths shared across this package. | ||
| const ( | ||
| // MeshGraph is the Kiali API path that returns the mesh graph/status. | ||
| MeshGraph = "/api/mesh/graph" | ||
| AuthInfo = "/api/auth/info" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package kiali | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "fmt" | ||
| "io" | ||
| "k8s.io/klog/v2" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
| ) | ||
|
|
||
| type Kiali struct { | ||
| manager *Manager | ||
| } | ||
|
|
||
| func (m *Manager) GetKiali() *Kiali { | ||
| return &Kiali{manager: m} | ||
| } | ||
|
|
||
| func (k *Kiali) GetKiali() *Kiali { | ||
| return k | ||
| } | ||
|
|
||
| // validateAndGetURL validates the Kiali client configuration and returns the full URL | ||
| // by safely concatenating the base URL with the provided endpoint, avoiding duplicate | ||
| // or missing slashes regardless of trailing/leading slashes. | ||
| func (k *Kiali) validateAndGetURL(endpoint string) (string, error) { | ||
| if k == nil || k.manager == nil || k.manager.KialiURL == "" { | ||
| return "", fmt.Errorf("kiali client not initialized") | ||
| } | ||
| baseStr := strings.TrimSpace(k.manager.KialiURL) | ||
| if baseStr == "" { | ||
| return "", fmt.Errorf("kiali server URL not configured") | ||
| } | ||
| baseURL, err := url.Parse(baseStr) | ||
| if err != nil { | ||
| return "", fmt.Errorf("invalid kiali base URL: %w", err) | ||
| } | ||
| if endpoint == "" { | ||
| return baseURL.String(), nil | ||
| } | ||
| ref, err := url.Parse(endpoint) | ||
| if err != nil { | ||
| return "", fmt.Errorf("invalid endpoint path: %w", err) | ||
| } | ||
| return baseURL.ResolveReference(ref).String(), nil | ||
| } | ||
|
|
||
| func (k *Kiali) createHTTPClient() *http.Client { | ||
| return &http.Client{ | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: k.manager.KialiInsecure, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // CurrentAuthorizationHeader returns the Authorization header value that the | ||
| // Kiali client is currently configured to use (Bearer <token>), or empty | ||
| // if no bearer token is configured. | ||
| func (k *Kiali) authorizationHeader() string { | ||
| if k == nil || k.manager == nil { | ||
| return "" | ||
| } | ||
| token := strings.TrimSpace(k.manager.BearerToken) | ||
| if token == "" { | ||
| return "" | ||
| } | ||
| lower := strings.ToLower(token) | ||
| if strings.HasPrefix(lower, "bearer ") { | ||
aljesusg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return "Bearer " + strings.TrimSpace(token[7:]) | ||
| } | ||
| return "Bearer " + token | ||
| } | ||
|
|
||
| // executeRequest executes an HTTP request and handles common error scenarios. | ||
| func (k *Kiali) executeRequest(ctx context.Context, endpoint string) (string, error) { | ||
| ApiCallURL, err := k.validateAndGetURL(endpoint) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| klog.V(0).Infof("Kiali Call URL: %s", ApiCallURL) | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, ApiCallURL, nil) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| authHeader := k.authorizationHeader() | ||
| if authHeader != "" { | ||
| req.Header.Set("Authorization", authHeader) | ||
| } | ||
| client := k.createHTTPClient() | ||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
| body, _ := io.ReadAll(resp.Body) | ||
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||
| if len(body) > 0 { | ||
| return "", fmt.Errorf("kiali API error: %s", strings.TrimSpace(string(body))) | ||
| } | ||
| return "", fmt.Errorf("kiali API error: status %d", resp.StatusCode) | ||
| } | ||
| return string(body), nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package kiali | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "testing" | ||
|
|
||
| "github.com/containers/kubernetes-mcp-server/pkg/config" | ||
| ) | ||
|
|
||
| func TestValidateAndGetURL_JoinsProperly(t *testing.T) { | ||
| m := NewManager(&config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example/"}}) | ||
| k := m.GetKiali() | ||
|
|
||
| full, err := k.validateAndGetURL("/api/path") | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if full != "https://kiali.example/api/path" { | ||
| t.Fatalf("unexpected url: %s", full) | ||
| } | ||
|
|
||
| m.KialiURL = "https://kiali.example" | ||
| full, err = k.validateAndGetURL("api/path") | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if full != "https://kiali.example/api/path" { | ||
| t.Fatalf("unexpected url: %s", full) | ||
| } | ||
|
|
||
| // preserve query | ||
| m.KialiURL = "https://kiali.example" | ||
| full, err = k.validateAndGetURL("/api/path?x=1&y=2") | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| u, _ := url.Parse(full) | ||
| if u.Path != "/api/path" || u.Query().Get("x") != "1" || u.Query().Get("y") != "2" { | ||
| t.Fatalf("unexpected parsed url: %s", full) | ||
| } | ||
| } | ||
|
|
||
| // CurrentAuthorizationHeader behavior is now implicit via executeRequest using Manager.BearerToken | ||
|
|
||
| func TestExecuteRequest_SetsAuthAndCallsServer(t *testing.T) { | ||
| // setup test server to assert path and auth header | ||
| var seenAuth string | ||
| var seenPath string | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| seenAuth = r.Header.Get("Authorization") | ||
| seenPath = r.URL.String() | ||
| _, _ = w.Write([]byte("ok")) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| m := NewManager(&config.StaticConfig{KialiOptions: config.KialiOptions{Url: srv.URL}}) | ||
| m.BearerToken = "token-xyz" | ||
| k := m.GetKiali() | ||
| out, err := k.executeRequest(context.Background(), "/api/ping?q=1") | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if out != "ok" { | ||
| t.Fatalf("unexpected body: %s", out) | ||
| } | ||
| if seenAuth != "Bearer token-xyz" { | ||
| t.Fatalf("expected auth header to be set, got '%s'", seenAuth) | ||
| } | ||
| if seenPath != "/api/ping?q=1" { | ||
| t.Fatalf("unexpected path: %s", seenPath) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package kiali | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/containers/kubernetes-mcp-server/pkg/config" | ||
| ) | ||
|
|
||
| type Manager struct { | ||
| BearerToken string | ||
| KialiURL string | ||
| KialiInsecure bool | ||
| } | ||
|
|
||
| func NewManager(config *config.StaticConfig) *Manager { | ||
| return &Manager{ | ||
| BearerToken: "", | ||
| KialiURL: config.KialiOptions.Url, | ||
| KialiInsecure: config.KialiOptions.Insecure, | ||
| } | ||
| } | ||
|
|
||
| func (m *Manager) Derived(_ context.Context) (*Kiali, error) { | ||
| return &Kiali{manager: m}, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package kiali | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/containers/kubernetes-mcp-server/pkg/config" | ||
| ) | ||
|
|
||
| func TestNewManagerUsesConfigFields(t *testing.T) { | ||
| cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example", Insecure: true}} | ||
| m := NewManager(cfg) | ||
| if m == nil { | ||
| t.Fatalf("expected manager, got nil") | ||
| } | ||
| if m.KialiURL != cfg.KialiOptions.Url { | ||
| t.Fatalf("expected KialiURL %s, got %s", cfg.KialiOptions.Url, m.KialiURL) | ||
| } | ||
| if m.KialiInsecure != cfg.KialiOptions.Insecure { | ||
| t.Fatalf("expected KialiInsecure %v, got %v", cfg.KialiOptions.Insecure, m.KialiInsecure) | ||
| } | ||
| } | ||
|
|
||
| func TestDerivedWithoutAuthorizationReturnsOriginalManager(t *testing.T) { | ||
| cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example"}} | ||
| m := NewManager(cfg) | ||
| k, err := m.Derived(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if k == nil || k.manager != m { | ||
| t.Fatalf("expected derived Kiali to keep original manager") | ||
| } | ||
| } | ||
|
|
||
| func TestDerivedPreservesURLAndToken(t *testing.T) { | ||
| cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example", Insecure: true}} | ||
| m := NewManager(cfg) | ||
| m.BearerToken = "token-abc" | ||
| k, err := m.Derived(context.Background()) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if k == nil || k.manager == nil { | ||
| t.Fatalf("expected derived Kiali with manager") | ||
| } | ||
| if k.manager.BearerToken != "token-abc" { | ||
| t.Fatalf("expected bearer token 'token-abc', got '%s'", k.manager.BearerToken) | ||
| } | ||
| if k.manager.KialiURL != m.KialiURL || k.manager.KialiInsecure != m.KialiInsecure { | ||
| t.Fatalf("expected Kiali URL/insecure preserved") | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.