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
1 change: 1 addition & 0 deletions docs/internals/go-package-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2769,6 +2769,7 @@ var (
ErrEnsureOperatorNamespaceFailed = newSentinelError("failed to ensure operator namespace", errx.CodeSetup, errx.DescSetup)
ErrApplyRBACFailed = newSentinelError("failed to apply RBAC", errx.CodeSetup, errx.DescSetup)
ErrReadManagerYAMLFailed = newSentinelError("failed to read manager.yaml", errx.CodeSetup, errx.DescSetup)
ErrReadIngressManifestFailed = newSentinelError("failed to read ingress manifest", errx.CodeSetup, errx.DescSetup)
ErrParseManagerYAMLFailed = newSentinelError("failed to parse manager.yaml", errx.CodeSetup, errx.DescSetup)
ErrSetOperatorImageFailed = newSentinelError("failed to set operator image", errx.CodeSetup, errx.DescSetup)
ErrMutateManagerYAMLFailed = newSentinelError("failed to mutate manager.yaml", errx.CodeSetup, errx.DescSetup)
Expand Down
1 change: 1 addition & 0 deletions internal/cli/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ var (
ErrEnsureOperatorNamespaceFailed = newSentinelError("failed to ensure operator namespace", errx.CodeSetup, errx.DescSetup)
ErrApplyRBACFailed = newSentinelError("failed to apply RBAC", errx.CodeSetup, errx.DescSetup)
ErrReadManagerYAMLFailed = newSentinelError("failed to read manager.yaml", errx.CodeSetup, errx.DescSetup)
ErrReadIngressManifestFailed = newSentinelError("failed to read ingress manifest", errx.CodeSetup, errx.DescSetup)
ErrParseManagerYAMLFailed = newSentinelError("failed to parse manager.yaml", errx.CodeSetup, errx.DescSetup)
ErrSetOperatorImageFailed = newSentinelError("failed to set operator image", errx.CodeSetup, errx.DescSetup)
ErrMutateManagerYAMLFailed = newSentinelError("failed to mutate manager.yaml", errx.CodeSetup, errx.DescSetup)
Expand Down
319 changes: 319 additions & 0 deletions internal/cli/setup/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,161 @@ func TestRenderAnalyticsSecretManifestDisablesExistingDevLoginsOutsideTestMode(t
}
}

func TestRenderAnalyticsConfigManifestPreservesExistingPublicOAuthConfig(t *testing.T) {
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
if commandHasArgs(spec, "get", "configmap", "mcp-sentinel-config", "-n", "mcp-sentinel", "-o", "json") {
return &core.MockCommand{
Args: spec.Args,
OutputData: []byte(`{"data":{"GOOGLE_CLIENT_ID":"client.apps.googleusercontent.com","OIDC_ISSUER":"https://accounts.google.com","OIDC_AUDIENCE":"client.apps.googleusercontent.com","OIDC_JWKS_URL":"https://www.googleapis.com/oauth2/v3/certs","PLATFORM_MODE":"public"}}`),
}
}
return &core.MockCommand{Args: spec.Args}
},
}
kubectl := core.NewTestKubectlClient(mock)

rendered, err := renderAnalyticsConfigManifest(kubectl, `apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-sentinel-config
namespace: mcp-sentinel
data:
GOOGLE_CLIENT_ID: ""
OIDC_ISSUER: ""
OIDC_AUDIENCE: ""
OIDC_JWKS_URL: ""
PLATFORM_MODE: "tenant"
`, setupplan.PlatformModeTenant)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var payload struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata map[string]any `yaml:"metadata"`
Data map[string]string `yaml:"data"`
}
if err := yaml.Unmarshal([]byte(rendered), &payload); err != nil {
t.Fatalf("unmarshal rendered config: %v", err)
}
if payload.APIVersion != "v1" || payload.Kind != "ConfigMap" {
t.Fatalf("expected manifest envelope to be preserved, got apiVersion=%q kind=%q", payload.APIVersion, payload.Kind)
}
if got := payload.Metadata["name"]; got != "mcp-sentinel-config" {
t.Fatalf("expected metadata.name to be preserved, got %#v", got)
}
if got := payload.Data["GOOGLE_CLIENT_ID"]; got != "client.apps.googleusercontent.com" {
t.Fatalf("expected GOOGLE_CLIENT_ID to be preserved, got %q", got)
}
if got := payload.Data["PLATFORM_MODE"]; got != setupplan.PlatformModePublic {
t.Fatalf("expected PLATFORM_MODE to stay public, got %q", got)
}
}

