Skip to content

Commit 5a0cba1

Browse files
committed
add two ways of UI binding
Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> On-behalf-of: @SAP karol.szwaj@sap.com
1 parent e00d253 commit 5a0cba1

9 files changed

Lines changed: 14291 additions & 334 deletions

File tree

backend/http/handler.go

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ func (h *handler) AddRoutes(mux *mux.Router) error {
149149
// Public API routes (no authentication required)
150150
mux.HandleFunc("/api/healthz", h.handleHealthz).Methods(http.MethodGet)
151151
mux.HandleFunc("/api/bindable-resources", h.handleBindableResources).Methods(http.MethodGet)
152+
mux.HandleFunc("/api/konnector-manifests", h.handleKonnectorManifests).Methods(http.MethodGet)
152153

153154
// Generic authentication routes (support both UI and CLI)
154155
mux.HandleFunc("/api/authorize", h.authHandler.HandleAuthorize).Methods(http.MethodGet, http.MethodPost)
@@ -197,6 +198,92 @@ func (h *handler) handlePing(w http.ResponseWriter, r *http.Request) {
197198
w.Write([]byte("pong")) //nolint:errcheck
198199
}
199200

201+
// handleKonnectorManifests returns the pre-rendered konnector YAML manifests
202+
// that a consumer cluster needs to apply to deploy the konnector agent.
203+
func (h *handler) handleKonnectorManifests(w http.ResponseWriter, r *http.Request) {
204+
prepareNoCache(w)
205+
206+
konnectorVersion, err := bindversion.BinaryVersion(componentbaseversion.Get().GitVersion)
207+
if err != nil {
208+
konnectorVersion = "latest"
209+
}
210+
konnectorImage := fmt.Sprintf("ghcr.io/kube-bind/konnector:%s", konnectorVersion)
211+
212+
manifests := fmt.Sprintf(`apiVersion: v1
213+
kind: Namespace
214+
metadata:
215+
name: kube-bind
216+
---
217+
apiVersion: v1
218+
kind: ServiceAccount
219+
metadata:
220+
name: konnector
221+
namespace: kube-bind
222+
---
223+
apiVersion: rbac.authorization.k8s.io/v1
224+
kind: ClusterRole
225+
metadata:
226+
name: kube-bind-konnector
227+
rules:
228+
- apiGroups: ["*"]
229+
resources: ["*"]
230+
verbs: ["*"]
231+
---
232+
apiVersion: rbac.authorization.k8s.io/v1
233+
kind: ClusterRoleBinding
234+
metadata:
235+
name: kube-bind-konnector
236+
roleRef:
237+
apiGroup: rbac.authorization.k8s.io
238+
kind: ClusterRole
239+
name: kube-bind-konnector
240+
subjects:
241+
- kind: ServiceAccount
242+
name: konnector
243+
namespace: kube-bind
244+
---
245+
apiVersion: apps/v1
246+
kind: Deployment
247+
metadata:
248+
name: konnector
249+
namespace: kube-bind
250+
labels:
251+
app: konnector
252+
spec:
253+
replicas: 2
254+
selector:
255+
matchLabels:
256+
app: konnector
257+
template:
258+
metadata:
259+
labels:
260+
app: konnector
261+
spec:
262+
restartPolicy: Always
263+
serviceAccountName: konnector
264+
containers:
265+
- name: konnector
266+
image: %s
267+
env:
268+
- name: POD_NAME
269+
valueFrom:
270+
fieldRef:
271+
fieldPath: metadata.name
272+
- name: POD_NAMESPACE
273+
valueFrom:
274+
fieldRef:
275+
fieldPath: metadata.namespace
276+
readinessProbe:
277+
httpGet:
278+
path: /healthz
279+
port: 8090
280+
`, konnectorImage)
281+
282+
w.Header().Set("Content-Type", "text/yaml")
283+
w.Header().Set("Content-Disposition", "attachment; filename=konnector.yaml")
284+
w.Write([]byte(manifests)) //nolint:errcheck
285+
}
286+
200287
func (h *handler) handleLogout(w http.ResponseWriter, r *http.Request) {
201288
prepareNoCache(w)
202289

@@ -368,7 +455,8 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
368455
}
369456

370457
// Resolve the UI sentinel to a real identity derived from the authenticated session.
371-
if identity == auth.UIIdentity {
458+
isUIFlow := identity == auth.UIIdentity
459+
if isUIFlow {
372460
identity = state.Token.Issuer + "/" + state.Token.Subject
373461
logger.Info("Resolved ui-identity from session", "identity", identity)
374462
}
@@ -411,6 +499,26 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
411499
},
412500
}
413501

502+
// For UI-only flow, create the APIServiceExportRequest on the provider cluster
503+
// and wait for reconciliation. In CLI flow the konnector handles this instead.
504+
var exportRequestName string
505+
if isUIFlow {
506+
exportRequest, err := h.kubeManager.CreateAPIServiceExportRequest(
507+
r.Context(),
508+
params.ClusterID,
509+
handleResult.Namespace,
510+
bindRequest.Name,
511+
request.Spec,
512+
)
513+
if err != nil {
514+
logger.Error(err, "failed to create APIServiceExportRequest")
515+
statusCode, code, details := mapErrorToCode(err)
516+
writeErrorResponse(w, statusCode, code, "Failed to create API service export request", details)
517+
return
518+
}
519+
exportRequestName = exportRequest.Name
520+
}
521+
414522
// callback response
415523
requestBytes, err := json.Marshal(&request)
416524
if err != nil {
@@ -430,8 +538,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
430538
ID: state.Token.Issuer + "/" + state.Token.Subject,
431539
},
432540
},
433-
Kubeconfig: handleResult.Kubeconfig,
434-
Requests: []runtime.RawExtension{{Raw: requestBytes}},
541+
Kubeconfig: handleResult.Kubeconfig,
542+
Requests: []runtime.RawExtension{{Raw: requestBytes}},
543+
ProviderNamespace: handleResult.Namespace,
544+
BindingName: exportRequestName,
435545
}
436546

