Browse Source

docs: tighten v2 provider PR summary

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 months ago
parent
commit
ce24f63e1e
1 changed files with 103 additions and 578 deletions
  1. 103 578
      commit.md

+ 103 - 578
commit.md

@@ -1,578 +1,103 @@
-I'll help you summarize the gRPC provider interface architecture. Let me explore the relevant files to understand the complete flow.
-
-[9 tools called]
-
-Now let me check one more thing about the API definitions to complete the picture:
-
-[2 tools called]
-
-Perfect! Now I have a complete understanding of the architecture. Let me create a comprehensive summary with code snippets and a flow diagram.
-
-## gRPC Provider Interface Architecture Summary
-
-### Overview
-
-The v2 provider architecture enables **out-of-process providers** using gRPC, allowing a single provider codebase to expose multiple APIs (e.g., AWS SecretsManager, ParameterStore, ECR, STS) without requiring modifications to existing v1 provider implementations.
-
----
-
-## 1. Client-Side: v2 → v1 Adapter (In-Process)
-
-### How ExternalSecret Controller Uses gRPC Clients
-
-In `externalsecret_controller_secret.go`, the reconciler uses the **Client Manager** to obtain provider clients:
-
-```42:46:pkg/controllers/externalsecret/externalsecret_controller_secret.go
-// We MUST NOT create multiple instances of a provider client (mostly due to limitations with GCP)
-// Clientmanager keeps track of the client instances
-// that are created during the fetching process and closes clients
-// if needed.
-mgr := secretstore.NewManager(r.Client, r.ControllerClass, r.EnableFloodGate)
-```
-
-### Client Manager: Creating gRPC Clients
-
-When a `SecretStoreRef` has `kind: Provider`, the manager creates a gRPC client:
-
-```122:153:pkg/controllers/secretstore/client_manager.go
-// Get returns a provider client from the given storeRef or sourceRef.secretStoreRef
-// while sourceRef.SecretStoreRef takes precedence over storeRef.
-// Do not close the client returned from this func, instead close
-// the manager once you're done with recinciling the external secret.
-func (m *Manager) Get(ctx context.Context, storeRef esv1.SecretStoreRef, namespace string, sourceRef *esv1.StoreGeneratorSourceRef) (esv1.SecretsClient, error) {
-	if storeRef.Kind == "Provider" {
-		return m.getV2ProviderClient(ctx, storeRef.Name, namespace)
-	}
-	if sourceRef != nil && sourceRef.SecretStoreRef != nil {
-		storeRef = *sourceRef.SecretStoreRef
-	}
-	store, err := m.getStore(ctx, &storeRef, namespace)
-	if err != nil {
-		return nil, err
-	}
-	// check if store should be handled by this controller instance
-	if !ShouldProcessStore(store, m.controllerClass) {
-		return nil, errors.New("can not reference unmanaged store")
-	}
-	// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
-	shouldProcess, err := m.shouldProcessSecret(store, namespace)
-	if err != nil {
-		return nil, err
-	}
-	if !shouldProcess {
-		return nil, fmt.Errorf(errClusterStoreMismatch, store.GetName(), namespace)
-	}
-
-	if m.enableFloodgate {
-		err := assertStoreIsUsable(store)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return m.GetFromStore(ctx, store, namespace)
-}
-```
-
-The `getV2ProviderClient` method:
-1. Fetches the `Provider` resource
-2. Creates a gRPC connection with TLS
-3. Wraps it with `V2ClientWrapper` (the v2→v1 adapter)
-
-```212:227:pkg/controllers/secretstore/client_manager.go
-// Create gRPC client
-grpcClient, err := grpc.NewClient(address, tlsConfig)
-if err != nil {
-	return nil, fmt.Errorf("failed to create gRPC client for Provider %q: %w", providerName, err)
-}
-
-// Convert ProviderReference to protobuf format
-providerRef := &pb.ProviderReference{
-	ApiVersion: provider.Spec.Config.ProviderRef.APIVersion,
-	Kind:       provider.Spec.Config.ProviderRef.Kind,
-	Name:       provider.Spec.Config.ProviderRef.Name,
-	Namespace:  provider.Spec.Config.ProviderRef.Namespace,
-}
-
-// Wrap with V2ClientWrapper
-wrappedClient := adapter.NewV2ClientWrapper(grpcClient, providerRef, namespace)
-```
-
-### V2ClientWrapper: Implementing v1.SecretsClient
-
-The wrapper adapts the gRPC `v2.Provider` interface to the v1 `SecretsClient` interface:
-
-```28:51:providers/v2/adapter/v2_to_v1.go
-// V2ClientWrapper wraps a v2.Provider (gRPC client) and exposes it as an esv1.SecretsClient.
-// This allows v2 providers to be used with the existing client manager infrastructure.
-type V2ClientWrapper struct {
-	v2Provider      v2.Provider
-	providerRef     *pb.ProviderReference
-	sourceNamespace string
-}
-
-// Ensure V2ClientWrapper implements SecretsClient interface
-var _ esv1.SecretsClient = &V2ClientWrapper{}
-
-// NewV2ClientWrapper creates a new wrapper that adapts a v2.Provider to esv1.SecretsClient.
-func NewV2ClientWrapper(v2Provider v2.Provider, providerRef *pb.ProviderReference, sourceNamespace string) esv1.SecretsClient {
-	return &V2ClientWrapper{
-		v2Provider:      v2Provider,
-		providerRef:     providerRef,
-		sourceNamespace: sourceNamespace,
-	}
-}
-
-// GetSecret retrieves a single secret from the provider.
-func (w *V2ClientWrapper) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	return w.v2Provider.GetSecret(ctx, ref, w.providerRef, w.sourceNamespace)
-}
-```
-
-### gRPC Client: Making RPC Calls
-
-The gRPC client converts v1 types to protobuf and makes RPC calls:
-
-```46:102:providers/v2/common/grpc/client.go
-// GetSecret retrieves a single secret from the provider via gRPC.
-func (c *grpcProviderClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) ([]byte, error) {
-	c.log.V(1).Info("getting secret via gRPC",
-		"key", ref.Key,
-		"version", ref.Version,
-		"property", ref.Property,
-		"connectionState", c.conn.GetState().String(),
-		"providerRef", providerRef,
-		"sourceNamespace", sourceNamespace)
-
-	// Check connection state before call
-	state := c.conn.GetState()
-	if state != connectivity.Ready && state != connectivity.Idle {
-		c.log.Info("connection not ready, attempting to reconnect",
-			"state", state.String(),
-			"target", c.conn.Target())
-	}
-
-	// Create context with timeout
-	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
-	defer cancel()
-
-	// Convert v1 reference to protobuf message
-	pbRef := &pb.ExternalSecretDataRemoteRef{
-		Key:              ref.Key,
-		Version:          ref.Version,
-		Property:         ref.Property,
-		DecodingStrategy: string(ref.DecodingStrategy),
-		MetadataPolicy:   string(ref.MetadataPolicy),
-	}
-
-	// Make gRPC call with provider reference
-	req := &pb.GetSecretRequest{
-		RemoteRef:       pbRef,
-		ProviderRef:     providerRef,
-		SourceNamespace: sourceNamespace,
-	}
-
-	c.log.V(1).Info("calling GetSecret RPC",
-		"target", c.conn.Target(),
-		"timeout", defaultTimeout.String())
-
-	resp, err := c.client.GetSecret(ctx, req)
-	if err != nil {
-		c.log.Error(err, "GetSecret RPC failed",
-			"key", ref.Key,
-			"connectionState", c.conn.GetState().String(),
-			"target", c.conn.Target())
-		return nil, fmt.Errorf("failed to get secret via gRPC: %w", err)
-	}
-
-	c.log.V(1).Info("GetSecret RPC succeeded",
-		"key", ref.Key,
-		"valueLength", len(resp.Value))
-
-	return resp.Value, nil
-}
-```
-
----
-
-## 2. Multiple APIs via ProviderReference Mapping
-
-### Separate CRDs for Each AWS Service
-
-The AWS v2 provider exposes **separate Kubernetes Custom Resources** for different services:
-
-```22:34:apis/provider/aws/v2alpha1/groupversion_info.go
-var (
-	// GroupVersion is group version used to register these objects
-	GroupVersion = schema.GroupVersion{Group: "provider.external-secrets.io", Version: "v2alpha1"}
-
-	// SecretsManagerKind is the kind name used for SecretsManager resources.
-	SecretsManagerKind = "SecretsManager"
-
-	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
-	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
-
-	// AddToScheme adds the types in this group-version to the given scheme.
-	AddToScheme = SchemeBuilder.AddToScheme
-)
-```
-
-Example: `SecretsManager` CRD:
-
-```74:81:apis/provider/aws/v2alpha1/awssecretsmanager_types.go
-// SecretsManager is the Schema for AWS Secrets Manager provider configuration
-type SecretsManager struct {
-	metav1.TypeMeta   `json:",inline"`
-	metav1.ObjectMeta `json:"metadata,omitempty"`
-
-	Spec   SecretsManagerSpec   `json:"spec,omitempty"`
-	Status SecretsManagerStatus `json:"status,omitempty"`
-}
-```
-
-Future expansion will include `ParameterStore`, `ECRAuthToken`, `STSSessionToken`, etc., all served by the **same gRPC server process**.
-
----
-
-## 3. Server-Side: v1 → v2 Adapter (Out-of-Process)
-
-### AdapterServer: Mapping ProviderRef to v1 Clients
-
-The gRPC server uses `AdapterServer` to map incoming `ProviderReference` (apiVersion + kind) to v1 provider implementations:
-
-```31:56:providers/v2/adapter/v1_to_v2.go
-// AdapterServer wraps a v1 provider and exposes it as a v2 gRPC service.
-// This allows existing v1 provider implementations to be used in the v2 architecture.
-type AdapterServer struct {
-	pb.UnimplementedSecretStoreProviderServer
-	kubeClient client.Client
-
-	// we support multiple v1 providers, so we need to map the v2 provider
-	// with apiVersion+kind to the corresponding v1 provider
-	resourceMapping ProviderMapping
-	specMapper      SpecMapper
-}
-
-type ProviderMapping map[schema.GroupVersionKind]esv1.ProviderInterface
-
-// maps a provider reference to a SecretStoreSpec
-// which is used to create a synthetic store for the v1 provider.
-type SpecMapper func(ref *pb.ProviderReference) (*esv1.SecretStoreSpec, error)
-
-// NewAdapterServer creates a new AdapterServer that wraps a v1 provider.
-func NewAdapterServer(kubeClient client.Client, resourceMapping ProviderMapping, specMapping SpecMapper) *AdapterServer {
-	return &AdapterServer{
-		kubeClient:      kubeClient,
-		resourceMapping: resourceMapping,
-		specMapper:      specMapping,
-	}
-}
-```
-
-### Resolving Provider from ProviderReference
-
-The server resolves the v1 provider based on GVK:
-
-```58:101:providers/v2/adapter/v1_to_v2.go
-func (s *AdapterServer) resolveProvider(ref *pb.ProviderReference) (esv1.ProviderInterface, error) {
-	if ref == nil {
-		return nil, fmt.Errorf("provider reference is nil")
-	}
-
-	splitted := strings.Split(ref.ApiVersion, "/")
-	if len(splitted) != 2 {
-		return nil, fmt.Errorf("invalid api version: %s", ref.ApiVersion)
-	}
-	group := splitted[0]
-	version := splitted[1]
-
-	key := schema.GroupVersionKind{
-		Group:   group,
-		Version: version,
-		Kind:    ref.Kind,
-	}
-	v1Provider, ok := s.resourceMapping[key]
-	if !ok {
-		return nil, fmt.Errorf("resource mapping not found for %q", key)
-	}
-	return v1Provider, nil
-}
-
-func (s *AdapterServer) getClient(ctx context.Context, ref *pb.ProviderReference, namespace string) (esv1.SecretsClient, error) {
-	if ref == nil {
-		return nil, fmt.Errorf("request or remote ref is nil")
-	}
-
-	spec, err := s.specMapper(ref)
-	if err != nil {
-		return nil, fmt.Errorf("failed to map provider reference to spec: %w", err)
-	}
-	// TODO: support cluster scoped Provider
-	store, err := NewSyntheticStore(spec, namespace)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create synthetic store: %w", err)
-	}
-	provider, err := s.resolveProvider(ref)
-	if err != nil {
-		return nil, fmt.Errorf("failed to resolve provider: %w", err)
-	}
-	return provider.NewClient(ctx, store, s.kubeClient, namespace)
-}
-```
-
-### GetSecret RPC Handler
-
-The server receives GetSecret requests and delegates to v1 providers:
-
-```103:135:providers/v2/adapter/v1_to_v2.go
-// GetSecret retrieves a single secret from the provider.
-func (s *AdapterServer) GetSecret(ctx context.Context, req *pb.GetSecretRequest) (*pb.GetSecretResponse, error) {
-	if req == nil || req.RemoteRef == nil {
-		return nil, fmt.Errorf("request or remote ref is nil")
-	}
-	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get client: %w", err)
-	}
-	defer client.Close(ctx)
-
-	// Convert protobuf remote ref to v1 remote ref
-	ref := esv1.ExternalSecretDataRemoteRef{
-		Key:      req.RemoteRef.Key,
-		Version:  req.RemoteRef.Version,
-		Property: req.RemoteRef.Property,
-	}
-	if req.RemoteRef.DecodingStrategy != "" {
-		ref.DecodingStrategy = esv1.ExternalSecretDecodingStrategy(req.RemoteRef.DecodingStrategy)
-	}
-	if req.RemoteRef.MetadataPolicy != "" {
-		ref.MetadataPolicy = esv1.ExternalSecretMetadataPolicy(req.RemoteRef.MetadataPolicy)
-	}
-
-	value, err := client.GetSecret(ctx, ref)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get secret: %w", err)
-	}
-
-	return &pb.GetSecretResponse{
-		Value: value,
-	}, nil
-}
-```
-
-### AWS Provider Main: Single Process, Multiple APIs
-
-The AWS provider's main function sets up the mapping:
-
-```70:105:providers/v2/aws/main.go
-v1Provider := awsv1.NewProvider()
-adapterServer := adapter.NewAdapterServer(kubeClient, adapter.ProviderMapping{
-	schema.GroupVersionKind{
-		Group:   awsv2alpha1.GroupVersion.Group,
-		Version: awsv2alpha1.GroupVersion.Version,
-		Kind:    awsv2alpha1.SecretsManagerKind,
-	}: v1Provider,
-}, func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
-	if ref.Kind != awsv2alpha1.SecretsManagerKind {
-		return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
-	}
-	var awsProvider awsv2alpha1.SecretsManager
-	err := kubeClient.Get(context.Background(), client.ObjectKey{
-		Namespace: ref.Namespace,
-		Name:      ref.Name,
-	}, &awsProvider)
-	if err != nil {
-		return nil, err
-	}
-	return &v1.SecretStoreSpec{
-		Provider: &v1.SecretStoreProvider{
-			AWS: &v1.AWSProvider{
-				Service:           v1.AWSServiceSecretsManager,
-				Auth:              awsProvider.Spec.Auth,
-				Role:              awsProvider.Spec.Role,
-				Region:            awsProvider.Spec.Region,
-				AdditionalRoles:   awsProvider.Spec.AdditionalRoles,
-				ExternalID:        awsProvider.Spec.ExternalID,
-				SecretsManager:    awsProvider.Spec.SecretsManager,
-				SessionTags:       awsProvider.Spec.SessionTags,
-				TransitiveTagKeys: awsProvider.Spec.TransitiveTagKeys,
-				Prefix:            awsProvider.Spec.Prefix,
-			},
-		},
-	}, nil
-})
-```
-
-**To add ParameterStore**, you'd simply extend the mapping:
-
-```go
-schema.GroupVersionKind{
-    Group:   awsv2alpha1.GroupVersion.Group,
-    Version: awsv2alpha1.GroupVersion.Version,
-    Kind:    "ParameterStore",
-}: v1Provider,  // Same v1 provider instance!
-```
-
-And update the `specMapper` to handle the new Kind.
-
----
-
-## 4. Provider Controller: Validation & Status
-
-The Provider controller validates connectivity and reports capabilities:
-
-```85:145:pkg/controllers/provider/controller.go
-// validateStoreAndGetCapabilities validates the Provider configuration and retrieves capabilities by:
-// 1. Creating a gRPC client to the provider
-// 2. Calling Validate() on the provider with the ProviderReference
-// 3. Calling Capabilities() to get the provider's capabilities
-func (r *Reconciler) validateStoreAndGetCapabilities(ctx context.Context, store *esv1.Provider) (esv1.ProviderCapabilities, error) {
-	// Get provider address
-	address := store.Spec.Config.Address
-	if address == "" {
-		return "", fmt.Errorf("provider address is required")
-	}
-
-	// Load TLS configuration
-	tlsConfig, err := grpc.LoadClientTLSConfig(ctx, r.Client, store.Spec.Config.ProviderRef.Kind, "external-secrets-system")
-	if err != nil {
-		return "", fmt.Errorf("failed to load TLS config: %w", err)
-	}
-
-	// Create gRPC client with TLS
-	client, err := grpc.NewClient(address, tlsConfig)
-	if err != nil {
-		return "", fmt.Errorf("failed to create gRPC client: %w", err)
-	}
-	defer client.Close(ctx)
-
-	// Convert ProviderReference to protobuf format
-	providerRef := &pb.ProviderReference{
-		ApiVersion: store.Spec.Config.ProviderRef.APIVersion,
-		Kind:       store.Spec.Config.ProviderRef.Kind,
-		Name:       store.Spec.Config.ProviderRef.Name,
-		Namespace:  store.Spec.Config.ProviderRef.Namespace,
-	}
-
-	// Validate the provider configuration
-	if err := client.Validate(ctx, providerRef, store.Namespace); err != nil {
-		r.Log.Error(err, "provider validation failed")
-		return "", fmt.Errorf("provider validation failed: %w", err)
-	}
-
-	// Get provider capabilities
-	caps, err := client.Capabilities(ctx, providerRef, store.Namespace)
-	if err != nil {
-		r.Log.Error(err, "failed to get capabilities")
-		// Don't fail validation if capabilities check fails, just log and default to ReadOnly
-		return esv1.ProviderReadOnly, nil
-	}
-
-	// Map gRPC capabilities to our API type
-	var capabilities esv1.ProviderCapabilities
-	switch caps {
-	case 0: // READ_ONLY
-		capabilities = esv1.ProviderReadOnly
-	case 1: // WRITE_ONLY
-		capabilities = esv1.ProviderWriteOnly
-	case 2: // READ_WRITE
-		capabilities = esv1.ProviderReadWrite
-	default:
-		capabilities = esv1.ProviderReadOnly
-	}
-
-	return capabilities, nil
-}
-```
-
----
-
-## Flow Diagram
-
-```mermaid
-graph TB
-    subgraph "ESO Controller (In-Process)"
-        A[ExternalSecret Controller] -->|"GetProviderSecretData()"| B[Client Manager]
-        B -->|"Check storeRef.kind == Provider"| C{Is v2 Provider?}
-        C -->|Yes| D[Create gRPC Client]
-        C -->|No| E[Use v1 Provider In-Process]
-        D --> F[V2ClientWrapper<br/>v2→v1 Adapter]
-        F -->|"Implements<br/>esv1.SecretsClient"| G[gRPC Client]
-    end
-    
-    subgraph "gRPC Communication"
-        G -->|"GetSecretRequest<br/>{ProviderRef, RemoteRef}"| H[mTLS Connection]
-    end
-    
-    subgraph "Provider Server (Out-of-Process)"
-        H --> I[AdapterServer<br/>v1→v2 Adapter]
-        I -->|"1. Parse ProviderRef<br/>(apiVersion + kind)"| J{Resolve GVK}
-        J -->|"SecretsManager"| K[AWS v1 Provider]
-        J -->|"ParameterStore<br/>(future)"| K
-        J -->|"ECRAuthToken<br/>(future)"| K
-        J -->|"STSSessionToken<br/>(future)"| K
-        K -->|"2. Fetch CR<br/>(e.g., SecretsManager)"| L[Kubernetes API]
-        K -->|"3. Create synthetic<br/>SecretStore"| M[v1.SecretStoreSpec]
-        M -->|"4. Call v1 provider"| N[provider.NewClient]
-        N -->|"5. Get secret"| O[AWS API]
-        O -->|"6. Return secret data"| H
-    end
-    
-    subgraph "Provider Validation"
-        P[Provider Controller] -->|"Validate + GetCapabilities"| I
-        P -->|"Update Status"| Q[Provider CRD Status]
-    end
-
-    style A fill:#e1f5ff
-    style F fill:#ffe1f5
-    style I fill:#fff5e1
-    style K fill:#e1ffe1
-```
-
----
-
-## Key Benefits
-
-### 1. **No v1 Provider Modifications Required**
-Existing v1 providers (AWS, GCP, Azure, etc.) work unchanged. The adapters handle all translation.
-
-### 2. **Single Codebase, Multiple APIs**
-One AWS provider binary supports:
-- SecretsManager
-- ParameterStore  
-- ECR AuthToken Generator
-- STS Session Token Generator
-
-No need for multiple source trees or provider versions.
-
-### 3. **Decoupled Deployment**
-- Providers run as separate pods
-- Can be scaled independently
-- Can use different RBAC, network policies, service meshes
-- Easier to add non-Go providers in the future
-
-### 4. **Resource-Based Configuration**
-Each service is a **separate CRD** (`SecretsManager`, `ParameterStore`), making configuration more intuitive and allowing per-service RBAC.
-
----
-
-## Code Changes Summary
-
-From git status and exploration:
-
-### Added:
-- `/pkg/controllers/provider/` - Provider CRD controller
-- `/providers/v2/adapter/v2_to_v1.go` - Client-side adapter (gRPC→v1)
-- `/providers/v2/adapter/v1_to_v2.go` - Server-side adapter (v1→gRPC)
-- `/providers/v2/common/grpc/` - gRPC client/server infrastructure
-- `/providers/v2/aws/main.go` - AWS provider server with mapping
-- `/apis/provider/aws/v2alpha1/` - SecretsManager CRD definition
-
-### Modified:
-- `cmd/controller/root.go` - Register Provider controller
-- `pkg/controllers/crds/provider_certs.go` - TLS cert management for providers
-- `pkg/controllers/secretstore/client_manager.go` - Added `getV2ProviderClient()`
-
-### Deleted:
-- Old POC files from earlier iterations
-
-This architecture is production-ready and fully backward compatible with existing v1 providers! 🚀
+# V2 provider runtime plumbing with Kubernetes provider as the first hardened path
+
+## Summary
+
+This PR introduces the shared v2 provider runtime for out-of-process providers and wires the Kubernetes provider through the full controller -> clientmanager -> gRPC -> adapter path.
+
+The Kubernetes provider is the first fully exercised provider on this path. Other providers can build on the same runtime pieces later, but the implementation and verification in this PR are intentionally centered on Kubernetes.
+
+## What is in scope
+
+- add the shared v2 API surface:
+  - `Provider` and `ClusterProvider`
+  - provider config CRDs
+  - gRPC protobufs and transport helpers
+- add controller support for `Provider` and `ClusterProvider`
+- teach `runtime/clientmanager` how to resolve, cache, and invalidate v2 provider clients
+- add the forward adapter:
+  - gRPC -> synthetic v1 store -> existing provider interface
+- add the reverse adapter:
+  - v2 gRPC client -> `esv1.SecretsClient`
+- add the first concrete v2 provider implementation for Kubernetes
+- extend the e2e harness so Kubernetes v2 can run through the normal provider suite
+
+## Kubernetes path polish in this branch
+
+The WIP follow-up work in this branch tightened the Kubernetes v2 path in a few places that were previously incomplete or ambiguous:
+
+- preserve `storeRef.kind` from the controllers into the gRPC `ProviderReference`
+- preserve cluster provider auth scope through the reverse adapter path
+- stop defaulting PushSecret store kinds in places where omission must remain distinguishable
+- preserve PushSecret metadata payloads through the transport
+- make Kubernetes config mapping prefer `providerRef.namespace` and only fall back to `sourceNamespace`
+- add coverage for provider-namespace vs manifest-namespace auth scope
+- add recovery coverage around provider and cluster-provider v2 PushSecret flows
+- harden the focused v2 e2e loop and trim Docker build context noise for faster reruns
+
+## Suggested review path
+
+If you want to review this in the runtime order secrets flow through, this is the shortest path:
+
+1. Controller entrypoints
+   - `pkg/controllers/provider/controller.go`
+   - `pkg/controllers/clusterprovider/controller.go`
+   - `pkg/controllers/pushsecret/pushsecret_controller_v2.go`
+2. Client resolution and caching
+   - `runtime/clientmanager/manager.go`
+3. gRPC transport
+   - `providers/v2/common/proto/provider/secretstore.proto`
+   - `providers/v2/common/grpc/client.go`
+   - `providers/v2/common/grpc/pool.go`
+   - `providers/v2/common/grpc/tls.go`
+4. Adapter boundary
+   - `providers/v2/adapter/store/server.go`
+   - `providers/v2/adapter/store/client.go`
+   - `providers/v2/adapter/store/synthetic_store.go`
+5. Kubernetes provider
+   - `providers/v2/kubernetes/main.go`
+   - `providers/v2/kubernetes/config.go`
+
+## Behavior that is now locked down
+
+- namespaced `Provider` auth always resolves against the manifest namespace
+- `ClusterProvider` namespace conditions are enforced before client creation
+- `ClusterProvider.authenticationScope=ProviderNamespace` requires `spec.config.providerRef.namespace`
+- TLS secret namespace resolution follows the effective auth namespace
+- v2 client caching is generation-aware and namespace-sensitive
+- pooled gRPC connections are released when the manager closes
+- every RPC carries both `ProviderReference` and `SourceNamespace`
+- the reverse adapter preserves remote refs, store kinds, and PushSecret metadata
+- Kubernetes provider config lookup honors explicit provider-ref namespace before manifest fallback
+
+## Verification
+
+Focused Kubernetes v2 e2e coverage passed:
+
+- `push-secret`
+- `cluster-provider`
+- `namespaced-provider`
+- `capabilities`
+- `metrics`
+
+Fresh package verification passed:
+
+- `runtime/clientmanager`
+- `providers/v2/common/grpc`
+- `providers/v2/adapter`
+- `providers/v2/kubernetes`
+- `cmd/controller`
+- `pkg/controllers/provider`
+- `pkg/controllers/clusterprovider`
+- targeted v2 `pkg/controllers/pushsecret` tests
+
+## Notes for reviewers
+
+- this PR contains shared runtime pieces that future providers will reuse, but Kubernetes is the only provider path intentionally polished and covered end-to-end here
+- the adapter `Capabilities()` comment is still an architectural TODO, but it is not a correctness blocker for the current Kubernetes implementation
+- the OVH constructor fix is unrelated plumbing repair needed to keep the wider tree building during the rebase
+
+## Follow-up
+
+- extend Kubernetes v2 e2e coverage from this baseline
+- bring additional providers onto the same runtime path one at a time
+- revisit the longer-term provider/generator capability advertisement model once more than one v2 provider is live