Skip to content

Commit 6405a60

Browse files
committed
Allow UI binding flow
Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> On-behalf-of: @SAP karol.szwaj@sap.com
1 parent f51dbcf commit 6405a60

5 files changed

Lines changed: 337 additions & 226 deletions

File tree

backend/http/handler.go

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

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

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

@@ -412,6 +499,22 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
412499
},
413500
}
414501

502+
// Create the APIServiceExportRequest on the provider cluster and wait for reconciliation.
503+
// This ensures the binding is fully set up server-side for both CLI and UI flows.
504+
exportRequest, err := h.kubeManager.CreateAPIServiceExportRequest(
505+
r.Context(),
506+
params.ClusterID,
507+
handleResult.Namespace,
508+
bindRequest.Name,
509+
request.Spec,
510+
)
511+
if err != nil {
512+
logger.Error(err, "failed to create APIServiceExportRequest")
513+
statusCode, code, details := mapErrorToCode(err)
514+
writeErrorResponse(w, statusCode, code, "Failed to create API service export request", details)
515+
return
516+
}
517+
415518
// callback response
416519
requestBytes, err := json.Marshal(&request)
417520
if err != nil {
@@ -431,8 +534,10 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
431534
ID: state.Token.Issuer + "/" + state.Token.Subject,
432535
},
433536
},
434-
Kubeconfig: handleResult.Kubeconfig,
435-
Requests: []runtime.RawExtension{{Raw: requestBytes}},
537+
Kubeconfig: handleResult.Kubeconfig,
538+
Requests: []runtime.RawExtension{{Raw: requestBytes}},
539+
ProviderNamespace: handleResult.Namespace,
540+
BindingName: exportRequest.Name,
436541
}
437542

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

backend/kubernetes/manager.go

Lines changed: 68 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,72 @@ 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
219+
var result *kubebindv1alpha2.APIServiceExportRequest
220+
if err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) {
221+
req := &kubebindv1alpha2.APIServiceExportRequest{}
222+
if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: createdName}, req); err != nil {
223+
if errors.IsNotFound(err) {
224+
return false, fmt.Errorf("APIServiceExportRequest %s was deleted", createdName)
225+
}
226+
return false, err
227+
}
228+
if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhaseSucceeded {
229+
result = req
230+
return true, nil
231+
}
232+
if req.Status.Phase == kubebindv1alpha2.APIServiceExportRequestPhaseFailed {
233+
return false, fmt.Errorf("APIServiceExportRequest failed: %s", req.Status.TerminalMessage)
234+
}
235+
return false, nil
236+
}); err != nil {
237+
return nil, fmt.Errorf("waiting for APIServiceExportRequest: %w", err)
238+
}
239+
240+
logger.Info("APIServiceExportRequest reconciled successfully")
241+
return result, nil
242+
}
243+
176244
func (m *Manager) ListCustomResourceDefinitions(ctx context.Context, cluster string, selector labels.Selector) (*apiextensionsv1.CustomResourceDefinitionList, error) {
177245
cl, err := m.manager.GetCluster(ctx, cluster)
178246
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)