Skip to content
Merged
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
2 changes: 1 addition & 1 deletion deploy/charts/backend/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.5.1"
appVersion: "v0.6.0"
3 changes: 1 addition & 2 deletions docs/content/setup/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ helm upgrade \
--set 'gatewayApi.route.hostnames[0]=kube-bind.example.com' \
--set gatewayApi.route.path=/ \
--set gatewayApi.route.pathType=PathPrefix \
--set image.tag=${VERSION} \
kube-bind \
--set image.tag=v${VERSION} \
kube-bind oci://ghcr.io/kube-bind/charts/backend --version ${VERSION}
```

Expand Down
1 change: 1 addition & 0 deletions docs/content/usage/.pages
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ nav:
- index.md
- api-concepts.md
- synchronization.md
- integrations
3 changes: 1 addition & 2 deletions docs/content/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ If you're new to kube-bind:

1. **Start with the [Quickstart Guide](../setup/quickstart.md)** for a hands-on introduction
2. **Review [API Concepts](api-concepts.md)** to understand the fundamental types
3. **Explore [Template References](template-references.md)** for advanced use cases
4. **Check the [Reference Documentation](../reference/)** for complete API specifications
3. **Check the [Reference Documentation](../reference/)** for complete API specifications

The konnector agents establish a secure, authenticated connection that allows:

Expand Down
2 changes: 2 additions & 0 deletions docs/content/usage/integrations/.pages
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nav:
- cert-manager.md
103 changes: 103 additions & 0 deletions docs/content/usage/integrations/cert-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Cert-Manager
description: |
Guide on integrating kube-bind with cert-manager for automated TLS certificate management.
weight: 10
---

# Cert-Manager Integration

1. **Install cert-manager** in your Kubernetes cluster, where kube-bind backend is running, if you haven't already. You can follow the official installation guide [here](https://cert-manager.io/docs/installation/kubernetes/).


2. **Create a `kube-bind` template for `Certificate` resources** to allow service consumers to request TLS certificates. Below is an example template:

```yaml
kubectl apply -f - <<EOF
apiVersion: kube-bind.io/v1alpha2
kind: APIServiceExportTemplate
metadata:
labels:
provider: cert-manager
name: certificate
spec:
permissionClaims:
- group: ""
resource: secrets
selector:
references:
- resource: certificates
group: cert-manager.io
jsonPath:
name: 'spec.secretName'
resources:
- group: cert-manager.io
resource: certificates
versions:
- v1
scope: Namespaced
EOF
```

3. **Login into the kube-bind CLI** and request a binding to the `certificate` template created above. This will allow you to create `Certificate` resources in your consumer cluster.

```bash
kubectl bind login https://kube-bind.example.com
# you will get redirected to UI to authenticate and pick the template
kubectl bind
```

4. **Wait for the binding to be established.** Once the binding is active, you can create `Certificate` resources in your consumer cluster, and you will get `Certificate` objects synced from the provider cluster.

```bash
kubectl bind
🌐 Opening kube-bind UI in your browser...
https://kube-bind.genericcontrolplane.io?redirect_url=....

Browser opened successfully
Waiting for binding completion from UI...
(Press Ctrl+C to cancel)

