Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions pkg/api/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package config

const (
ClusterProviderKubeConfig = "kubeconfig"
ClusterProviderInCluster = "in-cluster"
ClusterProviderDisabled = "disabled"
)

type AuthProvider interface {
// IsRequireOAuth indicates whether OAuth authentication is required.
IsRequireOAuth() bool
}

type ClusterProvider interface {
// GetClusterProviderStrategy returns the cluster provider strategy (if configured).
GetClusterProviderStrategy() string
// GetKubeConfigPath returns the path to the kubeconfig file (if configured).
GetKubeConfigPath() string
}

// Extended is the interface that all configuration extensions must implement.
// Each extended config manager registers a factory function to parse its config from TOML primitives
type Extended interface {
// Validate validates the extended configuration. Returns an error if the configuration is invalid.
Validate() error
}

type ExtendedProvider interface {
// GetProviderConfig returns the extended configuration for the given provider strategy.
// The boolean return value indicates whether the configuration was found.
GetProviderConfig(strategy string) (Extended, bool)
// GetToolsetConfig returns the extended configuration for the given toolset name.
// The boolean return value indicates whether the configuration was found.
GetToolsetConfig(name string) (Extended, bool)
}

type GroupVersionKind struct {
Group string `json:"group" toml:"group"`
Version string `json:"version" toml:"version"`
Kind string `json:"kind,omitempty" toml:"kind,omitempty"`
}

type DeniedResourcesProvider interface {
// GetDeniedResources returns a list of GroupVersionKinds that are denied.
GetDeniedResources() []GroupVersionKind
}

type BaseConfig interface {
AuthProvider
ClusterProvider
DeniedResourcesProvider
ExtendedProvider
}
43 changes: 25 additions & 18 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,18 @@ import (
"strings"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"k8s.io/klog/v2"
)

const (
DefaultDropInConfigDir = "conf.d"
)

const (
ClusterProviderKubeConfig = "kubeconfig"
ClusterProviderInCluster = "in-cluster"
ClusterProviderDisabled = "disabled"
)

