|
8 | 8 |
|
9 | 9 | . "github.com/onsi/ginkgo/v2" |
10 | 10 | . "github.com/onsi/gomega" |
| 11 | + appsv1 "k8s.io/api/apps/v1" |
11 | 12 | corev1 "k8s.io/api/core/v1" |
12 | 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
13 | 14 | "k8s.io/apimachinery/pkg/types" |
@@ -182,6 +183,56 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() { |
182 | 183 | Expect(runConfig.SchemaVersion).To(Equal(runner.CurrentSchemaVersion)) |
183 | 184 | }) |
184 | 185 |
|
| 186 | + It("Should create deployment with RunConfig volume mounts", func() { |
| 187 | + // Wait for the deployment to be created |
| 188 | + deployment := &appsv1.Deployment{} |
| 189 | + Eventually(func() error { |
| 190 | + return k8sClient.Get(ctx, types.NamespacedName{ |
| 191 | + Name: mcpServerName, |
| 192 | + Namespace: namespace, |
| 193 | + }, deployment) |
| 194 | + }, timeout, interval).Should(Succeed()) |
| 195 | + |
| 196 | + // Verify the deployment has the correct volume |
| 197 | + var runconfigVolume *corev1.Volume |
| 198 | + for i := range deployment.Spec.Template.Spec.Volumes { |
| 199 | + vol := &deployment.Spec.Template.Spec.Volumes[i] |
| 200 | + if vol.Name == "runconfig" { |
| 201 | + runconfigVolume = vol |
| 202 | + break |
| 203 | + } |
| 204 | + } |
| 205 | + Expect(runconfigVolume).NotTo(BeNil(), "RunConfig volume should exist in deployment") |
| 206 | + |
| 207 | + // Verify the volume references the correct ConfigMap |
| 208 | + Expect(runconfigVolume.ConfigMap).NotTo(BeNil()) |
| 209 | + Expect(runconfigVolume.ConfigMap.LocalObjectReference.Name).To(Equal(configMapName)) |
| 210 | + |
| 211 | + // Find the toolhive container |
| 212 | + var toolhiveContainer *corev1.Container |
| 213 | + for i := range deployment.Spec.Template.Spec.Containers { |
| 214 | + container := &deployment.Spec.Template.Spec.Containers[i] |
| 215 | + if container.Name == "toolhive" { |
| 216 | + toolhiveContainer = container |
| 217 | + break |
| 218 | + } |
| 219 | + } |
| 220 | + Expect(toolhiveContainer).NotTo(BeNil(), "Toolhive container should exist") |
| 221 | + |
| 222 | + // Verify the volume mount exists in the toolhive container |
| 223 | + var runconfigMount *corev1.VolumeMount |
| 224 | + for i := range toolhiveContainer.VolumeMounts { |
| 225 | + mount := &toolhiveContainer.VolumeMounts[i] |
| 226 | + if mount.Name == "runconfig" { |
| 227 | + runconfigMount = mount |
| 228 | + break |
| 229 | + } |
| 230 | + } |
| 231 | + Expect(runconfigMount).NotTo(BeNil(), "RunConfig volume mount should exist in toolhive container") |
| 232 | + Expect(runconfigMount.MountPath).To(Equal("/etc/runconfig")) |
| 233 | + Expect(runconfigMount.ReadOnly).To(BeTrue()) |
| 234 | + }) |
| 235 | + |
185 | 236 | It("Should not update ConfigMap when MCPServer spec is unchanged", func() { |
186 | 237 | // Get initial ConfigMap state |
187 | 238 | initialConfigMap := &corev1.ConfigMap{} |
@@ -657,5 +708,132 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() { |
657 | 708 | } |
658 | 709 | Expect(authMiddlewareFound).To(BeTrue(), "Auth middleware should be present in middleware_configs") |
659 | 710 | }) |
| 711 | + |
| 712 | + It("Should handle MCPServer with authorization ConfigMap reference", func() { |
| 713 | + namespace := "authz-configmap-ns" |
| 714 | + mcpServerName := "authz-configmap-server" |
| 715 | + configMapName := mcpServerName + "-runconfig" |
| 716 | + externalAuthzConfigMapName := "external-authz-config" |
| 717 | + |
| 718 | + // Create namespace |
| 719 | + ns := &corev1.Namespace{ |
| 720 | + ObjectMeta: metav1.ObjectMeta{ |
| 721 | + Name: namespace, |
| 722 | + }, |
| 723 | + } |
| 724 | + _ = k8sClient.Create(ctx, ns) |
| 725 | + |
| 726 | + // Create external authorization ConfigMap |
| 727 | + authzConfigMap := &corev1.ConfigMap{ |
| 728 | + ObjectMeta: metav1.ObjectMeta{ |
| 729 | + Name: externalAuthzConfigMapName, |
| 730 | + Namespace: namespace, |
| 731 | + }, |
| 732 | + Data: map[string]string{ |
| 733 | + "authz.json": `{ |
| 734 | + "version": "v1", |
| 735 | + "type": "cedarv1", |
| 736 | + "cedar": { |
| 737 | + "policies": [ |
| 738 | + "permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");", |
| 739 | + "permit(principal, action == Action::\"get_prompt\", resource == Prompt::\"greeting\");", |
| 740 | + "forbid(principal, action == Action::\"call_tool\", resource == Tool::\"sensitive_data\");" |
| 741 | + ], |
| 742 | + "entities_json": "[{\"uid\": {\"type\": \"User\", \"id\": \"user1\"}, \"attrs\": {\"name\": \"Alice\", \"role\": \"developer\"}},{\"uid\": {\"type\": \"User\", \"id\": \"admin\"}, \"attrs\": {\"name\": \"Bob\", \"role\": \"admin\"}}]" |
| 743 | + } |
| 744 | + }`, |
| 745 | + }, |
| 746 | + } |
| 747 | + Expect(k8sClient.Create(ctx, authzConfigMap)).Should(Succeed()) |
| 748 | + defer k8sClient.Delete(ctx, authzConfigMap) |
| 749 | + |
| 750 | + // Create MCPServer with ConfigMap authorization reference |
| 751 | + mcpServer := &mcpv1alpha1.MCPServer{ |
| 752 | + ObjectMeta: metav1.ObjectMeta{ |
| 753 | + Name: mcpServerName, |
| 754 | + Namespace: namespace, |
| 755 | + }, |
| 756 | + Spec: mcpv1alpha1.MCPServerSpec{ |
| 757 | + Image: "authz/mcp-server:latest", |
| 758 | + Transport: "stdio", |
| 759 | + ProxyPort: 8080, |
| 760 | + AuthzConfig: &mcpv1alpha1.AuthzConfigRef{ |
| 761 | + Type: mcpv1alpha1.AuthzConfigTypeConfigMap, |
| 762 | + ConfigMap: &mcpv1alpha1.ConfigMapAuthzRef{ |
| 763 | + Name: externalAuthzConfigMapName, |
| 764 | + Key: "authz.json", |
| 765 | + }, |
| 766 | + }, |
| 767 | + }, |
| 768 | + } |
| 769 | + |
| 770 | + Expect(k8sClient.Create(ctx, mcpServer)).Should(Succeed()) |
| 771 | + defer k8sClient.Delete(ctx, mcpServer) |
| 772 | + |
| 773 | + // Wait for RunConfig ConfigMap to be created |
| 774 | + configMap := &corev1.ConfigMap{} |
| 775 | + Eventually(func() error { |
| 776 | + return k8sClient.Get(ctx, types.NamespacedName{ |
| 777 | + Name: configMapName, |
| 778 | + Namespace: namespace, |
| 779 | + }, configMap) |
| 780 | + }, timeout, interval).Should(Succeed()) |
| 781 | + |
| 782 | + // Verify ConfigMap has the expected label |
| 783 | + Expect(configMap.Labels).To(HaveKeyWithValue("toolhive.stacklok.io/mcp-server", mcpServerName)) |
| 784 | + |
| 785 | + // Verify ConfigMap data contains runconfig.json |
| 786 | + Expect(configMap.Data).To(HaveKey("runconfig.json")) |
| 787 | + runConfigJSON := configMap.Data["runconfig.json"] |
| 788 | + Expect(runConfigJSON).NotTo(BeEmpty()) |
| 789 | + |
| 790 | + // Parse and verify RunConfig content |
| 791 | + var runConfig runner.RunConfig |
| 792 | + err := json.Unmarshal([]byte(runConfigJSON), &runConfig) |
| 793 | + Expect(err).NotTo(HaveOccurred()) |
| 794 | + |
| 795 | + // Verify authorization configuration was embedded from external ConfigMap |
| 796 | + Expect(runConfig.AuthzConfig).NotTo(BeNil()) |
| 797 | + Expect(runConfig.AuthzConfig.Version).To(Equal("v1")) |
| 798 | + Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigTypeCedarV1)) |
| 799 | + |
| 800 | + // Verify Cedar configuration |
| 801 | + Expect(runConfig.AuthzConfig.Cedar).NotTo(BeNil()) |
| 802 | + |
| 803 | + // Check policies are present |
| 804 | + Expect(runConfig.AuthzConfig.Cedar.Policies).To(HaveLen(3)) |
| 805 | + Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("call_tool")) |
| 806 | + Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("weather")) |
| 807 | + Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("get_prompt")) |
| 808 | + Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("greeting")) |
| 809 | + Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("forbid")) |
| 810 | + Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("sensitive_data")) |
| 811 | + |
| 812 | + // Verify entities are embedded |
| 813 | + Expect(runConfig.AuthzConfig.Cedar.EntitiesJSON).NotTo(BeEmpty()) |
| 814 | + |
| 815 | + // Parse entities to verify they're correctly embedded |
| 816 | + var entities []interface{} |
| 817 | + err = json.Unmarshal([]byte(runConfig.AuthzConfig.Cedar.EntitiesJSON), &entities) |
| 818 | + Expect(err).NotTo(HaveOccurred()) |
| 819 | + Expect(entities).To(HaveLen(2)) |
| 820 | + |
| 821 | + // Verify entity details |
| 822 | + entity1 := entities[0].(map[string]interface{}) |
| 823 | + uid1 := entity1["uid"].(map[string]interface{}) |
| 824 | + Expect(uid1["type"]).To(Equal("User")) |
| 825 | + Expect(uid1["id"]).To(Equal("user1")) |
| 826 | + attrs1 := entity1["attrs"].(map[string]interface{}) |
| 827 | + Expect(attrs1["name"]).To(Equal("Alice")) |
| 828 | + Expect(attrs1["role"]).To(Equal("developer")) |
| 829 | + |
| 830 | + entity2 := entities[1].(map[string]interface{}) |
| 831 | + uid2 := entity2["uid"].(map[string]interface{}) |
| 832 | + Expect(uid2["type"]).To(Equal("User")) |
| 833 | + Expect(uid2["id"]).To(Equal("admin")) |
| 834 | + attrs2 := entity2["attrs"].(map[string]interface{}) |
| 835 | + Expect(attrs2["name"]).To(Equal("Bob")) |
| 836 | + Expect(attrs2["role"]).To(Equal("admin")) |
| 837 | + }) |
660 | 838 | }) |
661 | 839 | }) |
0 commit comments