Binding completed successfully!
Created kube-bind namespace.
🔒 Created secret kube-bind/kubeconfig-p6mfh for host https://api.kcp-prod.kcp.internal.canary.k8s.ondemand.com:443, namespace kube-bind-dkxkx
🚀 Deploying konnector v0.6.0 to namespace kube-bind with custom image "ghcr.io/kube-bind/konnector:v0.6.0-rc1".
Waiting for the ...................
✅ Created APIServiceBinding certificate for 1 resources
Created 1 APIServiceBinding(s):
- certificate
Resources bound successfully!
```

5. **Create a `Certificate` resource** in your consumer cluster. The cert-manager in the provider cluster will handle the issuance and management of the TLS certificate.

!!! note
my-selfsigned-issuer must be present in the provider cluster for this example to work.

```yaml
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-tls-cert
namespace: default
spec:
commonName: my-ca
isCA: true
issuerRef:
kind: ClusterIssuer
name: my-selfsigned-issuer
secretName: my-tls-cert
EOF
```

6. Observe that the `Certificate` resource is created in the consumer cluster and the corresponding TLS secret is generated.

```bash
kubectl get certificates
NAME READY SECRET AGE
my-tls-cert True my-tls-cert 6m55s
kubectl get secrets
NAME TYPE DATA AGE
my-tls-cert kubernetes.io/tls 3 6m33s
```
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func (c *controller) enqueueProvider(logger klog.Logger, obj interface{}) {
return
}
if !c.isClaimed(logger, o, false) {
logger.V(4).Info("object is not claimed, skipping", "object", o.GetObjectKind().GroupVersionKind(), "name", o.GetName())
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (c *controller) enqueueProvider(logger klog.Logger, obj any) {
// Try to map the provider name back to the consumer name,
// but only to check if we "own" the object; we will actually
// enqueue the provider key after all.
consumerKey, err := c.reconciler.isolationStrategy.ToConsumerKey(types.NamespacedName{
consumerKey, err := c.isolationStrategy.ToConsumerKey(types.NamespacedName{
Namespace: ns,
Name: name,
})
Expand Down Expand Up @@ -216,7 +216,7 @@ func (c *controller) enqueueConsumer(logger klog.Logger, obj any) {
return
}

providerKey, err := c.reconciler.isolationStrategy.ToProviderKey(types.NamespacedName{
providerKey, err := c.isolationStrategy.ToProviderKey(types.NamespacedName{
Namespace: ns,
Name: name,
})
Expand Down Expand Up @@ -250,7 +250,10 @@ func (c *controller) enqueueServiceNamespace(logger klog.Logger, obj any) {
return // not for us
}

strategy := c.reconciler.isolationStrategy.(*isolation.ServiceNamespacedStrategy)
strategy, ok := c.isolationStrategy.(*isolation.ServiceNamespacedStrategy)
if !ok { // should not happen... but...
return
}
nsOnProviderCluster, err := strategy.ProviderNamespace(name)
if err != nil {
runtime.HandleError(err)
Expand Down Expand Up @@ -289,7 +292,7 @@ func (c *controller) Start(ctx context.Context, numThreads int) {
// APIServiceNamespaces are only of interest when syncing namespaced
// objects, and since these event handlers need the appropriate isolation
// strategy, we only start them when necessary.
if _, ok := c.reconciler.isolationStrategy.(*isolation.ServiceNamespacedStrategy); ok {
if _, ok := c.isolationStrategy.(*isolation.ServiceNamespacedStrategy); ok {
c.serviceNamespaceInformer.Informer().AddDynamicEventHandler(ctx, controllerName, cache.ResourceEventHandlerFuncs{
AddFunc: func(obj any) {
c.enqueueServiceNamespace(logger, obj)
Expand Down
15 changes: 11 additions & 4 deletions pkg/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func matchesReferences(logger klog.Logger, references []kubebindv1alpha2.Selecto
return false
}

logger.V(4).Info("checking references", "references", references, "object", obj, "potentiallyReferencedResources", potentiallyReferencedResources)

if potentiallyReferencedResources == nil {
return false
}
Expand Down Expand Up @@ -156,6 +158,7 @@ func IsClaimed(logger klog.Logger, selector kubebindv1alpha2.Selector, obj *unst
}
// Empty selector selects nothing
if selector.LabelSelector == nil && len(selector.NamedResources) == 0 && len(selector.References) == 0 {
logger.V(4).Info("no selector provided")
return false
}

Expand All @@ -178,7 +181,7 @@ func IsClaimedWithReference(
consumerClient dynamicclient.Interface,
serviceNamespaceLister ServiceNamespaceLister,
) bool {
logger = logger.V(4).WithValues("gvr", obj.GroupVersionKind().String(), "namespace", obj.GetNamespace(), "name", obj.GetName())
logger = logger.V(4).WithValues("gvr", obj.GroupVersionKind().String(), "namespace", obj.GetNamespace(), "name", obj.GetName(), "claim", claim)
logger.Info("checking if object is claimed")
copy := obj.DeepCopy()

Expand Down Expand Up @@ -208,11 +211,14 @@ func IsClaimedWithReference(
defer cancel()

for _, version := range versions {
objs, err := consumerClient.Resource(schema.GroupVersionResource{
gvr := schema.GroupVersionResource{
Comment thread
mjudeikis marked this conversation as resolved.
Group: ref.Group,
Resource: ref.Resource,
Version: version,
}).List(ctx, metav1.ListOptions{})
}
logger.V(4).Info("listing objects for reference claim", "gvr", gvr)

objs, err := consumerClient.Resource(gvr).List(ctx, metav1.ListOptions{})
if err != nil {
logger.Error(err, "failed to list objects for reference claim. Invalidating all claim.", "group", ref.Group, "resource", ref.Resource, "version", version)
return false
Expand Down Expand Up @@ -248,10 +254,11 @@ func IsClaimedWithReference(
}
}
if sn != nil {
logger.V(4).Info("remapping namespace for provider side claimed check", "originalNamespace", copy.GetNamespace(), "remappedNamespace", sn.Name)
copy.SetNamespace(sn.Name)
}

result := IsClaimed(logger, claim.Selector, copy, potentiallyReferencedResources)
logger.Info("IsClaimed result (provider side)", "result", result)
logger.V(4).Info("IsClaimed result (provider side)", "result", result)
return result
}
148 changes: 148 additions & 0 deletions pkg/resources/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,154 @@ func TestSelector_IsClaimed(t *testing.T) {
},
want: false, // Should NOT match because it doesn't have app=sheriff label and is not referenced by Sheriff
},
{
name: "cert-manager secret reference - should match when certificate references secret via secretName",
potentiallyReferencedResources: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]any{
"apiVersion": "cert-manager.io/v1",
"kind": "Certificate",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "default",
},
"spec": map[string]any{
"secretName": "my-tls-cert",
"commonName": "my-ca",
"isCA": true,
"issuerRef": map[string]any{
"kind": "ClusterIssuer",
"name": "kcp-ca",
},
},
},
},
},
},
selector: kubebindv1alpha2.Selector{
References: []kubebindv1alpha2.SelectorReference{
{
GroupResource: kubebindv1alpha2.GroupResource{
Group: "cert-manager.io",
Resource: "certificates",
},
JSONPath: &kubebindv1alpha2.JSONPath{
Name: "spec.secretName",
},
},
},
},
obj: &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "default",
"annotations": map[string]any{
"cert-manager.io/certificate-name": "my-tls-cert",
},
},
"type": "kubernetes.io/tls",
},
},
want: true, // Should match because certificate's .spec.secretName equals secret name and no namespace JSONPath means namespace matching is handled by caller
},
{
name: "cert-manager secret reference - should not match when certificate references different secret",
potentiallyReferencedResources: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]any{
"apiVersion": "cert-manager.io/v1",
"kind": "Certificate",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "default",
},
"spec": map[string]any{
"secretName": "other-secret",
"commonName": "my-ca",
"isCA": true,
},
},
},
},
},
selector: kubebindv1alpha2.Selector{
References: []kubebindv1alpha2.SelectorReference{
{
GroupResource: kubebindv1alpha2.GroupResource{
Group: "cert-manager.io",
Resource: "certificates",
},
JSONPath: &kubebindv1alpha2.JSONPath{
Name: "spec.secretName",
},
},
},
},
obj: &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "default",
},
"type": "kubernetes.io/tls",
},
},
want: false, // Should not match because certificate's .spec.secretName is "other-secret", not "my-tls-cert"
},
{
name: "cert-manager secret reference - cross-namespace scenario with no namespace JSONPath",
potentiallyReferencedResources: &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]any{
"apiVersion": "cert-manager.io/v1",
"kind": "Certificate",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "default",
},
"spec": map[string]any{
"secretName": "my-tls-cert",
"commonName": "my-ca",
"isCA": true,
},
},
},
},
},
selector: kubebindv1alpha2.Selector{
References: []kubebindv1alpha2.SelectorReference{
{
GroupResource: kubebindv1alpha2.GroupResource{
Group: "cert-manager.io",
Resource: "certificates",
},
JSONPath: &kubebindv1alpha2.JSONPath{
Name: "spec.secretName",
},
},
},
},
obj: &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]any{
"name": "my-tls-cert",
"namespace": "remapped-namespace", // Different namespace simulating provider-side remapping
},
"type": "kubernetes.io/tls",
},
},
want: true, // Should match because when no namespace JSONPath is provided, namespace matching is delegated to caller
},
}

for _, tt := range tests {
Expand Down