// StaticConfig is the configuration for the server.
// It allows to configure server specific settings and tools to be enabled or disabled.
type StaticConfig struct {
DeniedResources []GroupVersionKind `toml:"denied_resources"`
DeniedResources []configapi.GroupVersionKind `toml:"denied_resources"`

LogLevel int `toml:"log_level,omitzero"`
Port string `toml:"port,omitempty"`
Expand Down Expand Up @@ -80,19 +75,15 @@ type StaticConfig struct {
ToolsetConfigs map[string]toml.Primitive `toml:"toolset_configs,omitempty"`

// Internal: parsed provider configs (not exposed to TOML package)
parsedClusterProviderConfigs map[string]Extended
parsedClusterProviderConfigs map[string]configapi.Extended
// Internal: parsed toolset configs (not exposed to TOML package)
parsedToolsetConfigs map[string]Extended
parsedToolsetConfigs map[string]configapi.Extended

// Internal: the config.toml directory, to help resolve relative file paths
configDirPath string
}

type GroupVersionKind struct {
Group string `toml:"group"`
Version string `toml:"version"`
Kind string `toml:"kind,omitempty"`
}
var _ configapi.BaseConfig = (*StaticConfig)(nil)

type ReadConfigOpt func(cfg *StaticConfig)

Expand Down Expand Up @@ -292,13 +283,29 @@ func ReadToml(configData []byte, opts ...ReadConfigOpt) (*StaticConfig, error) {
return config, nil
}

func (c *StaticConfig) GetProviderConfig(strategy string) (Extended, bool) {
config, ok := c.parsedClusterProviderConfigs[strategy]
func (c *StaticConfig) GetClusterProviderStrategy() string {
return c.ClusterProviderStrategy
}

func (c *StaticConfig) GetDeniedResources() []configapi.GroupVersionKind {
return c.DeniedResources
}

return config, ok
func (c *StaticConfig) GetKubeConfigPath() string {
return c.KubeConfig
}

func (c *StaticConfig) GetToolsetConfig(name string) (Extended, bool) {
func (c *StaticConfig) GetProviderConfig(strategy string) (configapi.Extended, bool) {
cfg, ok := c.parsedClusterProviderConfigs[strategy]

return cfg, ok
}

func (c *StaticConfig) GetToolsetConfig(name string) (configapi.Extended, bool) {
cfg, ok := c.parsedToolsetConfigs[name]
return cfg, ok
}

func (c *StaticConfig) IsRequireOAuth() bool {
return c.RequireOAuth
}
11 changes: 6 additions & 5 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/stretchr/testify/suite"
)

Expand Down Expand Up @@ -136,11 +137,11 @@ func (s *ConfigSuite) TestReadConfigValid() {
s.Run("denied_resources", func() {
s.Require().Lenf(config.DeniedResources, 2, "Expected 2 denied resources, got %d", len(config.DeniedResources))
s.Run("contains apps/v1/Deployment", func() {
s.Contains(config.DeniedResources, GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
"Expected denied resources to contain apps/v1/Deployment")
})
s.Run("contains rbac.authorization.k8s.io/v1/Role", func() {
s.Contains(config.DeniedResources, GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
s.Contains(config.DeniedResources, configapi.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
"Expected denied resources to contain rbac.authorization.k8s.io/v1/Role")
})
})
Expand Down Expand Up @@ -777,16 +778,16 @@ func (s *ConfigSuite) TestDropInWithDeniedResources() {

s.Run("drop-in replaces denied_resources array", func() {
s.Len(config.DeniedResources, 2, "denied_resources should have 2 entries from drop-in")
s.Contains(config.DeniedResources, GroupVersionKind{
s.Contains(config.DeniedResources, configapi.GroupVersionKind{
Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole",
})
s.Contains(config.DeniedResources, GroupVersionKind{
s.Contains(config.DeniedResources, configapi.GroupVersionKind{
Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding",
})
})

s.Run("original denied_resources from main config are replaced", func() {
s.NotContains(config.DeniedResources, GroupVersionKind{
s.NotContains(config.DeniedResources, configapi.GroupVersionKind{
Group: "apps", Version: "v1", Kind: "Deployment",
}, "original entry should be replaced by drop-in")
})
Expand Down
15 changes: 5 additions & 10 deletions pkg/config/extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ import (
"fmt"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
)

// Extended is the interface that all configuration extensions must implement.
// Each extended config manager registers a factory function to parse its config from TOML primitives
type Extended interface {
Validate() error
}

type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error)
type ExtendedConfigParser func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error)

type extendedConfigRegistry struct {
parsers map[string]ExtendedConfigParser
Expand All @@ -33,11 +28,11 @@ func (r *extendedConfigRegistry) register(name string, parser ExtendedConfigPars
r.parsers[name] = parser
}

func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]Extended, error) {
func (r *extendedConfigRegistry) parse(ctx context.Context, metaData toml.MetaData, configs map[string]toml.Primitive) (map[string]configapi.Extended, error) {
if len(configs) == 0 {
return make(map[string]Extended), nil
return make(map[string]configapi.Extended), nil
}
parsedConfigs := make(map[string]Extended, len(configs))
parsedConfigs := make(map[string]configapi.Extended, len(configs))

for name, primitive := range configs {
parser, ok := r.parsers[name]
Expand Down
11 changes: 6 additions & 5 deletions pkg/config/provider_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/stretchr/testify/suite"
)

Expand All @@ -31,7 +32,7 @@ type ProviderConfigForTest struct {
IntProp int `toml:"int_prop"`
}

var _ Extended = (*ProviderConfigForTest)(nil)
var _ configapi.Extended = (*ProviderConfigForTest)(nil)

func (p *ProviderConfigForTest) Validate() error {
if p.StrProp == "force-error" {
Expand All @@ -40,7 +41,7 @@ func (p *ProviderConfigForTest) Validate() error {
return nil
}

func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
func providerConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
var providerConfigForTest ProviderConfigForTest
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
return nil, err
Expand Down Expand Up @@ -129,7 +130,7 @@ func (s *ProviderConfigSuite) TestReadConfigUnregisteredProviderConfig() {
}

func (s *ProviderConfigSuite) TestReadConfigParserError() {
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
return nil, errors.New("parser error forced by test")
})
invalidConfigPath := s.writeConfig(`
Expand All @@ -152,7 +153,7 @@ func (s *ProviderConfigSuite) TestReadConfigParserError() {

func (s *ProviderConfigSuite) TestConfigDirPathInContext() {
var capturedDirPath string
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var providerConfigForTest ProviderConfigForTest
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
Expand Down Expand Up @@ -328,7 +329,7 @@ func (s *ProviderConfigSuite) TestStandaloneConfigDirWithExtendedConfig() {
func (s *ProviderConfigSuite) TestConfigDirPathInContextStandalone() {
// Test that configDirPath is correctly set in context for standalone --config-dir
var capturedDirPath string
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
RegisterProviderConfig("test", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var providerConfigForTest ProviderConfigForTest
if err := md.PrimitiveDecode(primitive, &providerConfigForTest); err != nil {
Expand Down
9 changes: 5 additions & 4 deletions pkg/config/toolset_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/stretchr/testify/suite"
)

Expand All @@ -31,7 +32,7 @@ type ToolsetConfigForTest struct {
Timeout int `toml:"timeout"`
}

var _ Extended = (*ToolsetConfigForTest)(nil)
var _ configapi.Extended = (*ToolsetConfigForTest)(nil)

func (t *ToolsetConfigForTest) Validate() error {
if t.Endpoint == "force-error" {
Expand All @@ -40,7 +41,7 @@ func (t *ToolsetConfigForTest) Validate() error {
return nil
}

func toolsetConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
func toolsetConfigForTestParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
var toolsetConfigForTest ToolsetConfigForTest
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
return nil, err
Expand Down Expand Up @@ -127,7 +128,7 @@ func (s *ToolsetConfigSuite) TestReadConfigUnregisteredToolsetConfig() {

func (s *ToolsetConfigSuite) TestConfigDirPathInContext() {
var capturedDirPath string
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var toolsetConfigForTest ToolsetConfigForTest
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
Expand Down Expand Up @@ -299,7 +300,7 @@ func (s *ToolsetConfigSuite) TestStandaloneConfigDirWithExtendedConfig() {
func (s *ToolsetConfigSuite) TestConfigDirPathInContextStandalone() {
// Test that configDirPath is correctly set in context for standalone --config-dir
var capturedDirPath string
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (Extended, error) {
RegisterToolsetConfig("test-toolset", func(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
capturedDirPath = ConfigDirPathFromContext(ctx)
var toolsetConfigForTest ToolsetConfigForTest
if err := md.PrimitiveDecode(primitive, &toolsetConfigForTest); err != nil {
Expand Down
13 changes: 7 additions & 6 deletions pkg/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"

configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/containers/kubernetes-mcp-server/pkg/mcp"
)
Expand Down Expand Up @@ -240,7 +241,7 @@ func TestHealthCheck(t *testing.T) {
})
})
// Health exposed even when require Authorization
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: configapi.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
resp, err := http.Get(fmt.Sprintf("http://%s/healthz", ctx.HttpAddress))
if err != nil {
t.Fatalf("Failed to get health check endpoint with OAuth: %v", err)
Expand All @@ -261,7 +262,7 @@ func TestWellKnownReverseProxy(t *testing.T) {
".well-known/openid-configuration",
}
// With No Authorization URL configured
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: config.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
testCaseWithContext(t, &httpContext{StaticConfig: &config.StaticConfig{RequireOAuth: true, ValidateToken: true, ClusterProviderStrategy: configapi.ClusterProviderKubeConfig}}, func(ctx *httpContext) {
for _, path := range cases {
resp, err := http.Get(fmt.Sprintf("http://%s/%s", ctx.HttpAddress, path))
t.Cleanup(func() { _ = resp.Body.Close() })
Expand All @@ -285,7 +286,7 @@ func TestWellKnownReverseProxy(t *testing.T) {
AuthorizationURL: invalidPayloadServer.URL,
RequireOAuth: true,
ValidateToken: true,
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
ClusterProviderStrategy: configapi.ClusterProviderKubeConfig,
}
testCaseWithContext(t, &httpContext{StaticConfig: invalidPayloadConfig}, func(ctx *httpContext) {
for _, path := range cases {
Expand Down Expand Up @@ -315,7 +316,7 @@ func TestWellKnownReverseProxy(t *testing.T) {
AuthorizationURL: testServer.URL,
RequireOAuth: true,
ValidateToken: true,
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
ClusterProviderStrategy: configapi.ClusterProviderKubeConfig,
}
testCaseWithContext(t, &httpContext{StaticConfig: staticConfig}, func(ctx *httpContext) {
for _, path := range cases {
Expand Down Expand Up @@ -365,7 +366,7 @@ func TestWellKnownHeaderPropagation(t *testing.T) {
AuthorizationURL: testServer.URL,
RequireOAuth: true,
ValidateToken: true,
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
ClusterProviderStrategy: configapi.ClusterProviderKubeConfig,
}
testCaseWithContext(t, &httpContext{StaticConfig: staticConfig}, func(ctx *httpContext) {
for _, path := range cases {
Expand Down Expand Up @@ -479,7 +480,7 @@ func TestWellKnownOverrides(t *testing.T) {
AuthorizationURL: testServer.URL,
RequireOAuth: true,
ValidateToken: true,
ClusterProviderStrategy: config.ClusterProviderKubeConfig,
ClusterProviderStrategy: configapi.ClusterProviderKubeConfig,
}
// With Dynamic Client Registration disabled
disableDynamicRegistrationConfig := baseConfig
Expand Down
5 changes: 3 additions & 2 deletions pkg/kiali/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/BurntSushi/toml"
configapi "github.com/containers/kubernetes-mcp-server/pkg/api/config"
"github.com/containers/kubernetes-mcp-server/pkg/config"
)

Expand All @@ -20,7 +21,7 @@ type Config struct {
CertificateAuthority string `toml:"certificate_authority,omitempty"`
}

var _ config.Extended = (*Config)(nil)
var _ configapi.Extended = (*Config)(nil)

func (c *Config) Validate() error {
if c == nil {
Expand All @@ -45,7 +46,7 @@ func (c *Config) Validate() error {
return nil
}

func kialiToolsetParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (config.Extended, error) {
func kialiToolsetParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (configapi.Extended, error) {
var cfg Config
if err := md.PrimitiveDecode(primitive, &cfg); err != nil {
return nil, err
Expand Down
Loading