Skip to content

Commit 62e4074

Browse files
committed
add mapper & identity
1 parent b2567c7 commit 62e4074

7 files changed

Lines changed: 448 additions & 63 deletions

File tree

v2/README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ v2/
6060
Connection: writes go through its client, fresh reads through its API reader,
6161
and status/drift events arrive via a **watch on its cache** (event-driven, not
6262
polled — a low-frequency resync is only a backstop).
63+
- **Stop-on-disengage**: a Connection that loses readiness (revoked credential,
64+
unreachable provider, withdrawn RBAC) is disengaged, and its per-GVR syncers
65+
are torn down rather than left running against a dead cluster. When it becomes
66+
Ready again the provider re-engages as a fresh cluster and the syncers are
67+
rebuilt against it (a stale syncer would otherwise hold a dead client forever).
68+
- **Mapper extension point** (`engine/mapper`): the syncer routes every
69+
provider-side operation through a `Mapper` that translates the consumer object
70+
key to its provider key. Core ships only `Identity` (ns/name unchanged); an
71+
out-of-tree build supplies its own via `sync.WithMapper(...)` to restore v1's
72+
"Prefixed" key isolation without forking the engine. The interface maps keys
73+
only — it cannot change scope (cluster-scoped stays cluster-scoped), and it is
74+
deliberately kept out of the CRD API so the core API never promises renaming.
6375
- **Order-independent apply**: a `Connection` created before its Secret resolves
6476
when the Secret arrives (the konnector watches referenced Secrets); a binding
6577
created before its Connection resolves when the Connection goes Ready.
@@ -70,9 +82,10 @@ v2/
7082
gone, and keeps its Secret alive (via a finalizer) so teardown can still reach
7183
the provider — so `kubectl delete -f bundle.yaml` is order-don't-care.
7284

73-
Known POC simplifications (tracked against the proposal): the `Mapper`
74-
extension is not implemented; OpenAPI synthesis is best-effort (fidelity limits
75-
above); and syncer stop-on-disengage + productionization (RBAC/HA/Helm) remain.
85+
Known POC simplifications (tracked against the proposal): OpenAPI synthesis is
86+
best-effort (fidelity limits above); the `Mapper` seam exists but only `Identity`
87+
ships and `relatedResources` are not yet routed through it; and productionization
88+
(RBAC/HA/Helm) remains.
7689

