From 5fcf6c49a7dfa0eaf9ea15e747317dbbaddd78aa Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:30:30 +0000 Subject: [PATCH 1/3] ports authz-configmap-ref e2e test to int test we want to move as much e2e tests into integration tests as possible where resource creation and assertoin is concerned. this PR moves the authz configmap e2e test into an integration test and then verifies that the runconfig has the correct authz config. we also add an assertion to ensure teh proxyrunner deployment has the runconfig volume and volume mount Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- .../mcpserver_runconfig_integration_test.go | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/cmd/thv-operator/test-integration/mcp-server/mcpserver_runconfig_integration_test.go b/cmd/thv-operator/test-integration/mcp-server/mcpserver_runconfig_integration_test.go index 1f4c597d3..cf6cfb3fc 100644 --- a/cmd/thv-operator/test-integration/mcp-server/mcpserver_runconfig_integration_test.go +++ b/cmd/thv-operator/test-integration/mcp-server/mcpserver_runconfig_integration_test.go @@ -8,6 +8,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -182,6 +183,56 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() { Expect(runConfig.SchemaVersion).To(Equal(runner.CurrentSchemaVersion)) }) + It("Should create deployment with RunConfig volume mounts", func() { + // Wait for the deployment to be created + deployment := &appsv1.Deployment{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: mcpServerName, + Namespace: namespace, + }, deployment) + }, timeout, interval).Should(Succeed()) + + // Verify the deployment has the correct volume + var runconfigVolume *corev1.Volume + for i := range deployment.Spec.Template.Spec.Volumes { + vol := &deployment.Spec.Template.Spec.Volumes[i] + if vol.Name == "runconfig" { + runconfigVolume = vol + break + } + } + Expect(runconfigVolume).NotTo(BeNil(), "RunConfig volume should exist in deployment") + + // Verify the volume references the correct ConfigMap + Expect(runconfigVolume.ConfigMap).NotTo(BeNil()) + Expect(runconfigVolume.ConfigMap.LocalObjectReference.Name).To(Equal(configMapName)) + + // Find the toolhive container + var toolhiveContainer *corev1.Container + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + if container.Name == "toolhive" { + toolhiveContainer = container + break + } + } + Expect(toolhiveContainer).NotTo(BeNil(), "Toolhive container should exist") + + // Verify the volume mount exists in the toolhive container + var runconfigMount *corev1.VolumeMount + for i := range toolhiveContainer.VolumeMounts { + mount := &toolhiveContainer.VolumeMounts[i] + if mount.Name == "runconfig" { + runconfigMount = mount + break + } + } + Expect(runconfigMount).NotTo(BeNil(), "RunConfig volume mount should exist in toolhive container") + Expect(runconfigMount.MountPath).To(Equal("/etc/runconfig")) + Expect(runconfigMount.ReadOnly).To(BeTrue()) + }) + It("Should not update ConfigMap when MCPServer spec is unchanged", func() { // Get initial ConfigMap state initialConfigMap := &corev1.ConfigMap{} @@ -657,5 +708,132 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() { } Expect(authMiddlewareFound).To(BeTrue(), "Auth middleware should be present in middleware_configs") }) + + It("Should handle MCPServer with authorization ConfigMap reference", func() { + namespace := "authz-configmap-ns" + mcpServerName := "authz-configmap-server" + configMapName := mcpServerName + "-runconfig" + externalAuthzConfigMapName := "external-authz-config" + + // Create namespace + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + _ = k8sClient.Create(ctx, ns) + + // Create external authorization ConfigMap + authzConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: externalAuthzConfigMapName, + Namespace: namespace, + }, + Data: map[string]string{ + "authz.json": `{ + "version": "v1", + "type": "cedarv1", + "cedar": { + "policies": [ + "permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");", + "permit(principal, action == Action::\"get_prompt\", resource == Prompt::\"greeting\");", + "forbid(principal, action == Action::\"call_tool\", resource == Tool::\"sensitive_data\");" + ], + "entities_json": "[{\"uid\": {\"type\": \"User\", \"id\": \"user1\"}, \"attrs\": {\"name\": \"Alice\", \"role\": \"developer\"}},{\"uid\": {\"type\": \"User\", \"id\": \"admin\"}, \"attrs\": {\"name\": \"Bob\", \"role\": \"admin\"}}]" + } + }`, + }, + } + Expect(k8sClient.Create(ctx, authzConfigMap)).Should(Succeed()) + defer k8sClient.Delete(ctx, authzConfigMap) + + // Create MCPServer with ConfigMap authorization reference + mcpServer := &mcpv1alpha1.MCPServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: mcpServerName, + Namespace: namespace, + }, + Spec: mcpv1alpha1.MCPServerSpec{ + Image: "authz/mcp-server:latest", + Transport: "stdio", + ProxyPort: 8080, + AuthzConfig: &mcpv1alpha1.AuthzConfigRef{ + Type: mcpv1alpha1.AuthzConfigTypeConfigMap, + ConfigMap: &mcpv1alpha1.ConfigMapAuthzRef{ + Name: externalAuthzConfigMapName, + Key: "authz.json", + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed()) + defer k8sClient.Delete(ctx, mcpServer) + + // Wait for RunConfig ConfigMap to be created + configMap := &corev1.ConfigMap{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{ + Name: configMapName, + Namespace: namespace, + }, configMap) + }, timeout, interval).Should(Succeed()) + + // Verify ConfigMap has the expected label + Expect(configMap.Labels).To(HaveKeyWithValue("toolhive.stacklok.io/mcp-server", mcpServerName)) + + // Verify ConfigMap data contains runconfig.json + Expect(configMap.Data).To(HaveKey("runconfig.json")) + runConfigJSON := configMap.Data["runconfig.json"] + Expect(runConfigJSON).NotTo(BeEmpty()) + + // Parse and verify RunConfig content + var runConfig runner.RunConfig + err := json.Unmarshal([]byte(runConfigJSON), &runConfig) + Expect(err).NotTo(HaveOccurred()) + + // Verify authorization configuration was embedded from external ConfigMap + Expect(runConfig.AuthzConfig).NotTo(BeNil()) + Expect(runConfig.AuthzConfig.Version).To(Equal("v1")) + Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigTypeCedarV1)) + + // Verify Cedar configuration + Expect(runConfig.AuthzConfig.Cedar).NotTo(BeNil()) + + // Check policies are present + Expect(runConfig.AuthzConfig.Cedar.Policies).To(HaveLen(3)) + Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("call_tool")) + Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("weather")) + Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("get_prompt")) + Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("greeting")) + Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("forbid")) + Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("sensitive_data")) + + // Verify entities are embedded + Expect(runConfig.AuthzConfig.Cedar.EntitiesJSON).NotTo(BeEmpty()) + + // Parse entities to verify they're correctly embedded + var entities []interface{} + err = json.Unmarshal([]byte(runConfig.AuthzConfig.Cedar.EntitiesJSON), &entities) + Expect(err).NotTo(HaveOccurred()) + Expect(entities).To(HaveLen(2)) + + // Verify entity details + entity1 := entities[0].(map[string]interface{}) + uid1 := entity1["uid"].(map[string]interface{}) + Expect(uid1["type"]).To(Equal("User")) + Expect(uid1["id"]).To(Equal("user1")) + attrs1 := entity1["attrs"].(map[string]interface{}) + Expect(attrs1["name"]).To(Equal("Alice")) + Expect(attrs1["role"]).To(Equal("developer")) + + entity2 := entities[1].(map[string]interface{}) + uid2 := entity2["uid"].(map[string]interface{}) + Expect(uid2["type"]).To(Equal("User")) + Expect(uid2["id"]).To(Equal("admin")) + attrs2 := entity2["attrs"].(map[string]interface{}) + Expect(attrs2["name"]).To(Equal("Bob")) + Expect(attrs2["role"]).To(Equal("admin")) + }) }) }) From 6308d30710dee6610d2afa0da1e06b3c9753ccca Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:32:20 +0000 Subject: [PATCH 2/3] removes redundant authz-configmap-ref e2e test Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- .../assert-mcpserver-pod-running.yaml | 9 - .../assert-mcpserver-running.yaml | 7 - .../authz-configmap-ref/chainsaw-test.yaml | 167 ------------------ .../mcpserver-authz-ref.yaml | 27 --- 4 files changed, 210 deletions(-) delete mode 100644 test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-pod-running.yaml delete mode 100644 test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-running.yaml delete mode 100644 test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/chainsaw-test.yaml delete mode 100644 test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/mcpserver-authz-ref.yaml diff --git a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-pod-running.yaml b/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-pod-running.yaml deleted file mode 100644 index e564e1aa3..000000000 --- a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-pod-running.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - namespace: toolhive-system - labels: - app: mcpserver - app.kubernetes.io/instance: authz-configmap-ref-test -status: - phase: Running \ No newline at end of file diff --git a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-running.yaml b/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-running.yaml deleted file mode 100644 index 2562963e4..000000000 --- a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/assert-mcpserver-running.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 -kind: MCPServer -metadata: - name: authz-configmap-ref-test - namespace: toolhive-system -status: - phase: Running \ No newline at end of file diff --git a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/chainsaw-test.yaml b/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/chainsaw-test.yaml deleted file mode 100644 index 7cc23bde1..000000000 --- a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/chainsaw-test.yaml +++ /dev/null @@ -1,167 +0,0 @@ -apiVersion: chainsaw.kyverno.io/v1alpha1 -kind: Test -metadata: - name: authz-configmap-ref-test -spec: - description: Test that external ConfigMap-based authorization config is embedded into runconfig.json and used via volume mounting - steps: - - name: enable-configmap-mode - try: - - script: - content: | - echo "Setting TOOLHIVE_USE_CONFIGMAP=true on operator deployment..." - - kubectl patch deployment toolhive-operator -n toolhive-system --type='strategic' -p='{"spec":{"template":{"spec":{"containers":[{"name":"manager","env":[{"name":"TOOLHIVE_USE_CONFIGMAP","value":"true"}]}]}}}}' - - kubectl rollout status deployment/toolhive-operator -n toolhive-system --timeout=60s - - echo "Verifying TOOLHIVE_USE_CONFIGMAP environment variable is set..." - ENV_VAR=$(kubectl get deployment toolhive-operator -n toolhive-system -o jsonpath='{.spec.template.spec.containers[?(@.name=="manager")].env[?(@.name=="TOOLHIVE_USE_CONFIGMAP")].value}') - if [ "$ENV_VAR" = "true" ]; then - echo "✓ TOOLHIVE_USE_CONFIGMAP=true verified on operator deployment" - else - echo "✗ Failed to set TOOLHIVE_USE_CONFIGMAP environment variable" - exit 1 - fi - timeout: 120s - - - name: create-external-authz-configmap - try: - - script: - content: | - echo "Creating external authorization ConfigMap..." - kubectl apply -n toolhive-system -f - <<'EOF' - apiVersion: v1 - kind: ConfigMap - metadata: - name: external-authz-config - namespace: toolhive-system - data: - authz.json: | - { - "version": "v1", - "type": "cedarv1", - "cedar": { - "policies": [ - "permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");", - "permit(principal, action == Action::\"get_prompt\", resource == Prompt::\"greeting\");", - "forbid(principal, action == Action::\"call_tool\", resource == Tool::\"sensitive_data\");" - ], - "entities_json": "[{\"uid\": {\"type\": \"User\", \"id\": \"user1\"}, \"attrs\": {\"role\": \"viewer\"}}, {\"uid\": {\"type\": \"User\", \"id\": \"admin\"}, \"attrs\": {\"role\": \"admin\"}}]" - } - } - EOF - echo "✓ External authorization ConfigMap created" - timeout: 60s - - - name: create-mcpserver-with-authz-ref - try: - - apply: - file: mcpserver-authz-ref.yaml - - assert: - file: assert-mcpserver-running.yaml - timeout: 180s - - - name: verify-pod-running - try: - - assert: - file: assert-mcpserver-pod-running.yaml - timeout: 180s - - - name: verify-configmap-authz-config-embedded - try: - - script: - content: | - echo "Verifying RunConfig ConfigMap includes embedded authorization config..." - - for i in $(seq 1 20); do - if kubectl get configmap -n toolhive-system -l toolhive.stacklok.io/mcp-server=authz-configmap-ref-test >/dev/null 2>&1; then - echo "✓ RunConfig ConfigMap exists" - break - fi - echo " Waiting for runconfig ConfigMap... (attempt $i/20)" - sleep 3 - done - - CONFIGMAP_JSON=$(kubectl get configmap -n toolhive-system -l toolhive.stacklok.io/mcp-server=authz-configmap-ref-test -o jsonpath='{.items[0].data.runconfig\.json}' 2>/dev/null || echo "") - if [ -z "$CONFIGMAP_JSON" ]; then - echo "✗ ConfigMap does not contain runconfig.json data" - kubectl get configmap -n toolhive-system -l toolhive.stacklok.io/mcp-server=authz-configmap-ref-test -o yaml - exit 1 - fi - - echo "$CONFIGMAP_JSON" | jq -e '.authz_config' >/dev/null - VERSION=$(echo "$CONFIGMAP_JSON" | jq -r '.authz_config.version // empty') - TYPE=$(echo "$CONFIGMAP_JSON" | jq -r '.authz_config.type // empty') - test "$VERSION" = "v1" - test "$TYPE" = "cedarv1" - - echo "$CONFIGMAP_JSON" | jq -e '.authz_config.cedar' >/dev/null - POLICIES_COUNT=$(echo "$CONFIGMAP_JSON" | jq '.authz_config.cedar.policies | length') - test "$POLICIES_COUNT" = "3" - - echo "$CONFIGMAP_JSON" | jq -r '.authz_config.cedar.policies[]' | grep -q "weather" - echo "$CONFIGMAP_JSON" | jq -r '.authz_config.cedar.policies[]' | grep -q "greeting" - echo "$CONFIGMAP_JSON" | jq -r '.authz_config.cedar.policies[]' | grep -qi "forbid" - - ENTITIES_JSON=$(echo "$CONFIGMAP_JSON" | jq -r '.authz_config.cedar.entities_json // empty') - test -n "$ENTITIES_JSON" - ENTITIES_COUNT=$(echo "$ENTITIES_JSON" | jq '. | length') - test "$ENTITIES_COUNT" = "2" - - echo "✓ Authorization configuration embedded and validated in runconfig.json" - timeout: 120s - - - name: verify-proxyrunner-uses-volume-mounting - try: - - script: - content: | - echo "Verifying proxyrunner uses volume mounting..." - - DEPLOYMENT_ARGS=$(kubectl get deployment authz-configmap-ref-test -n toolhive-system -o jsonpath='{.spec.template.spec.containers[0].args}' 2>/dev/null || echo "") - VOLUME_MOUNTS=$(kubectl get deployment authz-configmap-ref-test -n toolhive-system -o jsonpath='{.spec.template.spec.containers[0].volumeMounts}' 2>/dev/null || echo "") - VOLUMES=$(kubectl get deployment authz-configmap-ref-test -n toolhive-system -o jsonpath='{.spec.template.spec.volumes}' 2>/dev/null || echo "") - - if [ -z "$DEPLOYMENT_ARGS" ]; then - echo "✗ Could not find deployment arguments" - exit 1 - fi - - echo "✓ Deployment arguments retrieved for volume mounting validation" - - # Verify volume mounting is configured - if echo "$VOLUME_MOUNTS" | jq -e '.[] | select(.name=="runconfig" and .mountPath=="/etc/runconfig")' > /dev/null 2>&1; then - echo "✓ runconfig volume mount found" - else - echo "✗ runconfig volume mount not found" - exit 1 - fi - - if echo "$VOLUMES" | jq -e '.[] | select(.name=="runconfig" and .configMap.name=="authz-configmap-ref-test-runconfig")' > /dev/null 2>&1; then - echo "✓ ConfigMap volume source found" - else - echo "✗ ConfigMap volume source not found" - exit 1 - fi - - if echo "$DEPLOYMENT_ARGS" | grep -q -- "--authz-config"; then - echo "✗ --authz-config flag found in deployment arguments (should not be present when using ConfigMap)" - echo "Deployment args: $DEPLOYMENT_ARGS" - exit 1 - fi - - echo "✓ Deployment args validated" - timeout: 60s - - - name: cleanup-configmap-mode - try: - - script: - content: | - echo "Cleaning up external authz ConfigMap and disabling ConfigMap mode..." - kubectl delete configmap external-authz-config -n toolhive-system --ignore-not-found - kubectl wait --for=delete configmap external-authz-config -n toolhive-system --timeout=60s || true - - kubectl patch deployment toolhive-operator -n toolhive-system --type='strategic' -p='{"spec":{"template":{"spec":{"containers":[{"name":"manager","env":[{"name":"TOOLHIVE_USE_CONFIGMAP","value":"false"}]}]}}}}' - kubectl rollout status deployment/toolhive-operator -n toolhive-system --timeout=60s - echo "✓ Cleanup completed" - timeout: 120s \ No newline at end of file diff --git a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/mcpserver-authz-ref.yaml b/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/mcpserver-authz-ref.yaml deleted file mode 100644 index 694b7b377..000000000 --- a/test/e2e/chainsaw/operator/single-tenancy/test-scenarios/authz-configmap-ref/mcpserver-authz-ref.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 -kind: MCPServer -metadata: - name: authz-configmap-ref-test - namespace: toolhive-system -spec: - image: ghcr.io/stackloklabs/yardstick/yardstick-server:0.0.2 - transport: stdio - proxyPort: 8080 - - # Reference external ConfigMap-based authorization configuration - authzConfig: - type: configMap - configMap: - name: external-authz-config - key: authz.json - - permissionProfile: - type: builtin - name: network - resources: - limits: - cpu: "100m" - memory: "128Mi" - requests: - cpu: "50m" - memory: "64Mi" \ No newline at end of file From 8467480e6c88dab226405bb6d6018e8de4a05514 Mon Sep 17 00:00:00 2001 From: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:37:46 +0000 Subject: [PATCH 3/3] lint Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com> --- .../mcp-server/mcpserver_controller_integration_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/thv-operator/test-integration/mcp-server/mcpserver_controller_integration_test.go b/cmd/thv-operator/test-integration/mcp-server/mcpserver_controller_integration_test.go index 60d738f7a..7d53aa7a8 100644 --- a/cmd/thv-operator/test-integration/mcp-server/mcpserver_controller_integration_test.go +++ b/cmd/thv-operator/test-integration/mcp-server/mcpserver_controller_integration_test.go @@ -27,6 +27,7 @@ var _ = Describe("MCPServer Controller Integration Tests", func() { interval = time.Millisecond * 250 defaultNamespace = "default" conditionTypeGroupRefValidated = "GroupRefValidated" + runconfigVolumeName = "runconfig" ) Context("When creating an Stdio MCPServer", Ordered, func() { @@ -148,7 +149,7 @@ var _ = Describe("MCPServer Controller Integration Tests", func() { foundRunconfigVolume := false for _, v := range templateSpec.Volumes { - if v.Name == "runconfig" && v.ConfigMap != nil && v.ConfigMap.Name == (mcpServerName+"-runconfig") { + if v.Name == runconfigVolumeName && v.ConfigMap != nil && v.ConfigMap.Name == (mcpServerName+"-runconfig") { foundRunconfigVolume = true break } @@ -160,7 +161,7 @@ var _ = Describe("MCPServer Controller Integration Tests", func() { // Verify that the runconfig ConfigMap is mounted as a volume foundRunconfigMount := false for _, vm := range container.VolumeMounts { - if vm.Name == "runconfig" && vm.MountPath == "/etc/runconfig" { + if vm.Name == runconfigVolumeName && vm.MountPath == "/etc/runconfig" { foundRunconfigMount = true break }