diff --git a/components/ambient-control-plane/cmd/ambient-control-plane/main.go b/components/ambient-control-plane/cmd/ambient-control-plane/main.go index 05ee27dfba..bac7d22d4d 100644 --- a/components/ambient-control-plane/cmd/ambient-control-plane/main.go +++ b/components/ambient-control-plane/cmd/ambient-control-plane/main.go @@ -156,6 +156,8 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error { HTTPSProxy: cfg.HTTPSProxy, NoProxy: cfg.NoProxy, ImagePullSecret: cfg.ImagePullSecret, + PlatformMode: cfg.PlatformMode, + MPPConfigNamespace: cfg.MPPConfigNamespace, } conn, err := grpc.NewClient(cfg.GRPCServerAddr, grpc.WithTransportCredentials(grpcCredentials(cfg.GRPCUseTLS))) @@ -196,7 +198,7 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error { inf.RegisterHandler("sessions", sessionRec.Reconcile) } - podSyncer := reconciler.NewPodStatusSyncer(factory, provisionerKube, log.Logger) + podSyncer := reconciler.NewPodStatusSyncer(factory, provisionerKube, cfg.PlatformMode, cfg.MPPConfigNamespace, log.Logger) tsErrCh := make(chan error, 1) go func() { diff --git a/components/ambient-control-plane/internal/kubeclient/kubeclient.go b/components/ambient-control-plane/internal/kubeclient/kubeclient.go index 3148cccbbf..2bc71863c1 100644 --- a/components/ambient-control-plane/internal/kubeclient/kubeclient.go +++ b/components/ambient-control-plane/internal/kubeclient/kubeclient.go @@ -314,6 +314,19 @@ func (kc *KubeClient) UpdateNetworkPolicy(ctx context.Context, obj *unstructured return kc.dynamic.Resource(NetworkPolicyGVR).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{}) } +func (kc *KubeClient) ListTenantNamespaces(ctx context.Context, namespace, labelSelector string) (*unstructured.UnstructuredList, error) { + gvr := schema.GroupVersionResource{ + Group: "tenant.paas.redhat.com", + Version: "v1alpha1", + Resource: "tenantnamespaces", + } + opts := metav1.ListOptions{} + if labelSelector != "" { + opts.LabelSelector = labelSelector + } + return kc.dynamic.Resource(gvr).Namespace(namespace).List(ctx, opts) +} + func (kc *KubeClient) GetResource(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { return kc.dynamic.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) } diff --git a/components/ambient-control-plane/internal/reconciler/kube_reconciler.go b/components/ambient-control-plane/internal/reconciler/kube_reconciler.go index e96fb1018a..634a26da63 100644 --- a/components/ambient-control-plane/internal/reconciler/kube_reconciler.go +++ b/components/ambient-control-plane/internal/reconciler/kube_reconciler.go @@ -72,6 +72,8 @@ type KubeReconcilerConfig struct { HTTPSProxy string NoProxy string ImagePullSecret string + PlatformMode string + MPPConfigNamespace string } type SimpleKubeReconciler struct { @@ -1008,6 +1010,12 @@ func (r *SimpleKubeReconciler) buildCredentialSidecars(sessionID string, namespa if r.cfg.NoProxy != "" { env = append(env, envVar("NO_PROXY", r.cfg.NoProxy)) } + if r.cfg.PlatformMode != "" { + env = append(env, envVar("PLATFORM_MODE", r.cfg.PlatformMode)) + } + if r.cfg.MPPConfigNamespace != "" { + env = append(env, envVar("MPP_CONFIG_NAMESPACE", r.cfg.MPPConfigNamespace)) + } sidecar := map[string]interface{}{ "name": spec.Name, diff --git a/components/ambient-control-plane/internal/reconciler/pod_sync.go b/components/ambient-control-plane/internal/reconciler/pod_sync.go index 1c10495d9e..97f67230de 100644 --- a/components/ambient-control-plane/internal/reconciler/pod_sync.go +++ b/components/ambient-control-plane/internal/reconciler/pod_sync.go @@ -18,16 +18,20 @@ const ( ) type PodStatusSyncer struct { - factory *SDKClientFactory - kube *kubeclient.KubeClient - logger zerolog.Logger + factory *SDKClientFactory + kube *kubeclient.KubeClient + platformMode string + mppConfigNamespace string + logger zerolog.Logger } -func NewPodStatusSyncer(factory *SDKClientFactory, kube *kubeclient.KubeClient, logger zerolog.Logger) *PodStatusSyncer { +func NewPodStatusSyncer(factory *SDKClientFactory, kube *kubeclient.KubeClient, platformMode, mppConfigNamespace string, logger zerolog.Logger) *PodStatusSyncer { return &PodStatusSyncer{ - factory: factory, - kube: kube, - logger: logger.With().Str("component", "pod-status-syncer").Logger(), + factory: factory, + kube: kube, + platformMode: platformMode, + mppConfigNamespace: mppConfigNamespace, + logger: logger.With().Str("component", "pod-status-syncer").Logger(), } } @@ -60,6 +64,9 @@ func (s *PodStatusSyncer) syncOnce(ctx context.Context) { } func (s *PodStatusSyncer) listManagedNamespaces(ctx context.Context) ([]string, error) { + if s.platformMode == "mpp" { + return s.listMPPManagedNamespaces(ctx) + } nsList, err := s.kube.ListNamespacesByLabel(ctx, managedLabelFilter) if err != nil { return nil, fmt.Errorf("listing managed namespaces: %w", err) @@ -72,6 +79,19 @@ func (s *PodStatusSyncer) listManagedNamespaces(ctx context.Context) ([]string, return names, nil } +func (s *PodStatusSyncer) listMPPManagedNamespaces(ctx context.Context) ([]string, error) { + tnList, err := s.kube.ListTenantNamespaces(ctx, s.mppConfigNamespace, managedLabelFilter) + if err != nil { + return nil, fmt.Errorf("listing managed TenantNamespaces in %s: %w", s.mppConfigNamespace, err) + } + + var names []string + for _, tn := range tnList.Items { + names = append(names, "ambient-code--"+tn.GetName()) + } + return names, nil +} + func (s *PodStatusSyncer) syncNamespace(ctx context.Context, namespace string) { pods, err := s.kube.ListPodsByLabel(ctx, namespace, managedLabelFilter) if err != nil { diff --git a/components/credential-sidecars/entrypoint/main.go b/components/credential-sidecars/entrypoint/main.go index e2c6c9606d..994079b339 100644 --- a/components/credential-sidecars/entrypoint/main.go +++ b/components/credential-sidecars/entrypoint/main.go @@ -63,7 +63,11 @@ func main() { exchanger.StartBackgroundRefresh() defer exchanger.Stop() - runSubprocess(os.Args[1:]) + args := os.Args[1:] + if os.Getenv("PLATFORM_MODE") == "mpp" && provider == "kubeconfig" { + args = injectMPPConfig(args) + } + runSubprocess(args) } func fetchAndSetCredential(bearerToken, apiURL, provider string) error { @@ -195,6 +199,22 @@ func isValidCredentialID(id string) bool { return len(id) > 0 } +func injectMPPConfig(args []string) []string { + const mppConfig = ` +[[denied_resources]] +group = "" +version = "v1" +kind = "Namespace" +` + configPath := "/tmp/mcp-mpp-config.toml" + if err := os.WriteFile(configPath, []byte(mppConfig), 0600); err != nil { + fmt.Fprintf(os.Stderr, "failed to write MPP MCP config: %v\n", err) + return args + } + fmt.Fprintf(os.Stderr, "MPP mode: injecting kubernetes-mcp-server config to deny cluster-scoped Namespace access\n") + return append([]string{args[0], "--config", configPath}, args[1:]...) +} + func runSubprocess(args []string) { cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout diff --git a/e2e/cypress/e2e/sessions.cy.ts b/e2e/cypress/e2e/sessions.cy.ts index 519ad07f03..2ede432254 100644 --- a/e2e/cypress/e2e/sessions.cy.ts +++ b/e2e/cypress/e2e/sessions.cy.ts @@ -1243,11 +1243,10 @@ describe('Ambient Session Management Tests', () => { cy.contains('Runner API Keys').click({ force: true }) cy.wait(500) - // Find any input fields and type cy.get('body').then(($inner) => { - const inputs = $inner.find('input[type="text"], input[type="password"]') - if (inputs.length) { - cy.wrap(inputs.first()).clear({ force: true }).type('test-api-key-value', { force: true }) + if ($inner.find('input[type="text"], input[type="password"]').length) { + cy.get('input[type="text"], input[type="password"]').first().clear({ force: true }) + cy.get('input[type="text"], input[type="password"]').first().type('test-api-key-value', { force: true }) cy.wait(200) } }) @@ -1268,15 +1267,12 @@ describe('Ambient Session Management Tests', () => { cy.contains('button', 'Add Environment Variable').click({ force: true }) cy.wait(500) - // Look for key/value inputs that appear after clicking Add cy.get('body').then(($inner) => { - const keyInputs = $inner.find('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]') - if (keyInputs.length) { - cy.wrap(keyInputs.last()).type('E2E_TEST_VAR', { force: true }) + if ($inner.find('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]').length) { + cy.get('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]').last().type('E2E_TEST_VAR', { force: true }) } - const valInputs = $inner.find('input[placeholder*="value"], input[placeholder*="VALUE"]') - if (valInputs.length) { - cy.wrap(valInputs.last()).type('test-value-123', { force: true }) + if ($inner.find('input[placeholder*="value"], input[placeholder*="VALUE"]').length) { + cy.get('input[placeholder*="value"], input[placeholder*="VALUE"]').last().type('test-value-123', { force: true }) } }) }