@@ -5,6 +5,7 @@ package server
55
66import (
77 "bytes"
8+ "context"
89 "encoding/json"
910 "fmt"
1011 "io"
@@ -13,16 +14,21 @@ import (
1314 "net/http/httputil"
1415 "net/url"
1516 "strings"
17+ "sync"
1618
1719 "github.com/gimlet-io/capacitor/pkg/kubernetes"
1820 "github.com/labstack/echo/v4"
21+ "k8s.io/client-go/discovery"
1922 "k8s.io/client-go/rest"
2023)
2124
2225// KubernetesProxy handles proxying requests to the Kubernetes API
2326type KubernetesProxy struct {
24- k8sClient * kubernetes.Client
25- proxy * httputil.ReverseProxy
27+ k8sClient * kubernetes.Client
28+ proxy * httputil.ReverseProxy
29+ fluxAPIPaths map [string ]string // Cache for discovered Flux API paths (kind -> API path template)
30+ fluxAPIPathsMu sync.RWMutex // Mutex for thread-safe access to fluxAPIPaths
31+ fluxAPIDiscovery sync.Once // Ensures discovery happens only once
2632}
2733
2834// NewKubernetesProxy creates a new KubernetesProxy
@@ -105,8 +111,9 @@ func NewKubernetesProxy(k8sClient *kubernetes.Client) (*KubernetesProxy, error)
105111 }
106112
107113 return & KubernetesProxy {
108- k8sClient : k8sClient ,
109- proxy : proxy ,
114+ k8sClient : k8sClient ,
115+ proxy : proxy ,
116+ fluxAPIPaths : make (map [string ]string ),
110117 }, nil
111118}
112119
@@ -172,3 +179,138 @@ func removeManagedFieldsFromAny(v *interface{}) {
172179 // primitives: nothing to do
173180 }
174181}
182+
183+ // discoverFluxAPIPaths discovers Flux API paths using Kubernetes discovery client
184+ // Returns a map of resource kind to API path template (e.g., "Kustomization" -> "/apis/kustomize.toolkit.fluxcd.io/v1/namespaces/%s/kustomizations/%s")
185+ // Uses sync.Once to ensure discovery happens only once per proxy instance
186+ func (p * KubernetesProxy ) discoverFluxAPIPaths () (map [string ]string , error ) {
187+ var discoveryErr error
188+
189+ // Use sync.Once to ensure discovery happens only once
190+ p .fluxAPIDiscovery .Do (func () {
191+ // Create discovery client
192+ discoveryClient , err := discovery .NewDiscoveryClientForConfig (p .k8sClient .Config )
193+ if err != nil {
194+ discoveryErr = fmt .Errorf ("failed to create discovery client: %w" , err )
195+ return
196+ }
197+
198+ // Get API groups
199+ apiGroups , err := discoveryClient .ServerGroups ()
200+ if err != nil {
201+ discoveryErr = fmt .Errorf ("failed to get API groups: %w" , err )
202+ return
203+ }
204+
205+ // Map of Flux API group prefixes to their resource kinds
206+ fluxGroupKinds := map [string ][]string {
207+ "kustomize.toolkit.fluxcd.io" : {"Kustomization" },
208+ "helm.toolkit.fluxcd.io" : {"HelmRelease" },
209+ "source.toolkit.fluxcd.io" : {"GitRepository" , "HelmRepository" , "HelmChart" , "OCIRepository" , "Bucket" },
210+ "notification.toolkit.fluxcd.io" : {"Alert" , "Provider" , "Receiver" },
211+ "image.toolkit.fluxcd.io" : {"ImagePolicy" , "ImageRepository" , "ImageUpdate" },
212+ "infra.contrib.fluxcd.io" : {"Terraform" },
213+ }
214+
215+ // Map of resource kind to plural form
216+ kindToPlural := map [string ]string {
217+ "Kustomization" : "kustomizations" ,
218+ "HelmRelease" : "helmreleases" ,
219+ "GitRepository" : "gitrepositories" ,
220+ "HelmRepository" : "helmrepositories" ,
221+ "HelmChart" : "helmcharts" ,
222+ "OCIRepository" : "ocirepositories" ,
223+ "Bucket" : "buckets" ,
224+ "Alert" : "alerts" ,
225+ "Provider" : "providers" ,
226+ "Receiver" : "receivers" ,
227+ "ImagePolicy" : "imagepolicies" ,
228+ "ImageRepository" : "imagerepositories" ,
229+ "ImageUpdate" : "imageupdateautomations" ,
230+ "Terraform" : "terraforms" ,
231+ }
232+
233+ discoveredPaths := make (map [string ]string )
234+
235+ // Iterate through API groups to find Flux groups
236+ for _ , group := range apiGroups .Groups {
237+ for groupPrefix , kinds := range fluxGroupKinds {
238+ if strings .Contains (group .Name , groupPrefix ) {
239+ // Find the preferred version
240+ preferredVersion := group .PreferredVersion .Version
241+ if preferredVersion == "" && len (group .Versions ) > 0 {
242+ preferredVersion = group .Versions [0 ].Version
243+ }
244+
245+ // Get resources for this group version
246+ groupVersion := fmt .Sprintf ("%s/%s" , group .Name , preferredVersion )
247+ resources , err := discoveryClient .ServerResourcesForGroupVersion (groupVersion )
248+ if err != nil {
249+ log .Printf ("Warning: failed to get resources for %s: %v" , groupVersion , err )
250+ continue
251+ }
252+
253+ // Map each kind to its API path
254+ for _ , kind := range kinds {
255+ plural , ok := kindToPlural [kind ]
256+ if ! ok {
257+ log .Printf ("Warning: no plural form found for kind %s" , kind )
258+ continue
259+ }
260+
261+ // Check if this resource exists in the discovered resources
262+ found := false
263+ for _ , resource := range resources .APIResources {
264+ if resource .Name == plural {
265+ found = true
266+ break
267+ }
268+ }
269+
270+ if found {
271+ // Build API path template: /apis/{group}/{version}/namespaces/{namespace}/{plural}/{name}
272+ apiPath := fmt .Sprintf ("/apis/%s/%s/namespaces/%%s/%s/%%s" , group .Name , preferredVersion , plural )
273+ discoveredPaths [kind ] = apiPath
274+ }
275+ }
276+ }
277+ }
278+ }
279+
280+ // Cache the results
281+ p .fluxAPIPathsMu .Lock ()
282+ p .fluxAPIPaths = discoveredPaths
283+ p .fluxAPIPathsMu .Unlock ()
284+ })
285+
286+ // Return error if discovery failed
287+ if discoveryErr != nil {
288+ return nil , discoveryErr
289+ }
290+
291+ // Return cached results (thread-safe read)
292+ p .fluxAPIPathsMu .RLock ()
293+ defer p .fluxAPIPathsMu .RUnlock ()
294+
295+ // Return a copy to prevent external modification
296+ result := make (map [string ]string , len (p .fluxAPIPaths ))
297+ for k , v := range p .fluxAPIPaths {
298+ result [k ] = v
299+ }
300+ return result , nil
301+ }
302+
303+ // getFluxAPIPath returns the API path for a Flux resource kind, discovering it if necessary
304+ func (p * KubernetesProxy ) getFluxAPIPath (ctx context.Context , kind string ) (string , error ) {
305+ paths , err := p .discoverFluxAPIPaths ()
306+ if err != nil {
307+ return "" , err
308+ }
309+
310+ apiPath , found := paths [kind ]
311+ if ! found {
312+ return "" , fmt .Errorf ("flux resource kind %s not found in discovered API paths" , kind )
313+ }
314+
315+ return apiPath , nil
316+ }
0 commit comments