437547
payload, err := json.Marshal(&response)

backend/kubernetes/manager.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"strings"
23+
"time"
2324

2425
authzv1 "k8s.io/api/authorization/v1"
2526
corev1 "k8s.io/api/core/v1"
@@ -30,6 +31,7 @@ import (
3031
"k8s.io/apimachinery/pkg/labels"
3132
"k8s.io/apimachinery/pkg/runtime/schema"
3233
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/wait"
3335
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
3436
"k8s.io/klog/v2"
3537
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -173,6 +175,80 @@ func (m *Manager) HandleResources(
173175
}, nil
174176
}
175177

178+
// CreateAPIServiceExportRequest creates an APIServiceExportRequest in the given namespace
179+
// on the provider cluster and waits for it to be reconciled (Succeeded or Failed).
180+
func (m *Manager) CreateAPIServiceExportRequest(
181+
ctx context.Context,
182+
cluster, namespace, name string,
183+
spec kubebindv1alpha2.APIServiceExportRequestSpec,
184+
) (*kubebindv1alpha2.APIServiceExportRequest, error) {
185+
logger := klog.FromContext(ctx).WithValues("namespace", namespace, "name", name)
186+
187+
cl, err := m.manager.GetCluster(ctx, cluster)
188+
if err != nil {
189+
return nil, fmt.Errorf("failed to get cluster client: %w", err)
190+
}
191+
c := cl.GetClient()
192+
193+
exportRequest := &kubebindv1alpha2.APIServiceExportRequest{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: name,
196+
Namespace: namespace,
197+
},
198+
Spec: spec,
199+
}
200+
201+
// Create the APIServiceExportRequest, handling name conflicts
202+
if err := c.Create(ctx, exportRequest); err != nil {
203+
if !errors.IsAlreadyExists(err) {
204+
return nil, fmt.Errorf("failed to create APIServiceExportRequest: %w", err)
205+
}
206+
// Name conflict: use generateName
207+
exportRequest.Name = ""
208+
exportRequest.GenerateName = name + "-"
209+
if err := c.Create(ctx, exportRequest); err != nil {
210+
return nil, fmt.Errorf("failed to create APIServiceExportRequest with generated name: %w", err)
211+
}
212+
}
213+
214+
createdName := exportRequest.Name
215+
logger = logger.WithValues("createdName", createdName)
216+
logger.Info("Created APIServiceExportRequest, waiting for reconciliation")
217+
218+
// Poll until reconciled. The client reads from cache, so the object may not
219+
// be visible immediately after creation. Tolerate NotFound for an initial
220+
// grace period before treating it as a real deletion.
221+
var result *kubebindv1alpha2.APIServiceExportRequest
222+
seenOnce := false
223+
if err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 60*time.Second, false, func(ctx context.Context) (bool, error) {
224+
req := &kubebindv1alpha2.APIServiceExportRequest{}
225+
if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: createdName}, req); err != nil {
226+
if errors.IsNotFound(err) {
227+
if seenOnce {
228+
return false, fmt.Errorf("APIServiceExportRequest %s was deleted", createdName)
229+
}
230+
// Cache hasn't synced yet — keep polling.
231+
return false, nil
232+
}
233+
return false, err
234+
}
235+
seenOnce = true
236+
if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhaseSucceeded {
237+
result = req
238+
return true, nil
239+
}
240+
if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhaseFailed {
241+
return false, fmt.Errorf("APIServiceExportRequest failed: %s", req.Status.TerminalMessage)
242+
}
243+
return false, nil
244+
}); err != nil {
245+
return nil, fmt.Errorf("waiting for APIServiceExportRequest: %w", err)
246+
}
247+
248+
logger.Info("APIServiceExportRequest reconciled successfully")
249+
return result, nil
250+
}
251+
176252
func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster string, selector labels.Selector) (*apiextensionsv1.CustomResourceDefinitionList, error) {
177253
cl, err := m.manager.GetCluster(ctx, cluster)
178254
if err != nil {

sdk/apis/kubebind/v1alpha2/bindingresponse_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ type BindingResourceResponse struct {
5555
// +kubebuilder:validation:Required
5656
// +kubebuilder:validation:MinItems=1
5757
Requests []runtime.RawExtension `json:"requests"`
58+
59+
// providerNamespace is the namespace on the service provider cluster where the
60+
// binding resources (APIServiceExport, BoundSchema, etc.) are created and managed.
61+
// This is set when the binding is created via the UI flow.
62+
//
63+
// +optional
64+
// +kubebuilder:validation:Optional
65+
ProviderNamespace string `json:"providerNamespace,omitempty"`
66+
67+
// bindingName is the confirmed name for this binding, as created on the service
68+
// provider cluster.
69+
//
70+
// +optional
71+
// +kubebuilder:validation:Optional
72+
BindingName string `json:"bindingName,omitempty"`
5873
}
5974

6075
// BindingResponseAuthentication is the authentication data specific to the

0 commit comments

Comments
 (0)