Skip to content

Commit 20bf151

Browse files
committed
fix inability to reference same-named Kinds
There were a couple problems that were the root cause of this issue. First, I was only handling the difference between namespaced and non-namespaced CRDs in the GET action. For CREATE, DELETE, and APPLY, I was not properly differentiating between cluster-scoped and namespace-scoped CRDs. After fixing that, there was a problem with how the `gvrFromGVK` method on the connection struct was discovering an APIResource from the GroupVersionKind. I was only passing in the Kind instead of the GroupVersion information as well, which resulted in the discovery cache in the kube client failing to match the correct CRD when there were two different CRDs with the same Kind but different GroupVersions. Closes Issue #21 Signed-off-by: Jay Pipes <[email protected]>
1 parent ecc124b commit 20bf151

8 files changed

+295
-114
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Go Reference](https://pkg.go.dev/badge/github.com/gdt-dev/kube.svg)](https://pkg.go.dev/github.com/gdt-dev/kube)
44
[![Go Report Card](https://goreportcard.com/badge/github.com/gdt-dev/kube)](https://goreportcard.com/report/github.com/gdt-dev/kube)
5-
[![Build Status](https://github.com/gdt-dev/kube/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/gdt-dev/kube/actions)
5+
[![Build Status](https://github.com/gdt-dev/kube/actions/workflows/gate-tests.yml/badge.svg?branch=main)](https://github.com/gdt-dev/kube/actions)
66
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
77

88
<div style="float: left">

action.go

+86-45
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,11 @@ func (a *Action) get(
121121
ns string,
122122
out *interface{},
123123
) error {
124-
kind, name := a.Get.KindName()
125-
gvk := schema.GroupVersionKind{
126-
Kind: kind,
127-
}
128-
res, err := c.gvrFromGVK(gvk)
124+
res, err := c.gvrFromArg(a.Get.Arg)
129125
if err != nil {
130126
return err
131127
}
128+
name := a.Get.Name
132129
if name == "" {
133130
list, err := a.doList(ctx, c, res, ns)
134131
if err == nil {
@@ -154,7 +151,7 @@ func (a *Action) doList(
154151
resName := res.Resource
155152
labelSelString := ""
156153
opts := metav1.ListOptions{}
157-
withlabels := a.Get.Labels()
154+
withlabels := a.Get.Labels
158155
if withlabels != nil {
159156
// We already validated the label selector during parse-time
160157
labelsStr := labels.Set(withlabels).String()
@@ -250,21 +247,30 @@ func (a *Action) create(
250247
}
251248
for _, obj := range objs {
252249
gvk := obj.GetObjectKind().GroupVersionKind()
253-
ons := obj.GetNamespace()
254-
if ons == "" {
255-
ons = ns
256-
}
257250
res, err := c.gvrFromGVK(gvk)
258251
if err != nil {
259252
return err
260253
}
261254
resName := res.Resource
262-
debug.Println(ctx, "kube.create: %s (ns: %s)", resName, ons)
263-
obj, err := c.client.Resource(res).Namespace(ons).Create(
264-
ctx,
265-
obj,
266-
metav1.CreateOptions{},
267-
)
255+
if c.resourceNamespaced(res) {
256+
ons := obj.GetNamespace()
257+
if ons == "" {
258+
ons = ns
259+
}
260+
debug.Println(ctx, "kube.create: %s (ns: %s)", resName, ons)
261+
obj, err = c.client.Resource(res).Namespace(ons).Create(
262+
ctx,
263+
obj,
264+
metav1.CreateOptions{},
265+
)
266+
} else {
267+
debug.Println(ctx, "kube.create: %s (non-namespaced resource)", resName)
268+
obj, err = c.client.Resource(res).Create(
269+
ctx,
270+
obj,
271+
metav1.CreateOptions{},
272+
)
273+
}
268274
if err != nil {
269275
return err
270276
}
@@ -314,28 +320,44 @@ func (a *Action) apply(
314320
}
315321
for _, obj := range objs {
316322
gvk := obj.GetObjectKind().GroupVersionKind()
317-
ons := obj.GetNamespace()
318-
if ons == "" {
319-
ons = ns
320-
}
321323
res, err := c.gvrFromGVK(gvk)
322324
if err != nil {
323325
return err
324326
}
325327
resName := res.Resource
326-
debug.Println(ctx, "kube.apply: %s (ns: %s)", resName, ons)
327-
obj, err := c.client.Resource(res).Namespace(ns).Apply(
328-
ctx,
329-
// NOTE(jaypipes): Not sure why a separate name argument is
330-
// necessary considering `obj` is of type
331-
// `*unstructured.Unstructured` and therefore has the `GetName()`
332-
// method...
333-
obj.GetName(),
334-
obj,
335-
// TODO(jaypipes): Not sure if this hard-coded options struct is
336-
// always going to work. Maybe add ability to control it?
337-
metav1.ApplyOptions{FieldManager: fieldManagerName, Force: true},
338-
)
328+
if c.resourceNamespaced(res) {
329+
ons := obj.GetNamespace()
330+
if ons == "" {
331+
ons = ns
332+
}
333+
debug.Println(ctx, "kube.apply: %s (ns: %s)", resName, ons)
334+
obj, err = c.client.Resource(res).Namespace(ns).Apply(
335+
ctx,
336+
// NOTE(jaypipes): Not sure why a separate name argument is
337+
// necessary considering `obj` is of type
338+
// `*unstructured.Unstructured` and therefore has the `GetName()`
339+
// method...
340+
obj.GetName(),
341+
obj,
342+
// TODO(jaypipes): Not sure if this hard-coded options struct is
343+
// always going to work. Maybe add ability to control it?
344+
metav1.ApplyOptions{FieldManager: fieldManagerName, Force: true},
345+
)
346+
} else {
347+
debug.Println(ctx, "kube.apply: %s (non-namespaced resource)", resName)
348+
obj, err = c.client.Resource(res).Apply(
349+
ctx,
350+
// NOTE(jaypipes): Not sure why a separate name argument is
351+
// necessary considering `obj` is of type
352+
// `*unstructured.Unstructured` and therefore has the `GetName()`
353+
// method...
354+
obj.GetName(),
355+
obj,
356+
// TODO(jaypipes): Not sure if this hard-coded options struct is
357+
// always going to work. Maybe add ability to control it?
358+
metav1.ApplyOptions{FieldManager: fieldManagerName, Force: true},
359+
)
360+
}
339361
if err != nil {
340362
return err
341363
}
@@ -385,14 +407,11 @@ func (a *Action) delete(
385407
return nil
386408
}
387409

388-
kind, name := a.Delete.KindName()
389-
gvk := schema.GroupVersionKind{
390-
Kind: kind,
391-
}
392-
res, err := c.gvrFromGVK(gvk)
410+
res, err := c.gvrFromArg(a.Delete.Arg)
393411
if err != nil {
394412
return err
395413
}
414+
name := a.Delete.Name
396415
if name == "" {
397416
return a.doDeleteCollection(ctx, c, res, ns)
398417
}
@@ -408,11 +427,22 @@ func (a *Action) doDelete(
408427
name string,
409428
) error {
410429
resName := res.Resource
430+
if c.resourceNamespaced(res) {
431+
debug.Println(
432+
ctx, "kube.delete: %s/%s (ns: %s)",
433+
resName, name, ns,
434+
)
435+
return c.client.Resource(res).Namespace(ns).Delete(
436+
ctx,
437+
name,
438+
metav1.DeleteOptions{},
439+
)
440+
}
411441
debug.Println(
412-
ctx, "kube.delete: %s/%s (ns: %s)",
413-
resName, name, ns,
442+
ctx, "kube.delete: %s/%s (non-namespaced resource)",
443+
resName, name,
414444
)
415-
return c.client.Resource(res).Namespace(ns).Delete(
445+
return c.client.Resource(res).Delete(
416446
ctx,
417447
name,
418448
metav1.DeleteOptions{},
@@ -428,7 +458,7 @@ func (a *Action) doDeleteCollection(
428458
ns string,
429459
) error {
430460
opts := metav1.ListOptions{}
431-
withlabels := a.Delete.Labels()
461+
withlabels := a.Delete.Labels
432462
labelSelString := ""
433463
if withlabels != nil {
434464
// We already validated the label selector during parse-time
@@ -437,11 +467,22 @@ func (a *Action) doDeleteCollection(
437467
opts.LabelSelector = labelsStr
438468
}
439469
resName := res.Resource
470+
if c.resourceNamespaced(res) {
471+
debug.Println(
472+
ctx, "kube.delete: %s%s (ns: %s)",
473+
resName, labelSelString, ns,
474+
)
475+
return c.client.Resource(res).Namespace(ns).DeleteCollection(
476+
ctx,
477+
metav1.DeleteOptions{},
478+
opts,
479+
)
480+
}
440481
debug.Println(
441-
ctx, "kube.delete: %s%s (ns: %s)",
442-
resName, labelSelString, ns,
482+
ctx, "kube.delete: %s%s (non-namespaced resource)",
483+
resName, labelSelString,
443484
)
444-
return c.client.Resource(res).Namespace(ns).DeleteCollection(
485+
return c.client.Resource(res).DeleteCollection(
445486
ctx,
446487
metav1.DeleteOptions{},
447488
opts,

connect.go

+43-9
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,27 @@ type connection struct {
9797
client dynamic.Interface
9898
}
9999

100-
// mappingFor returns a RESTMapper for a given resource type or kind
101-
func (c *connection) mappingFor(typeOrKind string) (*meta.RESTMapping, error) {
102-
fullySpecifiedGVR, groupResource := schema.ParseResourceArg(typeOrKind)
100+
// mappingForGVK returns a RESTMapper for a given GroupVersionKind
101+
func (c *connection) mappingForGVK(gvk schema.GroupVersionKind) (*meta.RESTMapping, error) {
102+
mapping, err := c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
103+
if err != nil {
104+
// if we error out here, it is because we could not match a resource or a kind
105+
// for the given argument. To maintain consistency with previous behavior,
106+
// announce that a resource type could not be found.
107+
// if the error is _not_ a *meta.NoKindMatchError, then we had trouble doing discovery,
108+
// so we should return the original error since it may help a user diagnose what is actually wrong
109+
if meta.IsNoMatchError(err) {
110+
return nil, fmt.Errorf("the server doesn't have a resource type %q", gvk)
111+
}
112+
return nil, err
113+
}
114+
115+
return mapping, nil
116+
}
117+
118+
// mappingForArg returns a RESTMapper for a given GroupVersionKind
119+
func (c *connection) mappingForArg(arg string) (*meta.RESTMapping, error) {
120+
fullySpecifiedGVR, groupResource := schema.ParseResourceArg(arg)
103121
gvk := schema.GroupVersionKind{}
104122

105123
if fullySpecifiedGVR != nil {
@@ -112,7 +130,7 @@ func (c *connection) mappingFor(typeOrKind string) (*meta.RESTMapping, error) {
112130
return c.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
113131
}
114132

115-
fullySpecifiedGVK, groupKind := schema.ParseKindArg(typeOrKind)
133+
fullySpecifiedGVK, groupKind := schema.ParseKindArg(arg)
116134
if fullySpecifiedGVK == nil {
117135
gvk := groupKind.WithVersion("")
118136
fullySpecifiedGVK = &gvk
@@ -140,18 +158,34 @@ func (c *connection) mappingFor(typeOrKind string) (*meta.RESTMapping, error) {
140158
return mapping, nil
141159
}
142160

161+
// gvrFromArg returns a GroupVersionResource from a resource or kind arg
162+
// string, using the discovery client to look up the resource name (the plural
163+
// of the kind). The returned GroupVersionResource will have the proper Group
164+
// and Version filled in (as opposed to an APIResource which has empty Group
165+
// and Version strings because it "inherits" its APIResourceList's GroupVersion
166+
// ... ugh.)
167+
func (c *connection) gvrFromArg(
168+
arg string,
169+
) (schema.GroupVersionResource, error) {
170+
empty := schema.GroupVersionResource{}
171+
r, err := c.mappingForArg(arg)
172+
if err != nil {
173+
return empty, ResourceUnknown(arg)
174+
}
175+
176+
return r.Resource, nil
177+
}
178+
143179
// gvrFromGVK returns a GroupVersionResource from a GroupVersionKind, using the
144180
// discovery client to look up the resource name (the plural of the kind). The
145181
// returned GroupVersionResource will have the proper Group and Version filled
146182
// in (as opposed to an APIResource which has empty Group and Version strings
147183
// because it "inherits" its APIResourceList's GroupVersion ... ugh.)
148-
func (c *connection) gvrFromGVK(
149-
gvk schema.GroupVersionKind,
150-
) (schema.GroupVersionResource, error) {
184+
func (c *connection) gvrFromGVK(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
151185
empty := schema.GroupVersionResource{}
152-
r, err := c.mappingFor(gvk.Kind)
186+
r, err := c.mappingForGVK(gvk)
153187
if err != nil {
154-
return empty, ResourceUnknown(gvk)
188+
return empty, ResourceUnknown(gvk.String())
155189
}
156190

157191
return r.Resource, nil

errors.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
"github.com/gdt-dev/gdt/api"
1111
"gopkg.in/yaml.v3"
12-
"k8s.io/apimachinery/pkg/runtime/schema"
1312
)
1413

1514
var (
@@ -181,9 +180,10 @@ func InvalidWithLabels(err error, node *yaml.Node) error {
181180
)
182181
}
183182

184-
// ResourceUnknown returns ErrRuntimeResourceUnknown for a given kind
185-
func ResourceUnknown(gvk schema.GroupVersionKind) error {
186-
return fmt.Errorf("%w: %s", ErrResourceUnknown, gvk)
183+
// ResourceUnknown returns ErrRuntimeResourceUnknown for a given resource or
184+
// kind arg string
185+
func ResourceUnknown(arg string) error {
186+
return fmt.Errorf("%w: %s", ErrResourceUnknown, arg)
187187
}
188188

189189
// ExpectedNotFound returns ErrExpectedNotFound for a given status code or

eval_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ func TestCreateUnknownResource(t *testing.T) {
6767
require.Nil(err)
6868
}
6969

70+
func TestSameNamedKind(t *testing.T) {
71+
testutil.SkipIfNoKind(t)
72+
require := require.New(t)
73+
74+
fp := filepath.Join("testdata", "same-named-kind.yaml")
75+
76+
s, err := gdt.From(fp)
77+
require.Nil(err)
78+
require.NotNil(s)
79+
80+
ctx := gdtcontext.New()
81+
ctx = gdtcontext.RegisterFixture(ctx, "kind", kindfix.New())
82+
83+
err = s.Run(ctx, t)
84+
require.Nil(err)
85+
}
86+
7087
func TestDeleteResourceNotFound(t *testing.T) {
7188
testutil.SkipIfNoKind(t)
7289
require := require.New(t)

0 commit comments

Comments
 (0)