@@ -223,6 +223,160 @@ var _ = Describe("createWebhookSubcommand", func() {
223223 Expect (err .Error ()).To (ContainSubstring ("or pass --external-api-path for an external type" ))
224224 })
225225
226+ Context ("when several external resources share the same Group/Version/Kind" , func () {
227+ const (
228+ pathIO = "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
229+ pathK8sIO = "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
230+ domainIO = "cert-manager.io"
231+ domainK8s = "cert-manager.k8s.io"
232+ )
233+
234+ // storeExternal records an external resource sharing crewGroup/v1/captainKind but under
235+ // the given domain. Recorded resources are unique by full GVK, so distinct domains produce
236+ // distinct entries that collide only on Group/Version/Kind.
237+ storeExternal := func (domain , path string ) {
238+ Expect (cfg .AddResource (resource.Resource {
239+ GVK : resource.GVK {
240+ Group : crewGroup ,
241+ Domain : domain ,
242+ Version : "v1" ,
243+ Kind : captainKind ,
244+ },
245+ External : true ,
246+ Path : path ,
247+ Webhooks : & resource.Webhooks {},
248+ })).To (Succeed ())
249+ }
250+
251+ // storedByDomain re-reads the recorded resources keyed by domain, to assert entries are
252+ // left untouched.
253+ storedByDomain := func () map [string ]resource.Resource {
254+ stored , err := cfg .GetResources ()
255+ Expect (err ).NotTo (HaveOccurred ())
256+ byDomain := make (map [string ]resource.Resource , len (stored ))
257+ for _ , s := range stored {
258+ byDomain [s .Domain ] = s
259+ }
260+ return byDomain
261+ }
262+
263+ It ("should refuse and leave the PROJECT untouched when no domain is given" , func () {
264+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
265+ storeExternal (domainIO , pathIO )
266+ storeExternal (domainK8s , pathK8sIO )
267+ subCmd .options .DoDefaulting = true
268+
269+ err := subCmd .InjectResource (res )
270+
271+ Expect (err ).To (HaveOccurred ())
272+ Expect (err .Error ()).To (ContainSubstring ("match more than one resource" ))
273+ Expect (err .Error ()).To (ContainSubstring ("pass --external-api-domain" ))
274+ Expect (err .Error ()).To (ContainSubstring (domainIO ))
275+ Expect (err .Error ()).To (ContainSubstring (domainK8s ))
276+
277+ // Both entries keep their path and domain.
278+ byDomain := storedByDomain ()
279+ Expect (byDomain ).To (HaveLen (2 ))
280+ Expect (byDomain [domainIO ].Path ).To (Equal (pathIO ))
281+ Expect (byDomain [domainK8s ].Path ).To (Equal (pathK8sIO ))
282+ })
283+
284+ It ("should refuse regardless of the order the resources are recorded" , func () {
285+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
286+ // Reverse order from the previous test: nothing may depend on file order.
287+ storeExternal (domainK8s , pathK8sIO )
288+ storeExternal (domainIO , pathIO )
289+ subCmd .options .DoDefaulting = true
290+
291+ err := subCmd .InjectResource (res )
292+
293+ Expect (err ).To (HaveOccurred ())
294+ Expect (err .Error ()).To (ContainSubstring ("match more than one resource" ))
295+ })
296+
297+ It ("should work on the resource named by --external-api-domain and leave the other alone" , func () {
298+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
299+ storeExternal (domainIO , pathIO )
300+ storeExternal (domainK8s , pathK8sIO )
301+ subCmd .options .DoDefaulting = true
302+ subCmd .options .ExternalAPIDomain = domainK8s
303+
304+ err := subCmd .InjectResource (res )
305+
306+ Expect (err ).NotTo (HaveOccurred ())
307+ Expect (res .External ).To (BeTrue ())
308+ Expect (res .Domain ).To (Equal (domainK8s ))
309+ Expect (res .Path ).To (Equal (pathK8sIO ))
310+
311+ // The unnamed entry is untouched.
312+ Expect (storedByDomain ()[domainIO ].Path ).To (Equal (pathIO ))
313+ })
314+
315+ It ("should refuse when the given domain matches none of them" , func () {
316+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
317+ storeExternal (domainIO , pathIO )
318+ storeExternal (domainK8s , pathK8sIO )
319+ subCmd .options .DoDefaulting = true
320+ subCmd .options .ExternalAPIDomain = "does-not-exist.io"
321+
322+ err := subCmd .InjectResource (res )
323+
324+ Expect (err ).To (HaveOccurred ())
325+ Expect (err .Error ()).To (ContainSubstring ("no resource matches --external-api-domain" ))
326+ Expect (err .Error ()).To (ContainSubstring ("does-not-exist.io" ))
327+ Expect (err .Error ()).To (ContainSubstring (domainIO ))
328+ Expect (err .Error ()).To (ContainSubstring (domainK8s ))
329+ })
330+
331+ It ("should refuse without a domain even when one entry has an empty domain" , func () {
332+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
333+ // An empty flag must not silently select the empty-domain entry.
334+ storeExternal ("" , pathK8sIO )
335+ storeExternal (domainIO , pathIO )
336+ subCmd .options .DoDefaulting = true
337+
338+ err := subCmd .InjectResource (res )
339+
340+ Expect (err ).To (HaveOccurred ())
341+ Expect (err .Error ()).To (ContainSubstring ("match more than one resource" ))
342+ })
343+
344+ It ("should prefer the non-external entry when no domain is given" , func () {
345+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
346+ // A project (non-external) entry shares the GVK with an external one.
347+ Expect (cfg .AddResource (resource.Resource {
348+ GVK : resource.GVK {Group : crewGroup , Domain : testIO , Version : "v1" , Kind : captainKind },
349+ Path : "github.com/example/test/api/v1" ,
350+ Webhooks : & resource.Webhooks {},
351+ })).To (Succeed ())
352+ storeExternal (domainK8s , pathK8sIO )
353+ subCmd .options .DoDefaulting = true
354+
355+ err := subCmd .InjectResource (res )
356+
357+ // Resolves to the non-external entry, not the external one, without a domain.
358+ Expect (err ).NotTo (HaveOccurred ())
359+ Expect (res .External ).To (BeFalse ())
360+ })
361+ })
362+
363+ It ("should resolve a core type without --external-api-domain" , func () {
364+ Expect (subCmd .InjectConfig (cfg )).To (Succeed ())
365+ // A single recorded core type must resolve without --external-api-domain.
366+ Expect (cfg .AddResource (resource.Resource {
367+ GVK : res .GVK ,
368+ Core : true ,
369+ Path : "k8s.io/api/core/v1" ,
370+ Webhooks : & resource.Webhooks {},
371+ })).To (Succeed ())
372+ subCmd .options .DoDefaulting = true
373+
374+ err := subCmd .InjectResource (res )
375+
376+ Expect (err ).NotTo (HaveOccurred ())
377+ Expect (res .Core ).To (BeTrue ())
378+ })
379+
226380 Context ("isValidVersion" , func () {
227381 BeforeEach (func () {
228382 res = & resource.Resource {
0 commit comments