func TestRenderAnalyticsConfigManifestAppliesExplicitPlatformMode(t *testing.T) {
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
if commandHasArgs(spec, "get", "configmap", "mcp-sentinel-config", "-n", "mcp-sentinel", "-o", "json") {
return &core.MockCommand{
Args: spec.Args,
OutputData: []byte(`{"data":{"PLATFORM_MODE":"public"}}`),
}
}
return &core.MockCommand{Args: spec.Args}
},
}
kubectl := core.NewTestKubectlClient(mock)

rendered, err := renderAnalyticsConfigManifest(kubectl, `apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-sentinel-config
namespace: mcp-sentinel
data:
PLATFORM_MODE: "tenant"
`, setupplan.PlatformModeOrg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var payload struct {
Data map[string]string `yaml:"data"`
}
if err := yaml.Unmarshal([]byte(rendered), &payload); err != nil {
t.Fatalf("unmarshal rendered config: %v", err)
}
if got := payload.Data["PLATFORM_MODE"]; got != setupplan.PlatformModeOrg {
t.Fatalf("expected explicit platform mode to win, got %q", got)
}
}

func TestRenderAnalyticsConfigManifestHandlesMissingConfigMap(t *testing.T) {
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
if commandHasArgs(spec, "get", "configmap", "mcp-sentinel-config", "-n", "mcp-sentinel", "-o", "json") {
return &core.MockCommand{
Args: spec.Args,
OutputData: []byte("Error from server (NotFound): configmaps \"mcp-sentinel-config\" not found"),
OutputErr: errors.New("exit status 1"),
}
}
return &core.MockCommand{Args: spec.Args}
},
}
kubectl := core.NewTestKubectlClient(mock)

rendered, err := renderAnalyticsConfigManifest(kubectl, `apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-sentinel-config
namespace: mcp-sentinel
data:
PLATFORM_MODE: "tenant"
`, setupplan.PlatformModeTenant)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

var payload struct {
Data map[string]string `yaml:"data"`
}
if err := yaml.Unmarshal([]byte(rendered), &payload); err != nil {
t.Fatalf("unmarshal rendered config: %v", err)
}
if got := payload.Data["PLATFORM_MODE"]; got != setupplan.PlatformModeTenant {
t.Fatalf("expected tenant mode on fresh install, got %q", got)
}
}

func TestRenderAnalyticsConfigManifestHandlesEmptyConfigMapOutput(t *testing.T) {
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
cmd := &core.MockCommand{Args: spec.Args}
if commandHasArgs(spec, "get", "configmap", "mcp-sentinel-config", "-n", core.DefaultAnalyticsNamespace, "-o", "json") {
cmd.OutputData = []byte("")
}
return cmd
},
}
kubectl := core.NewTestKubectlClient(mock)

rendered, err := renderAnalyticsConfigManifest(kubectl, `apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-sentinel-config
data:
PLATFORM_MODE: "tenant"
`, setupplan.PlatformModeTenant)
if err != nil {
t.Fatalf("renderAnalyticsConfigManifest returned error: %v", err)
}
if !strings.Contains(rendered, `PLATFORM_MODE: tenant`) {
t.Fatalf("expected rendered config to preserve platform mode, got %q", rendered)
}
}

func TestEnsureCSVIncludes(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -1907,6 +2062,170 @@ func TestDeployOperatorManifestsWithKubectl(t *testing.T) {
}
}

func TestEnsureRepoManagedTraefikMiddlewareResourcesAppliesMiddlewareSupportToRepoManagedController(t *testing.T) {
root := repoRootForTest(t)
origDir, err := os.Getwd()
if err != nil {
t.Fatalf("get working dir: %v", err)
}
if err := os.Chdir(root); err != nil {
t.Fatalf("chdir to repo root: %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(origDir)
})

var created []*core.MockCommand
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
cmd := &core.MockCommand{Args: spec.Args}
created = append(created, cmd)
switch {
case commandHasArgs(spec, "get", "deployment", "-A", "--no-headers", "-o", "custom-columns=NS:.metadata.namespace,NAME:.metadata.name"):
cmd.OutputData = []byte("traefik traefik\n")
case commandHasArgs(spec, "get", "deployment", "traefik", "-n", "traefik", "-o", "json"):
cmd.OutputData = []byte(`{"spec":{"template":{"spec":{"containers":[{"name":"traefik","args":["--providers.kubernetesingress=true"],"volumeMounts":[]}],"volumes":[]}}}}`)
}
return cmd
},
}
kubectl := core.NewTestKubectlClient(mock)

if err := ensureRepoManagedTraefikMiddlewareResources(kubectl, zap.NewNop()); err != nil {
t.Fatalf("unexpected error: %v", err)
}

