Skip to content
Open
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
29 changes: 24 additions & 5 deletions engine/binding/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/kbind/kbind/engine/crdpull"
"github.com/kbind/kbind/engine/openapi"
"github.com/kbind/kbind/engine/remote"
corev1alpha1 "github.com/kbind/kbind/sdk/apis/core/v1alpha1"
)
Expand Down Expand Up @@ -110,13 +111,31 @@ func (b *base) reconcileAccessor(ctx context.Context, obj corev1alpha1.BindingAc
}
var hash string
if conn.Status.ActiveSchemaSource == corev1alpha1.SchemaSourceOpenAPI {
// CRD-less provider: the Connection synthesized + installed the CRD.
// The binding just consumes it (no provider apiextensions to read).
// CRD-less provider (kcp): install the CRD for THIS bound API only
// (unless pullPolicy: None, where the user manages CRDs). The Connection
// no longer installs the whole discovery set, so the provider's built-in
// groups (e.g. *.kcp.io) never reach the consumer — only what a binding
// references does.
crd := &apiextensionsv1.CustomResourceDefinition{}
if err := b.client.Get(ctx, client.ObjectKey{Name: api.Name}, crd); err != nil {
err := b.client.Get(ctx, client.ObjectKey{Name: api.Name}, crd)
if apierrors.IsNotFound(err) && conn.Spec.Schema.PullPolicy != corev1alpha1.PullPolicyNone {
cfg, cerr := remote.RestConfigFromConnection(ctx, b.client, conn)
if cerr != nil {
return fmt.Errorf("provider rest config: %w", cerr)
}
synth, serr := openapi.SynthesizeCRD(ctx, cfg, api.Name)
if serr != nil {
return fmt.Errorf("synthesizing CRD %q: %w", api.Name, serr)
}
if _, ierr := crdpull.Install(ctx, b.client, synth, conn.Name, conn.Spec.Schema.UpdatePolicy != corev1alpha1.UpdatePolicyOnce); ierr != nil {
return fmt.Errorf("installing synthesized CRD %q: %w", api.Name, ierr)
}
err = b.client.Get(ctx, client.ObjectKey{Name: api.Name}, crd)
}
if err != nil {
if apierrors.IsNotFound(err) {
// The API is exported, but the Connection has not installed the
// synthesized CRD yet — a transient wait, not "not exported".
// Exported but the CRD is not present yet (pullPolicy: None until
// the user applies it) — a transient wait, not "not exported".
pendingSchema = append(pendingSchema, api.Name)
continue
}
Expand Down
52 changes: 37 additions & 15 deletions engine/connection/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,32 @@ func (r *Reconciler) reconcile(ctx context.Context, conn *corev1alpha1.Connectio
setCondition(conn, corev1alpha1.ConditionPermissionDenied, metav1.ConditionFalse, corev1alpha1.ReasonAsExpected, "no RBAC denials")

// pullPolicy: All — eagerly install every exported CRD on the consumer,
// without waiting for a binding. (Bound/None defer to the binding.)
// without waiting for a binding. (Bound/None defer to the binding.) The CRD
// source pulls the provider CRD object; the OpenAPI source has no CRD object
// to pull, so it synthesizes and installs.
if conn.Spec.Schema.PullPolicy == corev1alpha1.PullPolicyAll {
for i := range exported {
if _, _, err := crdpull.Pull(ctx, r.Client, providerClient, exported[i].Name, conn.Name, crdpull.Options{
Create: true,
Update: conn.Spec.Schema.UpdatePolicy != corev1alpha1.UpdatePolicyOnce,
}); err != nil {
return fmt.Errorf("eager-pulling CRD %q: %w", exported[i].Name, err)
if source == corev1alpha1.SchemaSourceOpenAPI {
cfg, err := remote.RestConfigFromConnection(ctx, r.Client, conn)
if err != nil {
return fmt.Errorf("provider rest config: %w", err)
}
crds, err := openapi.SynthesizeCRDs(ctx, cfg)
if err != nil {
return fmt.Errorf("synthesizing CRDs: %w", err)
}
for _, crd := range crds {
if _, err := crdpull.Install(ctx, r.Client, crd, conn.Name, conn.Spec.Schema.UpdatePolicy != corev1alpha1.UpdatePolicyOnce); err != nil {
return fmt.Errorf("eager-installing CRD %q: %w", crd.Name, err)
}
}
} else {
for i := range exported {
if _, _, err := crdpull.Pull(ctx, r.Client, providerClient, exported[i].Name, conn.Name, crdpull.Options{
Create: true,
Update: conn.Spec.Schema.UpdatePolicy != corev1alpha1.UpdatePolicyOnce,
}); err != nil {
return fmt.Errorf("eager-pulling CRD %q: %w", exported[i].Name, err)
}
}
}
}
Expand Down Expand Up @@ -460,10 +478,12 @@ func (r *Reconciler) bindingsReferencing(ctx context.Context, name string) (int,
// discoverExportedCRDs lists provider CRDs carrying the exported label and maps
// them to ExportedAPI entries.
// discoverAndInstall resolves the schema source and returns the active source +
// exported APIs. For CRD it lists label-gated provider CRDs (the binding pulls
// them later); for OpenAPI it synthesizes CRDs from discovery + /openapi/v3 and
// installs them on the consumer (CRD-less providers like kcp). Auto probes CRD
// first and falls back to OpenAPI.
// exported APIs. It NEVER installs here: installation is gated by pullPolicy
// (All: eager in Reconcile; Bound: the binding installs only referenced APIs;
// None: never), identically for both sources. For CRD it lists label-gated
// provider CRDs; for OpenAPI it synthesizes candidate CRDs from discovery +
// /openapi/v3 (CRD-less providers like kcp) purely to populate exportedAPIs.
// Auto probes CRD first and falls back to OpenAPI.
func (r *Reconciler) discoverAndInstall(ctx context.Context, conn *corev1alpha1.Connection, providerClient client.Client) (corev1alpha1.SchemaSource, []corev1alpha1.ExportedAPI, error) {
src := conn.Spec.Schema.Source
if src == "" {
Expand All @@ -481,7 +501,12 @@ func (r *Reconciler) discoverAndInstall(ctx context.Context, conn *corev1alpha1.
// Auto with no labeled CRDs → fall through to OpenAPI.
}

// OpenAPI: synthesize CRDs from discovery + /openapi/v3 and install them.
// OpenAPI: synthesize candidate CRDs from discovery + /openapi/v3 to populate
// exportedAPIs. Do NOT install here — installation is pullPolicy-gated (see
// Reconcile for All, the binding for Bound). This keeps the provider's
// built-in groups (e.g. kcp's *.kcp.io) off the consumer unless a binding
// references them, while still letting the binding go Ready (the API is in
// exportedAPIs) so status + related-resource sync work.
cfg, err := remote.RestConfigFromConnection(ctx, r.Client, conn)
if err != nil {
return "", nil, err
Expand All @@ -492,9 +517,6 @@ func (r *Reconciler) discoverAndInstall(ctx context.Context, conn *corev1alpha1.
}
exported := make([]corev1alpha1.ExportedAPI, 0, len(crds))
for _, crd := range crds {
if _, err := crdpull.Install(ctx, r.Client, crd, conn.Name, conn.Spec.Schema.UpdatePolicy != corev1alpha1.UpdatePolicyOnce); err != nil {
return "", nil, fmt.Errorf("installing synthesized CRD %q: %w", crd.Name, err)
}
versions := make([]string, len(crd.Spec.Versions))
for i, v := range crd.Spec.Versions {
versions[i] = v.Name
Expand Down
16 changes: 16 additions & 0 deletions engine/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ func SynthesizeCRDs(ctx context.Context, cfg *rest.Config) ([]*apiextensionsv1.C
return out, nil
}

// SynthesizeCRD synthesizes the single CRD named "<plural>.<group>", for
// on-demand install of one bound API (schema.source: OpenAPI, pullPolicy: Bound).
// Returns an error if the provider does not export that resource.
func SynthesizeCRD(ctx context.Context, cfg *rest.Config, name string) (*apiextensionsv1.CustomResourceDefinition, error) {
crds, err := SynthesizeCRDs(ctx, cfg)
if err != nil {
return nil, err
}
for _, crd := range crds {
if crd.Name == name {
return crd, nil
}
}
return nil, fmt.Errorf("provider does not export %q", name)
}

func buildCRD(gr schema.GroupResource, versions []crdVersionInfo, preferredVer string) *apiextensionsv1.CustomResourceDefinition {
// Keep deterministic order so that we don't trigger updates without actually changing the schema.
sort.SliceStable(versions, func(i, j int) bool {
Expand Down
21 changes: 15 additions & 6 deletions test/e2e/schema_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,23 @@ func TestSlimCoreOpenAPISource(t *testing.T) {
return conn.Status.Conditions, err
}, corev1alpha1.ConditionReady)

t.Run("Connection synthesizes and installs the CRD via OpenAPI", func(t *testing.T) {
t.Run("Connection discovers via OpenAPI but installs no unbound CRD (pullPolicy: Bound)", func(t *testing.T) {
conn := &corev1alpha1.Connection{}
require.NoError(t, env.ConsumerClient.Get(ctx, client.ObjectKey{Name: "demo-provider"}, conn))
require.Equal(t, corev1alpha1.SchemaSourceOpenAPI, conn.Status.ActiveSchemaSource)
_, ok := conn.Status.ExportsAPI(widgetCRDName)
require.True(t, ok, "OpenAPI discovery should export %s", widgetCRDName)

// Under the default pullPolicy: Bound the Connection populates
// exportedAPIs but installs nothing — install is deferred to a binding.
// (This keeps a provider's built-in groups, e.g. kcp's *.kcp.io, off the
// consumer unless a binding references them.)
crd := &apiextensionsv1.CustomResourceDefinition{}
require.Eventually(t, func() bool {
return env.ConsumerClient.Get(ctx, client.ObjectKey{Name: widgetCRDName}, crd) == nil
}, 30*time.Second, 200*time.Millisecond, "the synthesized CRD should be installed on the consumer")
require.Equal(t, "true", crd.Labels[corev1alpha1.LabelManaged])
err := env.ConsumerClient.Get(ctx, client.ObjectKey{Name: widgetCRDName}, crd)
require.True(t, apierrors.IsNotFound(err), "the synthesized CRD must not be installed before a binding references it")
})

t.Run("a binding syncs an instance over the synthesized CRD", func(t *testing.T) {
t.Run("a binding installs the synthesized CRD and syncs an instance", func(t *testing.T) {
require.NoError(t, env.ConsumerClient.Create(ctx, &corev1alpha1.ClusterBinding{
ObjectMeta: metav1.ObjectMeta{Name: "widgets"},
Spec: corev1alpha1.BindingSpec{
Expand All @@ -81,6 +83,13 @@ func TestSlimCoreOpenAPISource(t *testing.T) {
return cb.Status.Conditions, err
}, corev1alpha1.ConditionReady)

// The binding installed the synthesized CRD on demand (pullPolicy: Bound).
crd := &apiextensionsv1.CustomResourceDefinition{}
require.Eventually(t, func() bool {
return env.ConsumerClient.Get(ctx, client.ObjectKey{Name: widgetCRDName}, crd) == nil
}, 30*time.Second, 200*time.Millisecond, "the binding should install the synthesized CRD")
require.Equal(t, "true", crd.Labels[corev1alpha1.LabelManaged])

consumerWidgets := env.ConsumerDyn.Resource(gvr).Namespace(instanceNS)
providerWidgets := env.ProviderDyn.Resource(gvr).Namespace(instanceNS)
require.Eventually(t, func() bool {
Expand Down