7790
## Build
7891

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package mapper defines the Mapper extension point: how the konnector
18+
// translates object identity (namespace/name) between the consumer and the
19+
// provider when syncing instances.
20+
//
21+
// Core ships exactly one implementation — Identity — and the interface is a
22+
// compile-time seam so out-of-tree konnector builds can restore tenancy
23+
// key-mapping (e.g. v1's "Prefixed" isolation) without forking the engine. It
24+
// is deliberately kept out of the CRD API: the core API never promises
25+
// renaming.
26+
//
27+
// The interface maps KEYS only; it cannot change an object's scope
28+
// (cluster-scoped stays cluster-scoped). That is the hard line v2 draws — v1's
29+
// "Namespaced" scope-conversion isolation is intentionally not expressible here.
30+
// A later, separate Transformer interface (mutating the object payload — label
31+
// injection, field stripping) is possible but deliberately deferred.
32+
package mapper
33+
34+
import (
35+
"k8s.io/apimachinery/pkg/runtime/schema"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
)
38+
39+
// ObjectKey is a namespace/name identity. It aliases controller-runtime's
40+
// client.ObjectKey for drop-in interop with the syncer.
41+
type ObjectKey = client.ObjectKey
42+
43+
// Mapper translates object identity between consumer and provider. Core
44+
// registers exactly one implementation: Identity.
45+
type Mapper interface {
46+
// ToProvider maps a consumer object key to its provider key.
47+
ToProvider(gvr schema.GroupVersionResource, key ObjectKey) (ObjectKey, error)
48+
// ToConsumer is the inverse of ToProvider; it must round-trip, i.e.
49+
// ToConsumer(gvr, ToProvider(gvr, k)) == k.
50+
ToConsumer(gvr schema.GroupVersionResource, key ObjectKey) (ObjectKey, error)
51+
}
52+
53+
// Identity is the core Mapper: the consumer ns/name equals the provider ns/name
54+
// with no transformation. It is the only mapping core ships.
55+
type Identity struct{}
56+
57+
// ToProvider returns key unchanged.
58+
func (Identity) ToProvider(_ schema.GroupVersionResource, key ObjectKey) (ObjectKey, error) {
59+
return key, nil
60+
}
61+
62+
// ToConsumer returns key unchanged.
63+
func (Identity) ToConsumer(_ schema.GroupVersionResource, key ObjectKey) (ObjectKey, error) {
64+
return key, nil
65+
}
66+
67+
var _ Mapper = Identity{}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mapper_test
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/require"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
26+
"github.com/kube-bind/kube-bind/v2/konnector/engine/mapper"
27+
)
28+
29+
var widgetGVR = schema.GroupVersionResource{Group: "example.org", Version: "v1", Resource: "widgets"}
30+
31+
func TestIdentity_RoundTrips(t *testing.T) {
32+
m := mapper.Identity{}
33+
in := mapper.ObjectKey{Namespace: "team-a", Name: "w1"}
34+
35+
prov, err := m.ToProvider(widgetGVR, in)
36+
require.NoError(t, err)
37+
require.Equal(t, in, prov, "Identity must not change the key on the way to the provider")
38+
39+
back, err := m.ToConsumer(widgetGVR, prov)
40+
require.NoError(t, err)
41+
require.Equal(t, in, back, "Identity must round-trip")
42+
}
43+
44+
// prefixMapper is an out-of-tree-style Mapper that prefixes the provider
45+
// namespace, exercising the interface the way a custom build would (v1's
46+
// "Prefixed" isolation). It lives in the test to prove the seam is usable and
47+
// that the round-trip contract is satisfiable by a non-identity mapping.
48+
type prefixMapper struct{ prefix string }
49+
50+
func (p prefixMapper) ToProvider(_ schema.GroupVersionResource, key mapper.ObjectKey) (mapper.ObjectKey, error) {
51+
return mapper.ObjectKey{Namespace: p.prefix + key.Namespace, Name: key.Name}, nil
52+
}
53+
54+
func (p prefixMapper) ToConsumer(_ schema.GroupVersionResource, key mapper.ObjectKey) (mapper.ObjectKey, error) {
55+
return mapper.ObjectKey{Namespace: strings.TrimPrefix(key.Namespace, p.prefix), Name: key.Name}, nil
56+
}
57+
58+
func TestMapper_NonIdentityRoundTrips(t *testing.T) {
59+
var m mapper.Mapper = prefixMapper{prefix: "consumer-7-"}
60+
in := mapper.ObjectKey{Namespace: "team-a", Name: "w1"}
61+
62+
prov, err := m.ToProvider(widgetGVR, in)
63+
require.NoError(t, err)
64+
require.Equal(t, "consumer-7-team-a", prov.Namespace)
65+
require.Equal(t, "w1", prov.Name)
66+
67+
back, err := m.ToConsumer(widgetGVR, prov)
68+
require.NoError(t, err)
69+
require.Equal(t, in, back, "a Mapper must round-trip: ToConsumer(ToProvider(k)) == k")
70+
}

v2/konnector/engine/provider/connection_provider.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,34 @@ func (p *ConnectionProvider) Reconcile(ctx context.Context, req reconcile.Reques
118118
}
119119

120120
p.lock.Lock()
121-
defer p.lock.Unlock()
121+
mcMgr := p.mcMgr
122+
_, engaged := p.clusters[key]
123+
p.lock.Unlock()
122124

123-
if p.mcMgr == nil {
125+
if mcMgr == nil {
124126
return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
125127
}
126-
if _, ok := p.clusters[key]; ok {
128+
// A Connection that is no longer Ready (e.g. its credential was revoked, the
129+
// provider became unreachable, or RBAC was withdrawn) must be disengaged, not
130+
// just left running against a dead cluster. This also covers a Connection
131+
// mid-deletion that has already flipped not-Ready.
132+
if !isReady(conn) {
133+
if engaged {
134+
p.disengage(key)
135+
log.Info("Disengaged provider cluster (Connection no longer ready)")
136+
} else {
137+
log.V(4).Info("Connection not ready yet, not engaging")
138+
}
127139
return reconcile.Result{}, nil
128140
}
129-
if !isReady(conn) {
130-
log.V(4).Info("Connection not ready yet, not engaging")
141+
if engaged {
142+
return reconcile.Result{}, nil
143+
}
144+
145+
p.lock.Lock()
146+
defer p.lock.Unlock()
147+
// Re-check under the lock in case a concurrent reconcile engaged it.
148+
if _, ok := p.clusters[key]; ok {
131149
return reconcile.Result{}, nil
132150
}
133151

0 commit comments

Comments
 (0)