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
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,14 @@ func (r *reconciler) reconcile(ctx context.Context, clusterName string, cl clien
}
}

// Default the pretty name to the request name if not provided.
clusterPrettyName := req.Spec.ClusterIdentity.PrettyName
if clusterPrettyName == "" {
clusterPrettyName = req.Name
}

// Handle resources and get kubeconfig
result, err := r.kubeManager.HandleResources(ctx, req.Spec.Author, req.Spec.ClusterIdentity.Identity, clusterName)
result, err := r.kubeManager.HandleResources(ctx, req.Spec.Author, req.Spec.ClusterIdentity.Identity, clusterName, clusterPrettyName)
if err != nil {
meta.SetStatusCondition(&req.Status.Conditions, metav1.Condition{
Type: string(kubebindv1alpha2.BindableResourcesRequestConditionReady),
Expand Down
9 changes: 7 additions & 2 deletions backend/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,12 @@ func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {
consumerID = identity
}

handleResult, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject, consumerID, params.ClusterID)
clusterPrettyName := bindRequest.Spec.ClusterIdentity.PrettyName
if clusterPrettyName == "" {
clusterPrettyName = bindRequest.Name
}

handleResult, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject, consumerID, params.ClusterID, clusterPrettyName)
if err != nil {
logger.Error(err, "failed to handle resources")
statusCode, code, details := mapErrorToCode(err)
Expand Down Expand Up @@ -599,7 +604,7 @@ func (h *handler) handleApplyBinding(w http.ResponseWriter, r *http.Request) {
}

// Get the provider kubeconfig for this user's namespace
handleResult, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject, identity, params.ClusterID)
handleResult, err := h.kubeManager.HandleResources(r.Context(), state.Token.Subject, identity, params.ClusterID, req.BindingName)
if err != nil {
logger.Error(err, "failed to handle resources for apply-binding")
statusCode, code, details := mapErrorToCode(err)
Expand Down
4 changes: 2 additions & 2 deletions backend/kubernetes/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ type HandleResourcesResult struct {

func (m *Manager) HandleResources(
ctx context.Context,
author, identity, cluster string,
author, identity, cluster, clusterPrettyName string,
) (*HandleResourcesResult, error) {
logger := klog.FromContext(ctx).WithValues("identity", identity)
ctx = klog.NewContext(ctx, logger)
Expand Down Expand Up @@ -148,7 +148,7 @@ func (m *Manager) HandleResources(
err = c.Get(ctx, types.NamespacedName{Namespace: ns, Name: kuberesources.ClusterBindingName}, &cb)
switch {
case errors.IsNotFound(err):
if err := kuberesources.CreateClusterBinding(ctx, c, ns, "kubeconfig", m.providerPrettyName); err != nil {
if err := kuberesources.CreateClusterBinding(ctx, c, ns, "kubeconfig", m.providerPrettyName, identity, author, clusterPrettyName); err != nil {
return nil, err
}
case err != nil:
Expand Down
18 changes: 15 additions & 3 deletions backend/kubernetes/resources/cluster_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,25 @@ import (
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
)

func CreateClusterBinding(ctx context.Context, client client.Client, ns, secretName, providerPrettyName string) error {
func CreateClusterBinding(ctx context.Context, client client.Client, ns, secretName, providerPrettyName, identity, author, clusterPrettyName string) error {
logger := klog.FromContext(ctx)

annotations := map[string]string{}
if identity != "" {
annotations[IdentityAnnotationKey] = identity
}
if author != "" {
annotations[AuthorAnnotationKey] = author
}
if clusterPrettyName != "" {
annotations[PrettyNameAnnotationKey] = clusterPrettyName
}

clusterBinding := &kubebindv1alpha2.ClusterBinding{
ObjectMeta: metav1.ObjectMeta{
Name: ClusterBindingName,
Namespace: ns,
Name: ClusterBindingName,
Namespace: ns,
Annotations: annotations,
},
Spec: kubebindv1alpha2.ClusterBindingSpec{
ProviderPrettyName: providerPrettyName,
Expand Down
1 change: 1 addition & 0 deletions backend/kubernetes/resources/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
const (
IdentityAnnotationKey = "backend.kube-bind.io/identity"
AuthorAnnotationKey = "backend.kube-bind.io/author"
PrettyNameAnnotationKey = "backend.kube-bind.io/cluster-pretty-name"
legacyIdentityAnnotationKey = "example-backend.kube-bind.io/identity"
)

Expand Down
14 changes: 8 additions & 6 deletions backend/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,15 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
}

func (options *Options) Complete() (*CompletedOptions, error) {
if !options.FrontendDisabled {
// Serve must complete first as OIDC may depend on it
// to reuse the listener.
if err := options.Serve.Complete(); err != nil {
return nil, err
}
// Serve must complete first as OIDC may depend on it
// to reuse the listener. The web server is always started
// (at minimum for the healthz endpoint), so the listener
// is required even when the frontend is disabled.
if err := options.Serve.Complete(); err != nil {
return nil, err
}

if !options.FrontendDisabled {
if err := options.OIDC.Complete(options.Serve.Listener); err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func New(
opts: opts,
ignorePrefixes: ignorePrefixes,
scheme: scheme,
vwCache: shared.NewVWClientCache(baseConfig, scheme),
vwCache: shared.NewVWClientCache(baseConfig),
}

return r, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func New(
manager: mgr,
opts: opts,
scheme: scheme,
vwCache: shared.NewVWClientCache(baseConfig, scheme),
vwCache: shared.NewVWClientCache(baseConfig),
}, nil
}

Expand Down
57 changes: 35 additions & 22 deletions backend/provider/kcp/controllers/shared/vw.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import (
"sync"

apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
apisv1alpha1client "github.com/kcp-dev/sdk/client/clientset/versioned/typed/apis/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -57,27 +58,27 @@ func ExtractClusterID(clusterConfig *rest.Config) (string, error) {
return pathParts[5], nil
}

// VWClientCache caches controller-runtime clients keyed by cluster ID to avoid
// creating a new client on every schema lookup.
// VWClientCache caches typed apis.kcp.io clients keyed by cluster ID. The
// clients target the apiresourceschema virtual workspace and skip discovery
// (which the VW does not fully serve), so we use the typed kcp REST client
// rather than controller-runtime's client.Client.
type VWClientCache struct {
mu sync.RWMutex
clients map[string]client.Client
clients map[string]apisv1alpha1client.ApisV1alpha1Interface
baseConfig *rest.Config
scheme *runtime.Scheme
}

// NewVWClientCache creates a new VWClientCache.
func NewVWClientCache(baseConfig *rest.Config, scheme *runtime.Scheme) *VWClientCache {
func NewVWClientCache(baseConfig *rest.Config) *VWClientCache {
return &VWClientCache{
clients: make(map[string]client.Client),
clients: make(map[string]apisv1alpha1client.ApisV1alpha1Interface),
baseConfig: baseConfig,
scheme: scheme,
}
}

// GetClient returns a cached client for the given cluster ID, creating one if
// necessary.
func (c *VWClientCache) GetClient(clusterID string) (client.Client, error) {
func (c *VWClientCache) GetClient(clusterID string) (apisv1alpha1client.ApisV1alpha1Interface, error) {
c.mu.RLock()
if cl, ok := c.clients[clusterID]; ok {
c.mu.RUnlock()
Expand All @@ -93,28 +94,40 @@ func (c *VWClientCache) GetClient(clusterID string) (client.Client, error) {
return cl, nil
}

cl, err := newVWClient(c.baseConfig, c.scheme, clusterID)
cl, err := newVWClient(c.baseConfig, clusterID)
if err != nil {
return nil, err
}
c.clients[clusterID] = cl
return cl, nil
}

// newVWClient creates a client pointing at the apiresourceschema virtual workspace
// for the given cluster ID:
// https://host:port/services/apiresourceschema/{clusterID}/clusters/*/
func newVWClient(baseConfig *rest.Config, scheme *runtime.Scheme, clusterID string) (client.Client, error) {
// newVWClient creates a typed apis.kcp.io client pointing at the
// apiresourceschema virtual workspace for the given cluster ID:
//
// https://host:port/services/apiresourceschema/{clusterID}/clusters/{clusterID}
//
// Two important details:
//
// 1. We replace the path on baseConfig.Host (which typically points at a
// workspace such as .../clusters/<provider>) rather than appending — otherwise
// the URL ends up with two /clusters/ segments and never matches the VW root.
//
// 2. The kcp VW resolver accepts either "*" or the literal consumer cluster
// name in the /clusters/<x>/ segment (see kcp pkg/virtual/apiresourceschema/
// builder/build.go digestURL). We use the literal name because client-go's
// REST request builder percent-encodes "*" to "%2A" when constructing the
// final URL, and the resolver compares the path segment against the literal
// string "*" — so a wildcard never matches in practice.
func newVWClient(baseConfig *rest.Config, clusterID string) (apisv1alpha1client.ApisV1alpha1Interface, error) {
cfg := rest.CopyConfig(baseConfig)
u, err := url.Parse(cfg.Host)
u, err := url.Parse(baseConfig.Host)
if err != nil {
return nil, fmt.Errorf("failed to parse base config host: %w", err)
}
cfg.Host = fmt.Sprintf("%s://%s/services/apiresourceschema/%s/clusters/%s", u.Scheme, u.Host, clusterID, clusterID)

u.Path = fmt.Sprintf("/services/apiresourceschema/%s/clusters/*", clusterID)
cfg.Host = u.String()

return client.New(cfg, client.Options{Scheme: scheme})
return apisv1alpha1client.NewForConfig(cfg)
}

// SchemaGetterWithFallback returns a function that first tries to get an
Expand Down Expand Up @@ -151,11 +164,11 @@ func SchemaGetterWithFallback(
return nil, fmt.Errorf("failed to get VW client for cluster %q: %w", clusterID, err)
}

var vwSchema apisv1alpha1.APIResourceSchema
if err := vwClient.Get(ctx, client.ObjectKey{Name: name}, &vwSchema); err != nil {
vwSchema, err := vwClient.APIResourceSchemas().Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("APIResourceSchema %q not found in workspace or VW: %w", name, err)
}

return &vwSchema, nil
return vwSchema, nil
}
}
Loading
Loading