Skip to content

Commit 37fabea

Browse files
committed
Make configuration toolsets and update docs
Signed-off-by: Alberto Gutierrez <aljesusg@gmail.com>
1 parent 0f3dd42 commit 37fabea

File tree

9 files changed

+172
-33
lines changed

9 files changed

+172
-33
lines changed

docs/KIALI_INTEGRATION.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Config (TOML):
1111
```toml
1212
toolsets = ["core", "kiali"]
1313

14-
[kiali]
14+
[toolset_configs.kiali]
1515
url = "https://kiali.example"
1616
# insecure = true # optional: allow insecure TLS
1717
```
@@ -25,7 +25,7 @@ kubernetes-mcp-server \
2525
[--kiali-insecure]
2626
```
2727

28-
When the `kiali` toolset is enabled, a Kiali URL is required. Without it, the server will refuse to start.
28+
When the `kiali` toolset is enabled, a Kiali toolset configuration is required. Provide it via `[toolset_configs.kiali]` in the config file or by passing flags (which populate the toolset config). If missing or invalid, the server will refuse to start.
2929

3030
### How authentication works
3131

@@ -38,7 +38,8 @@ When the `kiali` toolset is enabled, a Kiali URL is required. Without it, the se
3838

3939
### Troubleshooting
4040

41-
- Error: "kiali-url is required when kiali tools are enabled" → provide `--kiali-url` or set `[kiali].url` in the config TOML.
42-
- TLS issues against Kiali → try `--kiali-insecure` or `[kiali].insecure = true` for non-production environments.
41+
- Missing Kiali configuration when `kiali` toolset is enabled → provide `--kiali-url` or set `[toolset_configs.kiali].url` in the config TOML.
42+
- Invalid URL → ensure `[toolset_configs.kiali].url` is a valid `http(s)://host` URL.
43+
- TLS issues against Kiali → try `--kiali-insecure` or `[toolset_configs.kiali].insecure = true` for non-production environments.
4344

4445

pkg/config/config.go

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@ const (
1616
ClusterProviderDisabled = "disabled"
1717
)
1818

19-
// KialiOptions is the configuration for the kiali toolset.
20-
type KialiOptions struct {
21-
Url string `toml:"url,omitempty"`
22-
Insecure bool `toml:"insecure,omitempty"`
23-
}
24-
2519
// StaticConfig is the configuration for the server.
2620
// It allows to configure server specific settings and tools to be enabled or disabled.
2721
type StaticConfig struct {
@@ -74,11 +68,14 @@ type StaticConfig struct {
7468
// This map holds raw TOML primitives that will be parsed by registered provider parsers
7569
ClusterProviderConfigs map[string]toml.Primitive `toml:"cluster_provider_configs,omitempty"`
7670

77-
// KialiOptions is the configuration for the kiali toolset.
78-
KialiOptions KialiOptions `toml:"kiali,omitempty"`
71+
// Toolset-specific configurations
72+
// This map holds raw TOML primitives that will be parsed by registered toolset parsers
73+
ToolsetConfigs map[string]toml.Primitive `toml:"toolset_configs,omitempty"`
7974

8075
// Internal: parsed provider configs (not exposed to TOML package)
8176
parsedClusterProviderConfigs map[string]ProviderConfig
77+
// Internal: parsed toolset configs (not exposed to TOML package)
78+
parsedToolsetConfigs map[string]ToolsetConfig
8279

8380
// Internal: the config.toml directory, to help resolve relative file paths
8481
configDirPath string
@@ -136,6 +133,10 @@ func ReadToml(configData []byte, opts ...ReadConfigOpt) (*StaticConfig, error) {
136133
return nil, err
137134
}
138135

136+
if err := config.parseToolsetConfigs(md); err != nil {
137+
return nil, err
138+
}
139+
139140
return config, nil
140141
}
141142

@@ -172,3 +173,43 @@ func (c *StaticConfig) parseClusterProviderConfigs(md toml.MetaData) error {
172173

173174
return nil
174175
}
176+
177+
func (c *StaticConfig) parseToolsetConfigs(md toml.MetaData) error {
178+
if c.parsedToolsetConfigs == nil {
179+
c.parsedToolsetConfigs = make(map[string]ToolsetConfig, len(c.ToolsetConfigs))
180+
}
181+
182+
ctx := withConfigDirPath(context.Background(), c.configDirPath)
183+
184+
for name, primitive := range c.ToolsetConfigs {
185+
parser, ok := getToolsetConfigParser(name)
186+
if !ok {
187+
continue
188+
}
189+
190+
toolsetConfig, err := parser(ctx, primitive, md)
191+
if err != nil {
192+
return fmt.Errorf("failed to parse config for Toolset '%s': %w", name, err)
193+
}
194+
195+
if err := toolsetConfig.Validate(); err != nil {
196+
return fmt.Errorf("invalid config file for Toolset '%s': %w", name, err)
197+
}
198+
199+
c.parsedToolsetConfigs[name] = toolsetConfig
200+
}
201+
202+
return nil
203+
}
204+
205+
func (c *StaticConfig) GetToolsetConfig(name string) (ToolsetConfig, bool) {
206+
cfg, ok := c.parsedToolsetConfigs[name]
207+
return cfg, ok
208+
}
209+
210+
func (c *StaticConfig) SetToolsetConfig(name string, cfg ToolsetConfig) {
211+
if c.parsedToolsetConfigs == nil {
212+
c.parsedToolsetConfigs = make(map[string]ToolsetConfig)
213+
}
214+
c.parsedToolsetConfigs[name] = cfg
215+
}

pkg/config/toolset_config.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/BurntSushi/toml"
8+
)
9+
10+
// ToolsetConfig is the interface that all toolset-specific configurations must implement.
11+
// Each toolset registers a factory function to parse its config from TOML primitives
12+
type ToolsetConfig interface {
13+
Validate() error
14+
}
15+
16+
type ToolsetConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (ToolsetConfig, error)
17+
18+
var (
19+
toolsetConfigParsers = make(map[string]ToolsetConfigParser)
20+
)
21+
22+
func RegisterToolsetConfig(name string, parser ToolsetConfigParser) {
23+
if _, exists := toolsetConfigParsers[name]; exists {
24+
panic(fmt.Sprintf("toolset config parser already registered for toolset '%s'", name))
25+
}
26+
27+
toolsetConfigParsers[name] = parser
28+
}
29+
30+
func getToolsetConfigParser(name string) (ToolsetConfigParser, bool) {
31+
parser, ok := toolsetConfigParsers[name]
32+
33+
return parser, ok
34+
}

