Skip to content

Commit d84c3f0

Browse files
authored
Backend only mode (#445)
* Add backend only mode Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt> On-behalf-of: @SAP mangirdas.judeikis@sap.com * simplify doc * address reviews --------- Signed-off-by: Mangirdas Judeikis <mangirdas@judeikis.lt>
1 parent d15b44c commit d84c3f0

58 files changed

Lines changed: 4306 additions & 214 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
/*
2+
Copyright 2025 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 bindableresourcesrequest
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"reflect"
24+
"time"
25+
26+
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/api/meta"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/types"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/controller"
34+
"sigs.k8s.io/controller-runtime/pkg/log"
35+
mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
36+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
37+
mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile"
38+
39+
"github.com/kube-bind/kube-bind/backend/kubernetes"
40+
"github.com/kube-bind/kube-bind/pkg/indexers"
41+
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
42+
)
43+
44+
const (
45+
controllerName = "kube-bind-backend-bindableresourcesrequest"
46+
)
47+
48+
// BindableResourcesRequestReconciler reconciles a BindableResourcesRequest object.
49+
// This controller handles direct binding requests created as CRDs when the
50+
// backend is running without the HTTP API/OIDC flow (frontend disabled mode).
51+
//
52+
// The controller:
53+
// 1. Reads the kubeconfig from the referenced secret
54+
// 2. Creates a BindingResourceResponse secret with the kubeconfig
55+
// 3. Creates an APIServiceExportRequest based on the template
56+
type BindableResourcesRequestReconciler struct {
57+
manager mcmanager.Manager
58+
opts controller.TypedOptions[mcreconcile.Request]
59+
60+
informerScope kubebindv1alpha2.InformerScope
61+
isolation kubebindv1alpha2.Isolation
62+
reconciler reconciler
63+
}
64+
65+
// NewBindableResourcesRequestReconciler returns a new BindableResourcesRequestReconciler.
66+
func NewBindableResourcesRequestReconciler(
67+
ctx context.Context,
68+
mgr mcmanager.Manager,
69+
opts controller.TypedOptions[mcreconcile.Request],
70+
scope kubebindv1alpha2.InformerScope,
71+
isolation kubebindv1alpha2.Isolation,
72+
kubeManager *kubernetes.Manager,
73+
) (*BindableResourcesRequestReconciler, error) {
74+
// Set up field indexers for BindableResourcesRequests
75+
if err := mgr.GetFieldIndexer().IndexField(ctx, &kubebindv1alpha2.BindableResourcesRequest{}, indexers.BindableResourcesRequestByTemplate,
76+
indexers.IndexBindableResourcesRequestByTemplate); err != nil {
77+
return nil, fmt.Errorf("failed to setup BindableResourcesRequestByTemplate indexer: %w", err)
78+
}
79+
80+
r := &BindableResourcesRequestReconciler{
81+
manager: mgr,
82+
opts: opts,
83+
informerScope: scope,
84+
isolation: isolation,
85+
reconciler: reconciler{
86+
informerScope: scope,
87+
isolation: isolation,
88+
kubeManager: kubeManager,
89+
},
90+
}
91+
92+
return r, nil
93+
}
94+
95+
//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests,verbs=get;list;watch;create;update;patch;delete
96+
//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests/status,verbs=get;update;patch
97+
//+kubebuilder:rbac:groups=kube-bind.io,resources=bindableresourcesrequests/finalizers,verbs=update
98+
//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexportrequests,verbs=get;list;watch;create
99+
//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexporttemplates,verbs=get;list;watch
100+
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update
101+
102+
// Reconcile is part of the main kubernetes reconciliation loop.
103+
func (r *BindableResourcesRequestReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
104+
logger := log.FromContext(ctx)
105+
logger.Info("Reconciling BindableResourcesRequest", "cluster", req.ClusterName, "namespace", req.Namespace, "name", req.Name)
106+
107+
cl, err := r.manager.GetCluster(ctx, req.ClusterName)
108+
if err != nil {
109+
return ctrl.Result{}, fmt.Errorf("failed to get client for cluster %q: %w", req.ClusterName, err)
110+
}
111+
112+
client := cl.GetClient()
113+
114+
// Fetch the BindableResourcesRequest instance
115+
bindableRequest := &kubebindv1alpha2.BindableResourcesRequest{}
116+
if err := client.Get(ctx, req.NamespacedName, bindableRequest); err != nil {
117+
if errors.IsNotFound(err) {
118+
logger.Info("BindableResourcesRequest not found, ignoring")
119+
return ctrl.Result{}, nil
120+
}
121+
return ctrl.Result{}, fmt.Errorf("failed to get BindableResourcesRequest: %w", err)
122+
}
123+
124+
// Check TTL-based deletion for completed (ttl) requests
125+
if bindableRequest.Status.Phase == kubebindv1alpha2.BindableResourcesRequestPhaseSucceeded ||
126+
bindableRequest.Status.Phase == kubebindv1alpha2.BindableResourcesRequestPhaseFailed {
127+
return r.handleTTL(ctx, client, bindableRequest)
128+
}
129+
130+
// Create a copy to modify
131+
original := bindableRequest.DeepCopy()
132+
133+
// Run the reconciliation logic
134+
result, err := r.reconciler.reconcile(ctx, req.ClusterName, client, bindableRequest)
135+
if err != nil {
136+
logger.Error(err, "Failed to reconcile BindableResourcesRequest")
137+
if !reflect.DeepEqual(original.Status, bindableRequest.Status) {
138+
if updateErr := client.Status().Update(ctx, bindableRequest); updateErr != nil {
139+
logger.Error(updateErr, "Failed to update BindableResourcesRequest status")
140+
return ctrl.Result{}, fmt.Errorf("failed to update BindableResourcesRequest status: %w", updateErr)
141+
}
142+
}
143+
return ctrl.Result{}, err
144+
}
145+
146+
// Update status if it has changed
147+
if !reflect.DeepEqual(original.Status, bindableRequest.Status) {
148+
if err := client.Status().Update(ctx, bindableRequest); err != nil {
149+
logger.Error(err, "Failed to update BindableResourcesRequest status")
150+
return ctrl.Result{}, fmt.Errorf("failed to update BindableResourcesRequest status: %w", err)
151+
}
152+
logger.Info("BindableResourcesRequest status updated", "namespace", bindableRequest.Namespace, "name", bindableRequest.Name)
153+
}
154+
155+
return result, nil
156+
}
157+
158+
// handleTTL checks if the request should be deleted based on TTL settings.
159+
func (r *BindableResourcesRequestReconciler) handleTTL(ctx context.Context, cl client.Client, req *kubebindv1alpha2.BindableResourcesRequest) (ctrl.Result, error) {
160+
logger := log.FromContext(ctx)
161+
162+
// If no TTL is set, don't delete
163+
if req.Spec.TTLAfterFinished == nil {
164+
return ctrl.Result{}, nil
165+
}
166+
167+
// If no completion time is set, something is wrong - skip
168+
if req.Status.CompletionTime == nil {
169+
return ctrl.Result{}, nil
170+
}
171+
172+
ttl := req.Spec.TTLAfterFinished.Duration
173+
expireTime := req.Status.CompletionTime.Add(ttl)
174+
now := time.Now()
175+
176+
if now.After(expireTime) {
177+
logger.Info("TTL expired, deleting BindableResourcesRequest", "name", req.Name, "namespace", req.Namespace)
178+
if err := cl.Delete(ctx, req); err != nil {
179+
if errors.IsNotFound(err) {
180+
return ctrl.Result{}, nil
181+
}
182+
return ctrl.Result{}, fmt.Errorf("failed to delete expired BindableResourcesRequest: %w", err)
183+
}
184+
return ctrl.Result{}, nil
185+
}
186+
187+
// Requeue to check again when TTL expires
188+
requeueAfter := expireTime.Sub(now)
189+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
190+
}
191+
192+
// SetupWithManager sets up the controller with the Manager.
193+
func (r *BindableResourcesRequestReconciler) SetupWithManager(mgr mcmanager.Manager) error {
194+
return mcbuilder.ControllerManagedBy(mgr).
195+
For(&kubebindv1alpha2.BindableResourcesRequest{}).
196+
Owns(&corev1.Secret{}).
197+
WithOptions(r.opts).
198+
Named(controllerName).
199+
Complete(r)
200+
}
201+
202+
type reconciler struct {
203+
informerScope kubebindv1alpha2.InformerScope
204+
isolation kubebindv1alpha2.Isolation
205+
kubeManager *kubernetes.Manager
206+
}
207+
208+
func (r *reconciler) reconcile(ctx context.Context, clusterName string, cl client.Client, req *kubebindv1alpha2.BindableResourcesRequest) (ctrl.Result, error) {
209+
logger := log.FromContext(ctx)
210+
211+
// Determine the secret name and key to use for the binding response
212+
var secretName, secretKey string
213+
if req.Spec.KubeconfigSecretRef != nil {
214+
// Use the specified secret reference
215+
secretName = req.Spec.KubeconfigSecretRef.Name
216+
secretKey = req.Spec.KubeconfigSecretRef.Key
217+
if secretKey == "" {
218+
secretKey = "kubeconfig"
219+
}
220+
} else {
221+
// Create a new secret with default naming
222+
secretName = req.Name + "-binding-response"
223+
secretKey = "binding-response"
224+
}
225+
226+
// Update status with the secret reference
227+
req.Status.KubeconfigSecretRef = &kubebindv1alpha2.LocalSecretKeyRef{
228+
Name: secretName,
229+
Key: secretKey,
230+
}
231+
232+
// Get the template if specified
233+
var template kubebindv1alpha2.APIServiceExportTemplate
234+
if req.Spec.TemplateRef.Name != "" {
235+
if err := cl.Get(ctx, types.NamespacedName{Name: req.Spec.TemplateRef.Name}, &template); err != nil {
236+
if errors.IsNotFound(err) {
237+
req.Status.Phase = kubebindv1alpha2.BindableResourcesRequestPhaseFailed
238+
now := metav1.Now()
239+
req.Status.CompletionTime = &now
240+
meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{
241+
Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady),
242+
Status: metav1.ConditionFalse,
243+
Reason: "TemplateNotFound",
244+
Message: fmt.Sprintf("APIServiceExportTemplate %q not found", req.Spec.TemplateRef.Name),
245+
LastTransitionTime: now,
246+
})
247+
return ctrl.Result{}, nil // Don't retry - template doesn't exist
248+
}
249+
return ctrl.Result{}, fmt.Errorf("failed to get template %q: %w", req.Spec.TemplateRef.Name, err)
250+
}
251+
}
252+
253+
// Handle resources and get kubeconfig
254+
kfg, err := r.kubeManager.HandleResources(ctx, req.Spec.Author, req.Spec.ClusterIdentity.Identity, clusterName)
255+
if err != nil {
256+
meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{
257+
Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady),
258+
Status: metav1.ConditionFalse,
259+
Reason: "HandleResourcesFailed",
260+
Message: fmt.Sprintf("Failed to handle resources: %v", err),
261+
LastTransitionTime: metav1.Now(),
262+
})
263+
return ctrl.Result{}, fmt.Errorf("failed to handle resources for cluster identity %q: %w", req.Spec.ClusterIdentity.Identity, err)
264+
}
265+
266+
// Create or update the BindingResourceResponse secret
267+
if err := r.ensureBindingResponseSecret(ctx, cl, req, kfg, secretName, secretKey); err != nil {
268+
meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{
269+
Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady),
270+
Status: metav1.ConditionFalse,
271+
Reason: "SecretCreationFailed",
272+
Message: fmt.Sprintf("Failed to create/update secret: %v", err),
273+
LastTransitionTime: metav1.Now(),
274+
})
275+
return ctrl.Result{}, fmt.Errorf("failed to ensure binding response secret: %w", err)
276+
}
277+
278+
// Set success status
279+
now := metav1.Now()
280+
req.Status.Phase = kubebindv1alpha2.BindableResourcesRequestPhaseSucceeded
281+
req.Status.CompletionTime = &now
282+
meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{
283+
Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady),
284+
Status: metav1.ConditionTrue,
285+
Reason: "SecretReady",
286+
Message: fmt.Sprintf("Binding response secret %q is ready", secretName),
287+
LastTransitionTime: now,
288+
})
289+
290+
logger.Info("BindableResourcesRequest succeeded", "name", req.Name, "namespace", req.Namespace, "secret", secretName)
291+
292+
// If TTL is set, requeue for deletion
293+
if req.Spec.TTLAfterFinished != nil {
294+
return ctrl.Result{RequeueAfter: req.Spec.TTLAfterFinished.Duration}, nil
295+
}
296+
297+
return ctrl.Result{}, nil
298+
}
299+
300+
// ensureBindingResponseSecret creates or updates a secret containing the BindingResourceResponse
301+
// with only the kubeconfig set (no authentication or requests).
302+
func (r *reconciler) ensureBindingResponseSecret(
303+
ctx context.Context,
304+
cl client.Client,
305+
req *kubebindv1alpha2.BindableResourcesRequest,
306+
kubeconfig []byte,
307+
secretName string,
308+
secretKey string,
309+
) error {
310+
logger := log.FromContext(ctx)
311+
312+
// Create the BindingResourceResponse with only kubeconfig
313+
response := kubebindv1alpha2.BindingResourceResponse{
314+
TypeMeta: metav1.TypeMeta{
315+
APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(),
316+
Kind: "BindingResourceResponse",
317+
},
318+
Kubeconfig: kubeconfig,
319+
}
320+
321+
responseBytes, err := json.Marshal(&response)
322+
if err != nil {
323+
return fmt.Errorf("failed to marshal BindingResourceResponse: %w", err)
324+
}
325+
326+
// Check if secret already exists
327+
existingSecret := &corev1.Secret{}
328+
err = cl.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, existingSecret)
329+
if err != nil && !errors.IsNotFound(err) {
330+
return fmt.Errorf("failed to get existing secret: %w", err)
331+
}
332+
333+
// Build owner reference - the secret is owned by the BindableResourcesRequest
334+
ownerRef := metav1.OwnerReference{
335+
APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(),
336+
Kind: "BindableResourcesRequest",
337+
Name: req.Name,
338+
UID: req.UID,
339+
Controller: func() *bool { b := true; return &b }(),
340+
}
341+
342+
if errors.IsNotFound(err) {
343+
// Create new secret
344+
secret := &corev1.Secret{
345+
ObjectMeta: metav1.ObjectMeta{
346+
Name: secretName,
347+
Namespace: req.Namespace,
348+
OwnerReferences: []metav1.OwnerReference{ownerRef},
349+
},
350+
Type: corev1.SecretTypeOpaque,
351+
Data: map[string][]byte{
352+
secretKey: responseBytes,
353+
},
354+
}
355+
logger.Info("Creating BindingResourceResponse secret", "name", secretName, "namespace", req.Namespace, "key", secretKey)
356+
if err := cl.Create(ctx, secret); err != nil {
357+
if errors.IsAlreadyExists(err) {
358+
return nil // Race condition, secret was created
359+
}
360+
return fmt.Errorf("failed to create secret: %w", err)
361+
}
362+
} else {
363+
// Update existing secret
364+
// Ensure owner reference is set
365+
hasOwnerRef := false
366+
for _, ref := range existingSecret.OwnerReferences {
367+
if ref.UID == req.UID {
368+
hasOwnerRef = true
369+
break
370+
}
371+
}
372+
if !hasOwnerRef {
373+
existingSecret.OwnerReferences = append(existingSecret.OwnerReferences, ownerRef)
374+
}
375+
376+
// Update data if changed
377+
if existingSecret.Data == nil {
378+
existingSecret.Data = make(map[string][]byte)
379+
}
380+
if string(existingSecret.Data[secretKey]) != string(responseBytes) {
381+
existingSecret.Data[secretKey] = responseBytes
382+
logger.Info("Updating BindingResourceResponse secret", "name", secretName, "namespace", req.Namespace, "key", secretKey)
383+
if err := cl.Update(ctx, existingSecret); err != nil {
384+
return fmt.Errorf("failed to update secret: %w", err)
385+
}
386+
}
387+
}
388+
389+
return nil
390+
}

0 commit comments

Comments
 (0)