@@ -25,8 +25,9 @@ import (
2525 "sync"
2626
2727 apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
28+ apisv1alpha1client "github.com/kcp-dev/sdk/client/clientset/versioned/typed/apis/v1alpha1"
2829 "k8s.io/apimachinery/pkg/api/errors"
29- "k8s.io/apimachinery/pkg/runtime "
30+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1 "
3031 "k8s.io/client-go/rest"
3132 "sigs.k8s.io/controller-runtime/pkg/client"
3233 "sigs.k8s.io/controller-runtime/pkg/log"
@@ -57,27 +58,27 @@ func ExtractClusterID(clusterConfig *rest.Config) (string, error) {
5758 return pathParts [5 ], nil
5859}
5960
60- // VWClientCache caches controller-runtime clients keyed by cluster ID to avoid
61- // creating a new client on every schema lookup.
61+ // VWClientCache caches typed apis.kcp.io clients keyed by cluster ID. The
62+ // clients target the apiresourceschema virtual workspace and skip discovery
63+ // (which the VW does not fully serve), so we use the typed kcp REST client
64+ // rather than controller-runtime's client.Client.
6265type VWClientCache struct {
6366 mu sync.RWMutex
64- clients map [string ]client. Client
67+ clients map [string ]apisv1alpha1client. ApisV1alpha1Interface
6568 baseConfig * rest.Config
66- scheme * runtime.Scheme
6769}
6870
6971// NewVWClientCache creates a new VWClientCache.
70- func NewVWClientCache (baseConfig * rest.Config , scheme * runtime. Scheme ) * VWClientCache {
72+ func NewVWClientCache (baseConfig * rest.Config ) * VWClientCache {
7173 return & VWClientCache {
72- clients : make (map [string ]client. Client ),
74+ clients : make (map [string ]apisv1alpha1client. ApisV1alpha1Interface ),
7375 baseConfig : baseConfig ,
74- scheme : scheme ,
7576 }
7677}
7778
7879// GetClient returns a cached client for the given cluster ID, creating one if
7980// necessary.
80- func (c * VWClientCache ) GetClient (clusterID string ) (client. Client , error ) {
81+ func (c * VWClientCache ) GetClient (clusterID string ) (apisv1alpha1client. ApisV1alpha1Interface , error ) {
8182 c .mu .RLock ()
8283 if cl , ok := c .clients [clusterID ]; ok {
8384 c .mu .RUnlock ()
@@ -93,28 +94,40 @@ func (c *VWClientCache) GetClient(clusterID string) (client.Client, error) {
9394 return cl , nil
9495 }
9596
96- cl , err := newVWClient (c .baseConfig , c . scheme , clusterID )
97+ cl , err := newVWClient (c .baseConfig , clusterID )
9798 if err != nil {
9899 return nil , err
99100 }
100101 c .clients [clusterID ] = cl
101102 return cl , nil
102103}
103104
104- // newVWClient creates a client pointing at the apiresourceschema virtual workspace
105- // for the given cluster ID:
106- // https://host:port/services/apiresourceschema/{clusterID}/clusters/*/
107- func newVWClient (baseConfig * rest.Config , scheme * runtime.Scheme , clusterID string ) (client.Client , error ) {
105+ // newVWClient creates a typed apis.kcp.io client pointing at the
106+ // apiresourceschema virtual workspace for the given cluster ID:
107+ //
108+ // https://host:port/services/apiresourceschema/{clusterID}/clusters/{clusterID}
109+ //
110+ // Two important details:
111+ //
112+ // 1. We replace the path on baseConfig.Host (which typically points at a
113+ // workspace such as .../clusters/<provider>) rather than appending — otherwise
114+ // the URL ends up with two /clusters/ segments and never matches the VW root.
115+ //
116+ // 2. The kcp VW resolver accepts either "*" or the literal consumer cluster
117+ // name in the /clusters/<x>/ segment (see kcp pkg/virtual/apiresourceschema/
118+ // builder/build.go digestURL). We use the literal name because client-go's
119+ // REST request builder percent-encodes "*" to "%2A" when constructing the
120+ // final URL, and the resolver compares the path segment against the literal
121+ // string "*" — so a wildcard never matches in practice.
122+ func newVWClient (baseConfig * rest.Config , clusterID string ) (apisv1alpha1client.ApisV1alpha1Interface , error ) {
108123 cfg := rest .CopyConfig (baseConfig )
109- u , err := url .Parse (cfg .Host )
124+ u , err := url .Parse (baseConfig .Host )
110125 if err != nil {
111126 return nil , fmt .Errorf ("failed to parse base config host: %w" , err )
112127 }
128+ cfg .Host = fmt .Sprintf ("%s://%s/services/apiresourceschema/%s/clusters/%s" , u .Scheme , u .Host , clusterID , clusterID )
113129
114- u .Path = fmt .Sprintf ("/services/apiresourceschema/%s/clusters/*" , clusterID )
115- cfg .Host = u .String ()
116-
117- return client .New (cfg , client.Options {Scheme : scheme })
130+ return apisv1alpha1client .NewForConfig (cfg )
118131}
119132
120133// SchemaGetterWithFallback returns a function that first tries to get an
@@ -151,11 +164,11 @@ func SchemaGetterWithFallback(
151164 return nil , fmt .Errorf ("failed to get VW client for cluster %q: %w" , clusterID , err )
152165 }
153166
154- var vwSchema apisv1alpha1. APIResourceSchema
155- if err := vwClient . Get ( ctx , client. ObjectKey { Name : name }, & vwSchema ); err != nil {
167+ vwSchema , err := vwClient . APIResourceSchemas (). Get ( ctx , name , metav1. GetOptions {})
168+ if err != nil {
156169 return nil , fmt .Errorf ("APIResourceSchema %q not found in workspace or VW: %w" , name , err )
157170 }
158171
159- return & vwSchema , nil
172+ return vwSchema , nil
160173 }
161174}
0 commit comments