pkg/kiali/config.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package kiali
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/url"
7+
8+
"github.com/BurntSushi/toml"
9+
"github.com/containers/kubernetes-mcp-server/pkg/config"
10+
)
11+
12+
// Config holds Kiali toolset configuration
13+
type Config struct {
14+
Url string `toml:"url,omitempty"`
15+
Insecure bool `toml:"insecure,omitempty"`
16+
}
17+
18+
var _ config.ToolsetConfig = (*Config)(nil)
19+
20+
func (c *Config) Validate() error {
21+
if c == nil {
22+
return errors.New("kiali config is nil")
23+
}
24+
if c.Url == "" {
25+
return errors.New("kiali-url is required")
26+
}
27+
if u, err := url.Parse(c.Url); err != nil || u.Scheme == "" || u.Host == "" {
28+
return errors.New("kiali-url must be a valid URL")
29+
}
30+
return nil
31+
}
32+
33+
func kialiToolsetParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (config.ToolsetConfig, error) {
34+
var cfg Config
35+
if err := md.PrimitiveDecode(primitive, &cfg); err != nil {
36+
return nil, err
37+
}
38+
return &cfg, nil
39+
}
40+
41+
func init() {
42+
config.RegisterToolsetConfig("kiali", kialiToolsetParser)
43+
}

pkg/kiali/kiali_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
)
1212

1313
func TestValidateAndGetURL_JoinsProperly(t *testing.T) {
14-
m := NewManager(&config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example/"}})
14+
cfg := config.Default()
15+
cfg.SetToolsetConfig("kiali", &Config{Url: "https://kiali.example/"})
16+
m := NewManager(cfg)
1517
k := m.GetKiali()
1618

1719
full, err := k.validateAndGetURL("/api/path")
@@ -56,7 +58,9 @@ func TestExecuteRequest_SetsAuthAndCallsServer(t *testing.T) {
5658
}))
5759
defer srv.Close()
5860

59-
m := NewManager(&config.StaticConfig{KialiOptions: config.KialiOptions{Url: srv.URL}})
61+
cfg := config.Default()
62+
cfg.SetToolsetConfig("kiali", &Config{Url: srv.URL})
63+
m := NewManager(cfg)
6064
m.BearerToken = "token-xyz"
6165
k := m.GetKiali()
6266
out, err := k.executeRequest(context.Background(), "/api/ping?q=1")

pkg/kiali/manager.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ type Manager struct {
1313
}
1414

