Skip to content

Commit a035295

Browse files
authored
Merge branch 'main' into introduce-jwks-url
2 parents ee8b555 + ca0aa46 commit a035295

21 files changed

+180
-91
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
_output/
12
.idea/
23
.vscode/
34
.docusaurus/

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ LD_FLAGS = -s -w \
1515
-X '$(PACKAGE)/pkg/version.BinaryName=$(BINARY_NAME)'
1616
COMMON_BUILD_ARGS = -ldflags "$(LD_FLAGS)"
1717

18+
GOLANGCI_LINT = $(shell pwd)/_output/tools/bin/golangci-lint
19+
GOLANGCI_LINT_VERSION ?= v2.2.2
20+
1821
# NPM version should not append the -dirty flag
1922
NPM_VERSION ?= $(shell echo $(shell git describe --tags --always) | sed 's/^v//')
2023
OSES = darwin linux windows
@@ -97,3 +100,14 @@ format: ## Format the code
97100
.PHONY: tidy
98101
tidy: ## Tidy up the go modules
99102
go mod tidy
103+
104+
.PHONY: golangci-lint
105+
golangci-lint: ## Download and install golangci-lint if not already installed
106+
@[ -f $(GOLANGCI_LINT) ] || { \
107+
set -e ;\
108+
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\
109+
}
110+
111+
.PHONY: lint
112+
lint: golangci-lint ## Lint the code
113+
$(GOLANGCI_LINT) run --verbose --print-resources-usage

pkg/helm/helm.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@ func (h *Helm) Install(ctx context.Context, chart string, values map[string]inte
4545
install.Timeout = 5 * time.Minute
4646
install.DryRun = false
4747

48-
chartRequested, err := install.ChartPathOptions.LocateChart(chart, cli.New())
48+
chartRequested, err := install.LocateChart(chart, cli.New())
4949
if err != nil {
5050
return "", err
5151
}
5252
chartLoaded, err := loader.Load(chartRequested)
53+
if err != nil {
54+
return "", err
55+
}
5356

5457
installedRelease, err := install.RunWithContext(ctx, chartLoaded, values)
5558
if err != nil {

pkg/http/http_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7+
"flag"
78
"fmt"
89
"net"
910
"net/http"
1011
"os"
1112
"path/filepath"
13+
"regexp"
1214
"strings"
1315
"testing"
1416
"time"
@@ -47,7 +49,10 @@ func (c *httpContext) beforeEach() {
4749
_ = os.Setenv("KUBECONFIG", kubeConfig)
4850
// Capture logging
4951
c.klogState = klog.CaptureState()
50-
klog.SetLogger(textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1), textlogger.Output(&c.logBuffer))))
52+
flags := flag.NewFlagSet("test", flag.ContinueOnError)
53+
klog.InitFlags(flags)
54+
_ = flags.Set("v", "5")
55+
klog.SetLogger(textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(5), textlogger.Output(&c.logBuffer))))
5156
// Start server in random port
5257
ln, err := net.Listen("tcp", "0.0.0.0:0")
5358
if err != nil {
@@ -247,3 +252,29 @@ func TestWellKnownOAuthProtectedResource(t *testing.T) {
247252
})
248253
})
249254
}
255+
256+
func TestMiddlewareLogging(t *testing.T) {
257+
testCase(t, func(ctx *httpContext) {
258+
_, _ = http.Get(fmt.Sprintf("http://%s/.well-known/oauth-protected-resource", ctx.httpAddress))
259+
t.Run("Logs HTTP requests and responses", func(t *testing.T) {
260+
if !strings.Contains(ctx.logBuffer.String(), "GET /.well-known/oauth-protected-resource 200") {
261+
t.Errorf("Expected log entry for GET /.well-known/oauth-protected-resource, got: %s", ctx.logBuffer.String())
262+
}
263+
})
264+
t.Run("Logs HTTP request duration", func(t *testing.T) {
265+
expected := `"GET /.well-known/oauth-protected-resource 200 (.+)"`
266+
m := regexp.MustCompile(expected).FindStringSubmatch(ctx.logBuffer.String())
267+
if len(m) != 2 {
268+
t.Fatalf("Expected log entry to contain duration, got %s", ctx.logBuffer.String())
269+
}
270+
duration, err := time.ParseDuration(m[1])
271+
if err != nil {
272+
t.Fatalf("Failed to parse duration from log entry: %v", err)
273+
}
274+
if duration < 0 {
275+
t.Errorf("Expected duration to be non-negative, got %v", duration)
276+
}
277+
})
278+
})
279+
280+
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,16 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
119119
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
120120
cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled")
121121
cmd.Flags().BoolVar(&o.RequireOAuth, "require-oauth", o.RequireOAuth, "If true, requires OAuth authorization as defined in the Model Context Protocol (MCP) specification. This flag is ignored if transport type is stdio")
122-
cmd.Flags().MarkHidden("require-oauth")
122+
_ = cmd.Flags().MarkHidden("require-oauth")
123123
cmd.Flags().StringVar(&o.AuthorizationURL, "authorization-url", o.AuthorizationURL, "OAuth authorization server URL for protected resource endpoint. If not provided, the Kubernetes API server host will be used. Only valid if require-oauth is enabled.")
124-
cmd.Flags().MarkHidden("authorization-url")
124+
_ = cmd.Flags().MarkHidden("authorization-url")
125125
cmd.Flags().StringVar(&o.JwksURL, "jwks-url", o.JwksURL, "OAuth JWKS server URL for protected resource endpoint. Only valid if require-oauth is enabled.")
126-
cmd.Flags().MarkHidden("jwks-url")
126+
_ = cmd.Flags().MarkHidden("jwks-url")
127127
cmd.Flags().StringVar(&o.ServerURL, "server-url", o.ServerURL, "Server URL of this application. Optional. If set, this url will be served in protected resource metadata endpoint and tokens will be validated with this audience. If not set, expected audience is kubernetes-mcp-server. Only valid if require-oauth is enabled.")
128-
cmd.Flags().MarkHidden("server-url")
128+
_ = cmd.Flags().MarkHidden("server-url")
129129
cmd.Flags().StringVar(&o.CertificateAuthority, "certificate-authority", o.CertificateAuthority, "Certificate authority path to verify certificates. Optional. Only valid if require-oauth is enabled.")
130-
cmd.Flags().MarkHidden("certificate-authority")
131-
130+
_ = cmd.Flags().MarkHidden("certificate-authority")
131+
132132
return cmd
133133
}
134134

