@@ -115,8 +115,9 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
115115 "Used to scaffold webhooks for resources defined outside this project" )
116116
117117 fs .StringVar (& p .options .ExternalAPIDomain , "external-api-domain" , "" ,
118- "Domain name for the external API (e.g., cert-manager.io). " +
119- "Used to generate accurate RBAC markers and permissions for the external resources" )
118+ "Domain for the external API (e.g., cert-manager.io). Selects the recorded resource when " +
119+ "several share a group, version, and kind, and is used to generate accurate RBAC markers " +
120+ "and permissions for the external resources" )
120121
121122 fs .StringVar (& p .options .ExternalAPIModule , "external-api-module" , "" ,
122123 "External API module with optional version (e.g., github.com/cert-manager/cert-manager@v1.18.2)" )
@@ -246,78 +247,120 @@ func (p *createWebhookSubcommand) PostScaffold() error {
246247//
247248// The lookup is by Group/Version/Kind rather than the full GVK because res still carries the
248249// project domain, while an external resource keeps its own. Only external APIs can register the
249- // same Group/Version/Kind under different domains, so when several match: --external-api-domain
250- // selects the intended one; without it the non-external (core/project) entry is used; and when
251- // every match is external the request is ambiguous and refused.
250+ // same Group/Version/Kind under different domains, so when several match --external-api-domain
251+ // selects the intended one; without it resolution falls to the non-external (core/project) entry.
252252func (p * createWebhookSubcommand ) updateResourceFromConfig (res * resource.Resource ) error {
253253 resources , err := p .config .GetResources ()
254254 if err != nil {
255255 return fmt .Errorf ("failed to load resources from project configuration: %w" , err )
256256 }
257257
258258 // Collect every recorded resource sharing this Group/Version/Kind.
259- var matches []resource.Resource
259+ var candidates []resource.Resource
260260 for _ , r := range resources {
261261 if r .Group == res .Group && r .Version == res .Version && r .Kind == res .Kind {
262- matches = append (matches , r )
262+ candidates = append (candidates , r )
263263 }
264264 }
265265
266- var existingRes resource.Resource
267- switch len (matches ) {
266+ domain := p .options .ExternalAPIDomain
267+ var selected * resource.Resource
268+ switch len (candidates ) {
268269 case 0 :
269270 return nil // nothing recorded for this GVK; keep res as built from the flags
270271 case 1 :
271- existingRes = matches [0 ]
272+ if domain == "" {
273+ selected = & candidates [0 ] // recover the single record
274+ } else {
275+ selected , err = selectByDomain (candidates , domain , p .options .ExternalAPIPath )
276+ }
272277 default :
273- // Several entries share this GVK — only external APIs can, under different domains.
274- domain := p .options .ExternalAPIDomain
275- found := false
276- if domain != "" {
277- // A domain was named: use the entry that carries it.
278- for _ , m := range matches {
279- if m .Domain == domain {
280- existingRes , found = m , true
281- break
282- }
283- }
278+ if domain == "" {
279+ selected , err = selectNonExternal (candidates , res .Domain )
284280 } else {
285- // No domain named: use the non-external (core/project) entry, if any.
286- for _ , m := range matches {
287- if ! m .External {
288- existingRes , found = m , true
289- break
290- }
291- }
281+ selected , err = selectByDomain (candidates , domain , p .options .ExternalAPIPath )
292282 }
293- if ! found {
294- domains := make ([]string , len (matches ))
295- for i , m := range matches {
296- domains [i ] = m .Domain
297- }
298- if domain != "" {
299- return fmt .Errorf (
300- "no resource matches --external-api-domain %q for group %q, version %q and kind %q " +
301- "(recorded domains: %s)" ,
302- domain , res .Group , res .Version , res .Kind , strings .Join (domains , ", " ),
303- )
283+ }
284+ if err != nil {
285+ return err
286+ }
287+ if selected == nil {
288+ return nil // the flags describe a new resource
289+ }
290+
291+ res .Domain = selected .Domain
292+ res .Path = selected .Path
293+ res .Plural = selected .Plural
294+ res .External = selected .External
295+ res .Core = selected .Core
296+ res .Module = selected .Module
297+
298+ return nil
299+ }
300+
301+ // selectByDomain resolves the candidate carrying the given --external-api-domain, for any number
302+ // of candidates. When none carries it, a non-empty external path means the flags describe a new
303+ // resource (nil, nil); otherwise the request is refused.
304+ func selectByDomain (candidates []resource.Resource , domain , externalPath string ) (* resource.Resource , error ) {
305+ for i := range candidates {
306+ if candidates [i ].Domain == domain {
307+ return & candidates [i ], nil
308+ }
309+ }
310+ if externalPath != "" {
311+ return nil , nil
312+ }
313+ return nil , resolutionError (candidates , domain )
314+ }
315+
316+ // selectNonExternal resolves several same-GVK candidates when no domain is given: the target is
317+ // the non-external (core/project) entry. When a core and project entry collide it keeps the
318+ // project (its domain equals projectDomain); when every candidate is external it refuses.
319+ func selectNonExternal (candidates []resource.Resource , projectDomain string ) (* resource.Resource , error ) {
320+ var nonExternal []resource.Resource
321+ for _ , c := range candidates {
322+ if ! c .External {
323+ nonExternal = append (nonExternal , c )
324+ }
325+ }
326+
327+ switch len (nonExternal ) {
328+ case 0 :
329+ return nil , resolutionError (candidates , "" )
330+ case 1 :
331+ return & nonExternal [0 ], nil
332+ default :
333+ // A core and a project resource share the GVK; keep the project one.
334+ for i := range nonExternal {
335+ if nonExternal [i ].Domain == projectDomain {
336+ return & nonExternal [i ], nil
304337 }
305- return fmt .Errorf (
306- "group %q, version %q and kind %q match more than one resource (domains: %s): " +
307- "pass --external-api-domain to choose the one to work on" ,
308- res .Group , res .Version , res .Kind , strings .Join (domains , ", " ),
309- )
310338 }
339+ return nil , resolutionError (candidates , "" )
311340 }
341+ }
312342
313- res .Domain = existingRes .Domain
314- res .Path = existingRes .Path
315- res .Plural = existingRes .Plural
316- res .External = existingRes .External
317- res .Core = existingRes .Core
318- res .Module = existingRes .Module
343+ // resolutionError reports why the candidates could not be resolved to one, naming their domains.
344+ // With a named domain it reports that none matched; without one it asks for --external-api-domain.
345+ func resolutionError (candidates []resource.Resource , domain string ) error {
346+ g , v , k := candidates [0 ].Group , candidates [0 ].Version , candidates [0 ].Kind
347+ domains := make ([]string , len (candidates ))
348+ for i , c := range candidates {
349+ domains [i ] = c .Domain
350+ }
319351
320- return nil
352+ if domain != "" {
353+ return fmt .Errorf (
354+ "no resource matches --external-api-domain %q for group %q, version %q and kind %q " +
355+ "(recorded domains: %s)" ,
356+ domain , g , v , k , strings .Join (domains , ", " ),
357+ )
358+ }
359+ return fmt .Errorf (
360+ "group %q, version %q and kind %q match more than one resource (domains: %s): " +
361+ "pass --external-api-domain to choose the one to work on" ,
362+ g , v , k , strings .Join (domains , ", " ),
363+ )
321364}
322365
323366// Helper function to validate spoke versions
0 commit comments