1515
func NewManager(config *config.StaticConfig) *Manager {
16-
return &Manager{
16+
m := &Manager{
1717
BearerToken: "",
18-
KialiURL: config.KialiOptions.Url,
19-
KialiInsecure: config.KialiOptions.Insecure,
18+
KialiURL: "",
19+
KialiInsecure: false,
2020
}
21+
if cfg, ok := config.GetToolsetConfig("kiali"); ok {
22+
if kc, ok := cfg.(*Config); ok && kc != nil {
23+
m.KialiURL = kc.Url
24+
m.KialiInsecure = kc.Insecure
25+
}
26+
}
27+
return m
2128
}
2229

2330
func (m *Manager) Derived(_ context.Context) (*Kiali, error) {

pkg/kiali/manager_test.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,23 @@ import (
88
)
99

1010
func TestNewManagerUsesConfigFields(t *testing.T) {
11-
cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example", Insecure: true}}
11+
cfg := config.Default()
12+
cfg.SetToolsetConfig("kiali", &Config{Url: "https://kiali.example", Insecure: true})
1213
m := NewManager(cfg)
1314
if m == nil {
1415
t.Fatalf("expected manager, got nil")
1516
}
16-
if m.KialiURL != cfg.KialiOptions.Url {
17-
t.Fatalf("expected KialiURL %s, got %s", cfg.KialiOptions.Url, m.KialiURL)
17+
if m.KialiURL != "https://kiali.example" {
18+
t.Fatalf("expected KialiURL %s, got %s", "https://kiali.example", m.KialiURL)
1819
}
19-
if m.KialiInsecure != cfg.KialiOptions.Insecure {
20-
t.Fatalf("expected KialiInsecure %v, got %v", cfg.KialiOptions.Insecure, m.KialiInsecure)
20+
if m.KialiInsecure != true {
21+
t.Fatalf("expected KialiInsecure %v, got %v", true, m.KialiInsecure)
2122
}
2223
}
2324

2425
func TestDerivedWithoutAuthorizationReturnsOriginalManager(t *testing.T) {
25-
cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example"}}
26+
cfg := config.Default()
27+
cfg.SetToolsetConfig("kiali", &Config{Url: "https://kiali.example"})
2628
m := NewManager(cfg)
2729
k, err := m.Derived(context.Background())
2830
if err != nil {
@@ -34,7 +36,8 @@ func TestDerivedWithoutAuthorizationReturnsOriginalManager(t *testing.T) {
3436
}
3537

3638
func TestDerivedPreservesURLAndToken(t *testing.T) {
37-
cfg := &config.StaticConfig{KialiOptions: config.KialiOptions{Url: "https://kiali.example", Insecure: true}}
39+
cfg := config.Default()
40+
cfg.SetToolsetConfig("kiali", &Config{Url: "https://kiali.example", Insecure: true})
3841
m := NewManager(cfg)
3942
m.BearerToken = "token-abc"
4043
k, err := m.Derived(context.Background())

pkg/kiali/mesh_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ func TestMeshStatus_CallsGraphWithExpectedQuery(t *testing.T) {
1919
}))
2020
defer srv.Close()
2121

22-
m := NewManager(&config.StaticConfig{KialiOptions: config.KialiOptions{Url: srv.URL}})
22+
cfg := config.Default()
23+
cfg.SetToolsetConfig("kiali", &Config{Url: srv.URL})
24+
m := NewManager(cfg)
2325
m.BearerToken = "tkn"
2426
k := m.GetKiali()
2527
out, err := k.MeshStatus(context.Background())

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"github.com/containers/kubernetes-mcp-server/pkg/config"
2727
internalhttp "github.com/containers/kubernetes-mcp-server/pkg/http"
28+
"github.com/containers/kubernetes-mcp-server/pkg/kiali"
2829
"github.com/containers/kubernetes-mcp-server/pkg/mcp"
2930
"github.com/containers/kubernetes-mcp-server/pkg/output"
3031
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
@@ -243,11 +244,8 @@ func (m *MCPServerOptions) loadFlags(cmd *cobra.Command) {
243244
if cmd.Flag(flagDisableMultiCluster).Changed && m.DisableMultiCluster {
244245
m.StaticConfig.ClusterProviderStrategy = config.ClusterProviderDisabled
245246
}
246-
if cmd.Flag(flagKialiUrl).Changed {
247-
m.StaticConfig.KialiOptions.Url = m.KialiOptions.Url
248-
}
249-
if cmd.Flag(flagKialiInsecure).Changed {
250-
m.StaticConfig.KialiOptions.Insecure = m.KialiOptions.Insecure
247+
if cmd.Flag(flagKialiUrl).Changed || cmd.Flag(flagKialiInsecure).Changed {
248+
m.StaticConfig.SetToolsetConfig("kiali", &kiali.Config{Url: m.KialiOptions.Url, Insecure: m.KialiOptions.Insecure})
251249
}
252250
}
253251

@@ -294,9 +292,15 @@ func (m *MCPServerOptions) Validate() error {
294292
klog.Warningf("authorization-url is using http://, this is not recommended production use")
295293
}
296294
}
297-
/* If Kiali tools are enabled, validate the Kiali URL */
298-
if slices.Contains(m.StaticConfig.Toolsets, "kiali") && strings.TrimSpace(m.StaticConfig.KialiOptions.Url) == "" {
299-
return fmt.Errorf("kiali-url is required when kiali tools are enabled")
295+
/* If Kiali tools are enabled, validate Kiali toolset configuration */
296+
if slices.Contains(m.StaticConfig.Toolsets, "kiali") {
297+
cfg, ok := m.StaticConfig.GetToolsetConfig("kiali")
298+
if !ok {
299+
return fmt.Errorf("kiali configuration is required when kiali tools are enabled")
300+
}
301+
if err := cfg.Validate(); err != nil {
302+
return fmt.Errorf("invalid kiali configuration: %w", err)
303+
}
300304
}
301305
return nil
302306
}

0 commit comments

Comments
 (0)