@@ -257,11 +257,11 @@ func (m *MCPServerOptions) Validate() error {
257257
func (m *MCPServerOptions) Run() error {
258258
profile := mcp.ProfileFromString(m.Profile)
259259
if profile == nil {
260-
return fmt.Errorf("Invalid profile name: %s, valid names are: %s\n", m.Profile, strings.Join(mcp.ProfileNames, ", "))
260+
return fmt.Errorf("invalid profile name: %s, valid names are: %s", m.Profile, strings.Join(mcp.ProfileNames, ", "))
261261
}
262262
listOutput := output.FromString(m.StaticConfig.ListOutput)
263263
if listOutput == nil {
264-
return fmt.Errorf("Invalid output name: %s, valid names are: %s\n", m.StaticConfig.ListOutput, strings.Join(output.Names, ", "))
264+
return fmt.Errorf("invalid output name: %s, valid names are: %s", m.StaticConfig.ListOutput, strings.Join(output.Names, ", "))
265265
}
266266
klog.V(1).Info("Starting kubernetes-mcp-server")
267267
klog.V(1).Infof(" - Config: %s", m.ConfigPath)
@@ -314,7 +314,7 @@ func (m *MCPServerOptions) Run() error {
314314
StaticConfig: m.StaticConfig,
315315
})
316316
if err != nil {
317-
return fmt.Errorf("Failed to initialize MCP server: %w\n", err)
317+
return fmt.Errorf("failed to initialize MCP server: %w", err)
318318
}
319319
defer mcpServer.Close()
320320

pkg/kubernetes/configuration.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func (m *Manager) ConfigurationView(minify bool) (runtime.Object, error) {
106106
return nil, err
107107
}
108108
}
109+
//nolint:staticcheck
109110
if err = clientcmdapi.FlattenConfig(&cfg); err != nil {
110111
// ignore error
111112
//return "", err

pkg/kubernetes/impersonate_roundtripper.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package kubernetes
22

33
import "net/http"
44

5+
// nolint:unused
56
type impersonateRoundTripper struct {
67
delegate http.RoundTripper
78
}
89

10+
// nolint:unused
911
func (irt *impersonateRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
1012
// TODO: Solution won't work with discoveryclient which uses context.TODO() instead of the passed-in context
1113
if v, ok := req.Context().Value(OAuthAuthorizationHeader).(string); ok {

pkg/kubernetes/kubernetes.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
2727
)
2828

29+
type HeaderKey string
30+
2931
const (
30-
CustomAuthorizationHeader = "kubernetes-authorization"
31-
OAuthAuthorizationHeader = "Authorization"
32+
CustomAuthorizationHeader = HeaderKey("kubernetes-authorization")
33+
OAuthAuthorizationHeader = HeaderKey("Authorization")
3234

3335
CustomUserAgent = "kubernetes-mcp-server/bearer-token-auth"
3436
)
@@ -155,10 +157,10 @@ func (m *Manager) Derived(ctx context.Context) (*Kubernetes, error) {
155157
APIPath: m.cfg.APIPath,
156158
// Copy only server verification TLS settings (CA bundle and server name)
157159
TLSClientConfig: rest.TLSClientConfig{
158-
Insecure: m.cfg.TLSClientConfig.Insecure,
159-
ServerName: m.cfg.TLSClientConfig.ServerName,
160-
CAFile: m.cfg.TLSClientConfig.CAFile,
161-
CAData: m.cfg.TLSClientConfig.CAData,
160+
Insecure: m.cfg.Insecure,
161+
ServerName: m.cfg.ServerName,
162+
CAFile: m.cfg.CAFile,
163+
CAData: m.cfg.CAData,
162164
},
163165
BearerToken: strings.TrimPrefix(authorization, "Bearer "),
164166
// pass custom UserAgent to identify the client

pkg/kubernetes/kubernetes_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,17 @@ users:
137137
t.Errorf("expected Timeout %v, got %v", originalCfg.Timeout, derivedCfg.Timeout)
138138
}
139139

140-
if derivedCfg.TLSClientConfig.Insecure != originalCfg.TLSClientConfig.Insecure {
141-
t.Errorf("expected TLS Insecure %v, got %v", originalCfg.TLSClientConfig.Insecure, derivedCfg.TLSClientConfig.Insecure)
140+
if derivedCfg.Insecure != originalCfg.Insecure {
141+
t.Errorf("expected TLS Insecure %v, got %v", originalCfg.Insecure, derivedCfg.Insecure)
142142
}
143-
if derivedCfg.TLSClientConfig.ServerName != originalCfg.TLSClientConfig.ServerName {
144-
t.Errorf("expected TLS ServerName %s, got %s", originalCfg.TLSClientConfig.ServerName, derivedCfg.TLSClientConfig.ServerName)
143+
if derivedCfg.ServerName != originalCfg.ServerName {
144+
t.Errorf("expected TLS ServerName %s, got %s", originalCfg.ServerName, derivedCfg.ServerName)
145145
}
146-
if derivedCfg.TLSClientConfig.CAFile != originalCfg.TLSClientConfig.CAFile {
147-
t.Errorf("expected TLS CAFile %s, got %s", originalCfg.TLSClientConfig.CAFile, derivedCfg.TLSClientConfig.CAFile)
146+
if derivedCfg.CAFile != originalCfg.CAFile {
147+
t.Errorf("expected TLS CAFile %s, got %s", originalCfg.CAFile, derivedCfg.CAFile)
148148
}
149-
if string(derivedCfg.TLSClientConfig.CAData) != string(originalCfg.TLSClientConfig.CAData) {
150-
t.Errorf("expected TLS CAData %s, got %s", string(originalCfg.TLSClientConfig.CAData), string(derivedCfg.TLSClientConfig.CAData))
149+
if string(derivedCfg.CAData) != string(originalCfg.CAData) {
150+
t.Errorf("expected TLS CAData %s, got %s", string(originalCfg.CAData), string(derivedCfg.CAData))
151151
}
152152

153153
if derivedCfg.BearerToken != testBearerToken {
@@ -160,17 +160,17 @@ users:
160160
// Verify that sensitive fields are NOT copied to prevent credential leakage
161161
// The derived config should only use the bearer token from the Authorization header
162162
// and not inherit any authentication credentials from the original kubeconfig
163-
if derivedCfg.TLSClientConfig.CertFile != "" {
164-
t.Errorf("expected TLS CertFile to be empty, got %s", derivedCfg.TLSClientConfig.CertFile)
163+
if derivedCfg.CertFile != "" {
164+
t.Errorf("expected TLS CertFile to be empty, got %s", derivedCfg.CertFile)
165165
}
166-
if derivedCfg.TLSClientConfig.KeyFile != "" {
167-
t.Errorf("expected TLS KeyFile to be empty, got %s", derivedCfg.TLSClientConfig.KeyFile)
166+
if derivedCfg.KeyFile != "" {
167+
t.Errorf("expected TLS KeyFile to be empty, got %s", derivedCfg.KeyFile)
168168
}
169-
if len(derivedCfg.TLSClientConfig.CertData) != 0 {
170-
t.Errorf("expected TLS CertData to be empty, got %v", derivedCfg.TLSClientConfig.CertData)
169+
if len(derivedCfg.CertData) != 0 {
170+
t.Errorf("expected TLS CertData to be empty, got %v", derivedCfg.CertData)
171171
}
172-
if len(derivedCfg.TLSClientConfig.KeyData) != 0 {
173-
t.Errorf("expected TLS KeyData to be empty, got %v", derivedCfg.TLSClientConfig.KeyData)
172+
if len(derivedCfg.KeyData) != 0 {
173+
t.Errorf("expected TLS KeyData to be empty, got %v", derivedCfg.KeyData)
174174
}
175175

176176
if derivedCfg.Username != "" {

pkg/mcp/common_test.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package mcp
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
7+
"flag"
68
"fmt"
9+
"k8s.io/klog/v2"
10+
"k8s.io/klog/v2/textlogger"
711
"net/http/httptest"
812
"os"
913
"path/filepath"
1014
"runtime"
15+
"strconv"
1116
"testing"
1217
"time"
1318

@@ -25,12 +30,9 @@ import (
2530
apiextensionsv1spec "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2631
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
2732
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28-
"k8s.io/apimachinery/pkg/runtime/schema"
29-
"k8s.io/apimachinery/pkg/runtime/serializer"
3033
"k8s.io/apimachinery/pkg/watch"
3134
"k8s.io/client-go/kubernetes"
3235
"k8s.io/client-go/rest"
33-
"k8s.io/client-go/scale"
3436
"k8s.io/client-go/tools/clientcmd"
3537
"k8s.io/client-go/tools/clientcmd/api"
3638
toolswatch "k8s.io/client-go/tools/watch"
@@ -71,7 +73,7 @@ func TestMain(m *testing.M) {
7173
}
7274
envTestEnv.CheckCoherence()
7375
workflows.Use{}.Do(envTestEnv)
74-
versionDir := envTestEnv.Platform.Platform.BaseName(*envTestEnv.Version.AsConcrete())
76+
versionDir := envTestEnv.Platform.BaseName(*envTestEnv.Version.AsConcrete())
7577
envTest = &envtest.Environment{
7678
BinaryAssetsDirectory: filepath.Join(envTestDir, "k8s", versionDir),
7779
}
@@ -100,6 +102,7 @@ func TestMain(m *testing.M) {
100102
type mcpContext struct {
101103
profile Profile
102104
listOutput output.Output
105+
logLevel int
103106

104107
staticConfig *config.StaticConfig
105108
clientOptions []transport.ClientOption
@@ -111,6 +114,8 @@ type mcpContext struct {
111114
mcpServer *Server
112115
mcpHttpServer *httptest.Server
113116
mcpClient *client.Client
117+
klogState klog.State
118+
logBuffer bytes.Buffer
114119
}
115120

116121
func (c *mcpContext) beforeEach(t *testing.T) {
@@ -133,6 +138,13 @@ func (c *mcpContext) beforeEach(t *testing.T) {
133138
if c.before != nil {
134139
c.before(c)
135140
}
141+
// Set up logging
142+
c.klogState = klog.CaptureState()
143+
flags := flag.NewFlagSet("test", flag.ContinueOnError)
144+
klog.InitFlags(flags)
145+
_ = flags.Set("v", strconv.Itoa(c.logLevel))
146+
klog.SetLogger(textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(c.logLevel), textlogger.Output(&c.logBuffer))))
147+
// MCP Server
136148
if c.mcpServer, err = NewServer(Configuration{
137149
Profile: c.profile,
138150
ListOutput: c.listOutput,
@@ -146,6 +158,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
146158
t.Fatal(err)
147159
return
148160
}
161+
// MCP Client
149162
if err = c.mcpClient.Start(c.ctx); err != nil {
150163
t.Fatal(err)
151164
return
@@ -168,6 +181,7 @@ func (c *mcpContext) afterEach() {
168181
c.mcpServer.Close()
169182
_ = c.mcpClient.Close()
170183
c.mcpHttpServer.Close()
184+
c.klogState.Restore()
171185
}
172186

173187
func testCase(t *testing.T, test func(c *mcpContext)) {
@@ -190,9 +204,9 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
190204
fakeConfig.AuthInfos["additional-auth"] = api.NewAuthInfo()
191205
if rc != nil {
192206
fakeConfig.Clusters["fake"].Server = rc.Host
193-
fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.TLSClientConfig.CAData
194-
fakeConfig.AuthInfos["fake"].ClientKeyData = rc.TLSClientConfig.KeyData
195-
fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.TLSClientConfig.CertData
207+
fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.CAData
208+
fakeConfig.AuthInfos["fake"].ClientKeyData = rc.KeyData
209+
fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.CertData
196210
}
197211
fakeConfig.Contexts["fake-context"] = api.NewContext()
198212
fakeConfig.Contexts["fake-context"].Cluster = "fake"
@@ -264,18 +278,6 @@ func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset {
264278
return kubernetes.NewForConfigOrDie(envTestRestConfig)
265279
}
266280

267-
func (c *mcpContext) newRestClient(groupVersion *schema.GroupVersion) *rest.RESTClient {
268-
config := *envTestRestConfig
269-
config.GroupVersion = groupVersion
270-
config.APIPath = "/api"
271-
config.NegotiatedSerializer = serializer.NewCodecFactory(scale.NewScaleConverter().Scheme()).WithoutConversion()
272-
rc, err := rest.RESTClientFor(&config)
273-
if err != nil {
274-
panic(err)
275-
}
276-
return rc
277-
}
278-
279281
// newApiExtensionsClient creates a new ApiExtensions client with the envTest kubeconfig
280282
func (c *mcpContext) newApiExtensionsClient() *apiextensionsv1.ApiextensionsV1Client {
281283
return apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
@@ -286,6 +288,9 @@ func (c *mcpContext) crdApply(resource string) error {
286288
apiExtensionsV1Client := c.newApiExtensionsClient()
287289
var crd = &apiextensionsv1spec.CustomResourceDefinition{}
288290
err := json.Unmarshal([]byte(resource), crd)
291+
if err != nil {
292+
return fmt.Errorf("failed to create CRD %v", err)
293+
}
289294
_, err = apiExtensionsV1Client.CustomResourceDefinitions().Create(c.ctx, crd, metav1.CreateOptions{})
290295
if err != nil {
291296
return fmt.Errorf("failed to create CRD %v", err)

0 commit comments

Comments
 (0)