Skip to content

Commit 9ad67f7

Browse files
markturanskyuserclaudemergify[bot]
authored
feat: MPP-aware pod-status-syncer and kubernetes MCP sidecar (#1632)
## Summary - **pod-status-syncer**: On MPP clusters (`PLATFORM_MODE=mpp`), lists `TenantNamespace` resources (`tenant.paas.redhat.com/v1alpha1`) from the config namespace instead of `v1/Namespace` (which is forbidden). Derives actual namespace names with the `ambient-code--` prefix. - **kubeclient**: Adds `ListTenantNamespaces()` method for the TenantNamespace CRD. - **kube_reconciler**: Propagates `PLATFORM_MODE` and `MPP_CONFIG_NAMESPACE` env vars to credential sidecar containers so they can detect MPP mode at runtime. - **credential-entrypoint**: When `PLATFORM_MODE=mpp` and `provider=kubeconfig`, writes a TOML config with `denied_resources` blocking `v1/Namespace` and injects `--config` into the `kubernetes-mcp-server` subprocess args. No regression on vanilla Kubernetes — MPP code paths only activate when `PLATFORM_MODE=mpp` (set by `mpp-openshift` overlay). ## Files Changed < /dev/null | File | Change | |------|--------| | `components/ambient-control-plane/internal/reconciler/pod_sync.go` | MPP namespace listing via TenantNamespace | | `components/ambient-control-plane/internal/kubeclient/kubeclient.go` | `ListTenantNamespaces()` method | | `components/ambient-control-plane/cmd/ambient-control-plane/main.go` | Pass platform config to syncer + reconciler | | `components/ambient-control-plane/internal/reconciler/kube_reconciler.go` | Propagate PLATFORM_MODE to sidecar env | | `components/credential-sidecars/entrypoint/main.go` | `injectMPPConfig()` for k8s MCP sidecar | ## Test plan - [x] `go vet ./...` passes for ambient-control-plane - [x] `go build ./...` passes for ambient-control-plane and credential-sidecars/entrypoint - [x] `go test ./...` passes for ambient-control-plane - [x] All kustomize overlays build (base, production, mpp-openshift) - [ ] Deploy to MPP cluster and verify pod-status-syncer lists namespaces via TenantNamespace - [ ] Verify kubernetes-mcp-server sidecar starts with `--config` and `denied_resources` on MPP - [ ] Verify no regression on vanilla Kubernetes cluster 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added multi-platform/multi-tenant (MPP) mode: control plane discovers and syncs tenant namespaces and supports MPP-aware behavior. * Credential sidecars receive MPP-aware environment/configuration to restrict cluster-scoped namespace access. * **Tests** * E2E tests updated to improve workspace secret, API key, and environment-variable input flows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: user <u@example.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 992f98f commit 9ad67f7

6 files changed

Lines changed: 79 additions & 20 deletions

File tree

components/ambient-control-plane/cmd/ambient-control-plane/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error {
156156
HTTPSProxy: cfg.HTTPSProxy,
157157
NoProxy: cfg.NoProxy,
158158
ImagePullSecret: cfg.ImagePullSecret,
159+
PlatformMode: cfg.PlatformMode,
160+
MPPConfigNamespace: cfg.MPPConfigNamespace,
159161
}
160162

161163
conn, err := grpc.NewClient(cfg.GRPCServerAddr, grpc.WithTransportCredentials(grpcCredentials(cfg.GRPCUseTLS)))
@@ -196,7 +198,7 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error {
196198
inf.RegisterHandler("sessions", sessionRec.Reconcile)
197199
}
198200

199-
podSyncer := reconciler.NewPodStatusSyncer(factory, provisionerKube, log.Logger)
201+
podSyncer := reconciler.NewPodStatusSyncer(factory, provisionerKube, cfg.PlatformMode, cfg.MPPConfigNamespace, log.Logger)
200202

201203
tsErrCh := make(chan error, 1)
202204
go func() {

components/ambient-control-plane/internal/kubeclient/kubeclient.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ func (kc *KubeClient) UpdateNetworkPolicy(ctx context.Context, obj *unstructured
314314
return kc.dynamic.Resource(NetworkPolicyGVR).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{})
315315
}
316316

317+
func (kc *KubeClient) ListTenantNamespaces(ctx context.Context, namespace, labelSelector string) (*unstructured.UnstructuredList, error) {
318+
gvr := schema.GroupVersionResource{
319+
Group: "tenant.paas.redhat.com",
320+
Version: "v1alpha1",
321+
Resource: "tenantnamespaces",
322+
}
323+
opts := metav1.ListOptions{}
324+
if labelSelector != "" {
325+
opts.LabelSelector = labelSelector
326+
}
327+
return kc.dynamic.Resource(gvr).Namespace(namespace).List(ctx, opts)
328+
}
329+
317330
func (kc *KubeClient) GetResource(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
318331
return kc.dynamic.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
319332
}

components/ambient-control-plane/internal/reconciler/kube_reconciler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ type KubeReconcilerConfig struct {
7272
HTTPSProxy string
7373
NoProxy string
7474
ImagePullSecret string
75+
PlatformMode string
76+
MPPConfigNamespace string
7577
}
7678

7779
type SimpleKubeReconciler struct {
@@ -1008,6 +1010,12 @@ func (r *SimpleKubeReconciler) buildCredentialSidecars(sessionID string, namespa
10081010
if r.cfg.NoProxy != "" {
10091011
env = append(env, envVar("NO_PROXY", r.cfg.NoProxy))
10101012
}
1013+
if r.cfg.PlatformMode != "" {
1014+
env = append(env, envVar("PLATFORM_MODE", r.cfg.PlatformMode))
1015+
}
1016+
if r.cfg.MPPConfigNamespace != "" {
1017+
env = append(env, envVar("MPP_CONFIG_NAMESPACE", r.cfg.MPPConfigNamespace))
1018+
}
10111019

10121020
sidecar := map[string]interface{}{
10131021
"name": spec.Name,

components/ambient-control-plane/internal/reconciler/pod_sync.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@ const (
1818
)
1919

2020
type PodStatusSyncer struct {
21-
factory *SDKClientFactory
22-
kube *kubeclient.KubeClient
23-
logger zerolog.Logger
21+
factory *SDKClientFactory
22+
kube *kubeclient.KubeClient
23+
platformMode string
24+
mppConfigNamespace string
25+
logger zerolog.Logger
2426
}
2527

26-
func NewPodStatusSyncer(factory *SDKClientFactory, kube *kubeclient.KubeClient, logger zerolog.Logger) *PodStatusSyncer {
28+
func NewPodStatusSyncer(factory *SDKClientFactory, kube *kubeclient.KubeClient, platformMode, mppConfigNamespace string, logger zerolog.Logger) *PodStatusSyncer {
2729
return &PodStatusSyncer{
28-
factory: factory,
29-
kube: kube,
30-
logger: logger.With().Str("component", "pod-status-syncer").Logger(),
30+
factory: factory,
31+
kube: kube,
32+
platformMode: platformMode,
33+
mppConfigNamespace: mppConfigNamespace,
34+
logger: logger.With().Str("component", "pod-status-syncer").Logger(),
3135
}
3236
}
3337

@@ -60,6 +64,9 @@ func (s *PodStatusSyncer) syncOnce(ctx context.Context) {
6064
}
6165

6266
func (s *PodStatusSyncer) listManagedNamespaces(ctx context.Context) ([]string, error) {
67+
if s.platformMode == "mpp" {
68+
return s.listMPPManagedNamespaces(ctx)
69+
}
6370
nsList, err := s.kube.ListNamespacesByLabel(ctx, managedLabelFilter)
6471
if err != nil {
6572
return nil, fmt.Errorf("listing managed namespaces: %w", err)
@@ -72,6 +79,19 @@ func (s *PodStatusSyncer) listManagedNamespaces(ctx context.Context) ([]string,
7279
return names, nil
7380
}
7481

82+
func (s *PodStatusSyncer) listMPPManagedNamespaces(ctx context.Context) ([]string, error) {
83+
tnList, err := s.kube.ListTenantNamespaces(ctx, s.mppConfigNamespace, managedLabelFilter)
84+
if err != nil {
85+
return nil, fmt.Errorf("listing managed TenantNamespaces in %s: %w", s.mppConfigNamespace, err)
86+
}
87+
88+
var names []string
89+
for _, tn := range tnList.Items {
90+
names = append(names, "ambient-code--"+tn.GetName())
91+
}
92+
return names, nil
93+
}
94+
7595
func (s *PodStatusSyncer) syncNamespace(ctx context.Context, namespace string) {
7696
pods, err := s.kube.ListPodsByLabel(ctx, namespace, managedLabelFilter)
7797
if err != nil {

components/credential-sidecars/entrypoint/main.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ func main() {
6363
exchanger.StartBackgroundRefresh()
6464
defer exchanger.Stop()
6565

66-
runSubprocess(os.Args[1:])
66+
args := os.Args[1:]
67+
if os.Getenv("PLATFORM_MODE") == "mpp" && provider == "kubeconfig" {
68+
args = injectMPPConfig(args)
69+
}
70+
runSubprocess(args)
6771
}
6872

6973
func fetchAndSetCredential(bearerToken, apiURL, provider string) error {
@@ -195,6 +199,22 @@ func isValidCredentialID(id string) bool {
195199
return len(id) > 0
196200
}
197201

202+
func injectMPPConfig(args []string) []string {
203+
const mppConfig = `
204+
[[denied_resources]]
205+
group = ""
206+
version = "v1"
207+
kind = "Namespace"
208+
`
209+
configPath := "/tmp/mcp-mpp-config.toml"
210+
if err := os.WriteFile(configPath, []byte(mppConfig), 0600); err != nil {
211+
fmt.Fprintf(os.Stderr, "failed to write MPP MCP config: %v\n", err)
212+
return args
213+
}
214+
fmt.Fprintf(os.Stderr, "MPP mode: injecting kubernetes-mcp-server config to deny cluster-scoped Namespace access\n")
215+
return append([]string{args[0], "--config", configPath}, args[1:]...)
216+
}
217+
198218
func runSubprocess(args []string) {
199219
cmd := exec.Command(args[0], args[1:]...)
200220
cmd.Stdout = os.Stdout

e2e/cypress/e2e/sessions.cy.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,11 +1243,10 @@ describe('Ambient Session Management Tests', () => {
12431243
cy.contains('Runner API Keys').click({ force: true })
12441244
cy.wait(500)
12451245

1246-
// Find any input fields and type
12471246
cy.get('body').then(($inner) => {
1248-
const inputs = $inner.find('input[type="text"], input[type="password"]')
1249-
if (inputs.length) {
1250-
cy.wrap(inputs.first()).clear({ force: true }).type('test-api-key-value', { force: true })
1247+
if ($inner.find('input[type="text"], input[type="password"]').length) {
1248+
cy.get('input[type="text"], input[type="password"]').first().clear({ force: true })
1249+
cy.get('input[type="text"], input[type="password"]').first().type('test-api-key-value', { force: true })
12511250
cy.wait(200)
12521251
}
12531252
})
@@ -1268,15 +1267,12 @@ describe('Ambient Session Management Tests', () => {
12681267
cy.contains('button', 'Add Environment Variable').click({ force: true })
12691268
cy.wait(500)
12701269

1271-
// Look for key/value inputs that appear after clicking Add
12721270
cy.get('body').then(($inner) => {
1273-
const keyInputs = $inner.find('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]')
1274-
if (keyInputs.length) {
1275-
cy.wrap(keyInputs.last()).type('E2E_TEST_VAR', { force: true })
1271+
if ($inner.find('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]').length) {
1272+
cy.get('input[placeholder*="key"], input[placeholder*="KEY"], input[placeholder*="name"]').last().type('E2E_TEST_VAR', { force: true })
12761273
}
1277-
const valInputs = $inner.find('input[placeholder*="value"], input[placeholder*="VALUE"]')
1278-
if (valInputs.length) {
1279-
cy.wrap(valInputs.last()).type('test-value-123', { force: true })
1274+
if ($inner.find('input[placeholder*="value"], input[placeholder*="VALUE"]').length) {
1275+
cy.get('input[placeholder*="value"], input[placeholder*="VALUE"]').last().type('test-value-123', { force: true })
12801276
}
12811277
})
12821278
}

0 commit comments

Comments
 (0)