var (
hasLookup bool
hasDynamicApply bool
hasPluginApply bool
hasPatch bool
)
for _, cmd := range mock.Commands {
if commandHasArgs(cmd, "get", "deployment", "-A", "--no-headers", "-o", "custom-columns=NS:.metadata.namespace,NAME:.metadata.name") {
hasLookup = true
}
if commandHasArgs(cmd, "patch", "deployment", "traefik", "-n", "traefik", "--type=json") {
hasPatch = true
}
}
for _, cmd := range created {
if cmd.StdinR == nil {
continue
}
data, err := io.ReadAll(cmd.StdinR)
if err != nil {
t.Fatalf("read stdin: %v", err)
}
body := string(data)
if strings.Contains(body, "name: traefik-dynamic") && strings.Contains(body, "namespace: traefik") {
hasDynamicApply = true
}
if strings.Contains(body, "name: traefik-plugin-pii-redactor") && strings.Contains(body, "namespace: traefik") {
hasPluginApply = true
}
}
if !hasLookup {
t.Fatal("expected active traefik deployment lookup")
}
if !hasDynamicApply {
t.Fatal("expected traefik dynamic-config apply")
}
if !hasPluginApply {
t.Fatal("expected traefik plugin-source apply")
}
if !hasPatch {
t.Fatal("expected traefik deployment patch")
}
}

func TestEnsureRepoManagedTraefikMiddlewareResourcesAppliesMiddlewareSupportToExternalController(t *testing.T) {
var created []*core.MockCommand
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
cmd := &core.MockCommand{Args: spec.Args}
created = append(created, cmd)
switch {
case commandHasArgs(spec, "get", "deployment", "-A", "--no-headers", "-o", "custom-columns=NS:.metadata.namespace,NAME:.metadata.name"):
cmd.OutputData = []byte("kube-system traefik\n")
case commandHasArgs(spec, "get", "deployment", "traefik", "-n", "kube-system", "-o", "json"):
cmd.OutputData = []byte(`{"spec":{"template":{"spec":{"containers":[{"name":"sidecar","args":[],"volumeMounts":[]},{"name":"traefik","args":["--providers.kubernetesingress"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data"}]}}}}`)
}
return cmd
},
}
kubectl := core.NewTestKubectlClient(mock)

if err := ensureRepoManagedTraefikMiddlewareResources(kubectl, zap.NewNop()); err != nil {
t.Fatalf("unexpected error: %v", err)
}

var (
hasDynamicApply bool
hasPluginApply bool
hasPatch bool
)
for _, cmd := range mock.Commands {
if commandHasArgs(cmd, "patch", "deployment", "traefik", "-n", "kube-system", "--type=json") {
hasPatch = true
if idx := argIndex(cmd.Args, "-p"); idx != -1 && idx+1 < len(cmd.Args) {
patchBody := cmd.Args[idx+1]
if !strings.Contains(patchBody, "/spec/template/spec/containers/1/") {
t.Fatalf("expected patch to target traefik container index, got %s", patchBody)
}
}
}
}
for _, cmd := range created {
if cmd.StdinR == nil {
continue
}
data, err := io.ReadAll(cmd.StdinR)
if err != nil {
t.Fatalf("read stdin: %v", err)
}
body := string(data)
if strings.Contains(body, "name: traefik-dynamic") && strings.Contains(body, "namespace: kube-system") {
hasDynamicApply = true
}
if strings.Contains(body, "name: traefik-plugin-pii-redactor") && strings.Contains(body, "namespace: kube-system") {
hasPluginApply = true
}
}
if !hasDynamicApply {
t.Fatal("expected external traefik dynamic-config apply")
}
if !hasPluginApply {
t.Fatal("expected external traefik plugin-source apply")
}
if !hasPatch {
t.Fatal("expected external traefik deployment patch")
}
}

func TestEnsureRepoManagedTraefikMiddlewareResourcesSkipsWhenMissing(t *testing.T) {
mock := &core.MockExecutor{
CommandFunc: func(spec core.ExecSpec) *core.MockCommand {
cmd := &core.MockCommand{Args: spec.Args}
if commandHasArgs(spec, "get", "deployment", "-A", "--no-headers", "-o", "custom-columns=NS:.metadata.namespace,NAME:.metadata.name") {
cmd.OutputData = []byte("")
}
return cmd
},
}
kubectl := core.NewTestKubectlClient(mock)

if err := ensureRepoManagedTraefikMiddlewareResources(kubectl, zap.NewNop()); err != nil {
t.Fatalf("unexpected error: %v", err)
}

for _, cmd := range mock.Commands {
if idx := argIndex(cmd.Args, "-f"); idx != -1 && idx+1 < len(cmd.Args) {
t.Fatalf("did not expect manifest apply when repo-managed traefik is absent, got %v", cmd.Args)
}
}
}

func TestDeployOperatorManifestsWithKubectlUsesIfNotPresentForTestModeImage(t *testing.T) {

root := repoRootForTest(t)
Expand Down
Loading
Loading