Skip to content

Commit 4362bf7

Browse files
authored
Merge pull request kubernetes#89505 from knight42/fix/kubectl-explain-crd
fix(kubectl): explain crds whose resource name is the same as builtin objects
2 parents f9532e4 + 397f107 commit 4362bf7

File tree

4 files changed

+69
-25
lines changed

4 files changed

+69
-25
lines changed

staging/src/k8s.io/kubectl/pkg/cmd/explain/explain.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,14 @@ func (o *ExplainOptions) Run(args []string) error {
123123
// TODO: After we figured out the new syntax to separate group and resource, allow
124124
// the users to use it in explain (kubectl explain <group><syntax><resource>).
125125
// Refer to issue #16039 for why we do this. Refer to PR #15808 that used "/" syntax.
126-
inModel, fieldsPath, err := explain.SplitAndParseResourceRequest(args[0], o.Mapper)
126+
fullySpecifiedGVR, fieldsPath, err := explain.SplitAndParseResourceRequest(args[0], o.Mapper)
127127
if err != nil {
128128
return err
129129
}
130130

131-
// TODO: We should deduce the group for a resource by discovering the supported resources at server.
132-
fullySpecifiedGVR, groupResource := schema.ParseResourceArg(inModel)
133-
gvk := schema.GroupVersionKind{}
134-
if fullySpecifiedGVR != nil {
135-
gvk, _ = o.Mapper.KindFor(*fullySpecifiedGVR)
136-
}
131+
gvk, _ := o.Mapper.KindFor(fullySpecifiedGVR)
137132
if gvk.Empty() {
138-
gvk, err = o.Mapper.KindFor(groupResource.WithVersion(""))
133+
gvk, err = o.Mapper.KindFor(fullySpecifiedGVR.GroupResource().WithVersion(""))
139134
if err != nil {
140135
return err
141136
}
@@ -151,7 +146,7 @@ func (o *ExplainOptions) Run(args []string) error {
151146

152147
schema := o.Schema.LookupResource(gvk)
153148
if schema == nil {
154-
return fmt.Errorf("Couldn't find resource for %q", gvk)
149+
return fmt.Errorf("couldn't find resource for %q", gvk)
155150
}
156151

157152
return explain.PrintModelDescription(fieldsPath, o.Out, schema, gvk, recursive)

staging/src/k8s.io/kubectl/pkg/explain/explain.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import (
2020
"io"
2121
"strings"
2222

23+
"k8s.io/kube-openapi/pkg/util/proto"
24+
2325
"k8s.io/apimachinery/pkg/api/meta"
2426
"k8s.io/apimachinery/pkg/runtime/schema"
25-
"k8s.io/kube-openapi/pkg/util/proto"
2627
)
2728

2829
type fieldsPrinter interface {
@@ -43,10 +44,13 @@ func splitDotNotation(model string) (string, []string) {
4344
}
4445

4546
// SplitAndParseResourceRequest separates the users input into a model and fields
46-
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (string, []string, error) {
47+
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (schema.GroupVersionResource, []string, error) {
4748
inResource, fieldsPath := splitDotNotation(inResource)
48-
inResource, _ = mapper.ResourceSingularizer(inResource)
49-
return inResource, fieldsPath, nil
49+
gvr, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: inResource})
50+
if err != nil {
51+
return schema.GroupVersionResource{}, nil, err
52+
}
53+
return gvr, fieldsPath, nil
5054
}
5155

5256
// PrintModelDescription prints the description of a specific model or dot path.

staging/src/k8s.io/kubectl/pkg/explain/explain_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,38 @@ import (
2121
"testing"
2222

2323
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
2425
"k8s.io/kubectl/pkg/scheme"
2526
)
2627

2728
func TestSplitAndParseResourceRequest(t *testing.T) {
2829
tests := []struct {
2930
name string
30-
inresource string
31+
inResource string
3132

32-
expectedInResource string
33+
expectedGVR schema.GroupVersionResource
3334
expectedFieldsPath []string
3435
expectedErr bool
3536
}{
3637
{
3738
name: "no trailing period",
38-
inresource: "field1.field2.field3",
39+
inResource: "pods.field2.field3",
3940

40-
expectedInResource: "field1",
41+
expectedGVR: schema.GroupVersionResource{Resource: "pods", Version: "v1"},
4142
expectedFieldsPath: []string{"field2", "field3"},
4243
},
4344
{
4445
name: "trailing period with correct fieldsPath",
45-
inresource: "field1.field2.field3.",
46+
inResource: "service.field2.field3.",
4647

47-
expectedInResource: "field1",
48+
expectedGVR: schema.GroupVersionResource{Resource: "services", Version: "v1"},
4849
expectedFieldsPath: []string{"field2", "field3"},
4950
},
5051
{
5152
name: "trailing period with incorrect fieldsPath",
52-
inresource: "field1.field2.field3.",
53+
inResource: "node.field2.field3.",
5354

54-
expectedInResource: "field1",
55+
expectedGVR: schema.GroupVersionResource{Resource: "nodes", Version: "v1"},
5556
expectedFieldsPath: []string{"field2", "field3", ""},
5657
expectedErr: true,
5758
},
@@ -60,13 +61,13 @@ func TestSplitAndParseResourceRequest(t *testing.T) {
6061
mapper := testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...)
6162
for _, tt := range tests {
6263
t.Run(tt.name, func(t *testing.T) {
63-
gotInResource, gotFieldsPath, err := SplitAndParseResourceRequest(tt.inresource, mapper)
64+
gotGVR, gotFieldsPath, err := SplitAndParseResourceRequest(tt.inResource, mapper)
6465
if err != nil {
6566
t.Errorf("unexpected error: %v", err)
6667
}
6768

68-
if !reflect.DeepEqual(tt.expectedInResource, gotInResource) && !tt.expectedErr {
69-
t.Errorf("%s: expected inresource: %s, got: %s", tt.name, tt.expectedInResource, gotInResource)
69+
if !reflect.DeepEqual(tt.expectedGVR, gotGVR) && !tt.expectedErr {
70+
t.Errorf("%s: expected inResource: %s, got: %s", tt.name, tt.expectedGVR, gotGVR)
7071
}
7172

7273
if !reflect.DeepEqual(tt.expectedFieldsPath, gotFieldsPath) && !tt.expectedErr {

test/e2e/apimachinery/crd_publish_openapi.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ var _ = SIGDescribe("CustomResourcePublishOpenAPI [Privileged:ClusterAdmin]", fu
6868
framework.Failf("%v", err)
6969
}
7070

71+
customServiceShortName := "ksvc"
72+
crdSvc, err := setupCRDWithShortName(f, schemaCustomService, "service", customServiceShortName, "v1alpha1")
73+
if err != nil {
74+
framework.Failf("%v", err)
75+
}
76+
7177
meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-foo")
7278
ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name)
7379

@@ -120,6 +126,11 @@ var _ = SIGDescribe("CustomResourcePublishOpenAPI [Privileged:ClusterAdmin]", fu
120126
framework.Failf("%v", err)
121127
}
122128

129+
ginkgo.By("kubectl explain works for CR with the same resource name as built-in object")
130+
if err := verifyKubectlExplain(f.Namespace.Name, customServiceShortName+".spec", `(?s)DESCRIPTION:.*Specification of CustomService.*FIELDS:.*dummy.*<string>.*Dummy property`); err != nil {
131+
framework.Failf("%v", err)
132+
}
133+
123134
ginkgo.By("kubectl explain works to return error when explain is called on property that doesn't exist")
124135
if _, err := framework.RunKubectl(f.Namespace.Name, "explain", crd.Crd.Spec.Names.Plural+".spec.bars2"); err == nil || !strings.Contains(err.Error(), `field "bars2" does not exist`) {
125136
framework.Failf("unexpected no error when explaining property that doesn't exist: %v", err)
@@ -128,6 +139,9 @@ var _ = SIGDescribe("CustomResourcePublishOpenAPI [Privileged:ClusterAdmin]", fu
128139
if err := cleanupCRD(f, crd); err != nil {
129140
framework.Failf("%v", err)
130141
}
142+
if err := cleanupCRD(f, crdSvc); err != nil {
143+
framework.Failf("%v", err)
144+
}
131145
})
132146

133147
/*
@@ -476,7 +490,24 @@ func setupCRD(f *framework.Framework, schema []byte, groupSuffix string, version
476490
return setupCRDAndVerifySchema(f, schema, expect, groupSuffix, versions...)
477491
}
478492

493+
func setupCRDWithShortName(f *framework.Framework, schema []byte, groupSuffix, shortName string, versions ...string) (*crd.TestCrd, error) {
494+
expect := schema
495+
if schema == nil {
496+
// to be backwards compatible, we expect CRD controller to treat
497+
// CRD with nil schema specially and publish an empty schema
498+
expect = []byte(`type: object`)
499+
}
500+
setShortName := func(crd *apiextensionsv1.CustomResourceDefinition) {
501+
crd.Spec.Names.ShortNames = []string{shortName}
502+
}
503+
return setupCRDAndVerifySchemaWithOptions(f, schema, expect, groupSuffix, versions, setShortName)
504+
}
505+
479506
func setupCRDAndVerifySchema(f *framework.Framework, schema, expect []byte, groupSuffix string, versions ...string) (*crd.TestCrd, error) {
507+
return setupCRDAndVerifySchemaWithOptions(f, schema, expect, groupSuffix, versions)
508+
}
509+
510+
func setupCRDAndVerifySchemaWithOptions(f *framework.Framework, schema, expect []byte, groupSuffix string, versions []string, options ...crd.Option) (*crd.TestCrd, error) {
480511
group := fmt.Sprintf("%s-test-%s.example.com", f.BaseName, groupSuffix)
481512
if len(versions) == 0 {
482513
return nil, fmt.Errorf("require at least one version for CRD")
@@ -489,7 +520,7 @@ func setupCRDAndVerifySchema(f *framework.Framework, schema, expect []byte, grou
489520
}
490521
}
491522

492-
crd, err := crd.CreateMultiVersionTestCRD(f, group, func(crd *apiextensionsv1.CustomResourceDefinition) {
523+
options = append(options, func(crd *apiextensionsv1.CustomResourceDefinition) {
493524
var apiVersions []apiextensionsv1.CustomResourceDefinitionVersion
494525
for i, version := range versions {
495526
version := apiextensionsv1.CustomResourceDefinitionVersion{
@@ -514,6 +545,7 @@ func setupCRDAndVerifySchema(f *framework.Framework, schema, expect []byte, grou
514545
}
515546
crd.Spec.Versions = apiVersions
516547
})
548+
crd, err := crd.CreateMultiVersionTestCRD(f, group, options...)
517549
if err != nil {
518550
return nil, fmt.Errorf("failed to create CRD: %v", err)
519551
}
@@ -728,6 +760,18 @@ properties:
728760
pattern: in-tree|out-of-tree
729761
type: string`)
730762

763+
var schemaCustomService = []byte(`description: CustomService CRD for Testing
764+
type: object
765+
properties:
766+
spec:
767+
description: Specification of CustomService
768+
type: object
769+
properties:
770+
dummy:
771+
description: Dummy property.
772+
type: string
773+
`)
774+
731775
var schemaWaldo = []byte(`description: Waldo CRD for Testing
732776
type: object
733777
properties:

0 commit comments

Comments
 (0)