From 7d95d07e0ea5c274e79144a50e672c1e2c13efce Mon Sep 17 00:00:00 2001 From: Alper Rifat Ulucinar Date: Sun, 23 Jan 2022 06:57:09 +0300 Subject: [PATCH] Infer resource scope if one is not explicitly specified with the "--namespaced" command-line option - Fixes #9 - Support CRD short names in partial resource specifications Signed-off-by: Alper Rifat Ulucinar --- cmd/edit_status.go | 134 ++++++++++++++++++++++++++++++++++++++------- go.mod | 1 + go.sum | 3 +- 3 files changed, 116 insertions(+), 22 deletions(-) diff --git a/cmd/edit_status.go b/cmd/edit_status.go index 3d79dd1..66f6a5c 100644 --- a/cmd/edit_status.go +++ b/cmd/edit_status.go @@ -54,13 +54,14 @@ import ( jsonPatch "github.com/evanphx/json-patch" "github.com/pkg/errors" "github.com/spf13/cobra" + "go.uber.org/multierr" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/yaml" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" - metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -79,11 +80,11 @@ const ( envEditor = "EDITOR" envKubeEditor = "KUBE_EDITOR" - delimEditorNames = ":" - + delimEditorNames = ":" defaultEditorName = "vi" - - patternTmpFile = "kubectl-edit-status-" + defaultNamespaced = true + patternTmpFile = "kubectl-edit-status-" + flagNamespaced = "namespaced" fmtUsage = ` kubectl %s [flags] or, @@ -99,6 +100,7 @@ const ( // the current context on a user's KUBECONFIG type EditStatusOptions struct { configFlags *genericclioptions.ConfigFlags + cmd *cobra.Command namespaced bool namespace string @@ -128,7 +130,7 @@ func NewEditStatusOptions(streams genericclioptions.IOStreams) *EditStatusOption // NewCmdEditStatus provides a cobra command wrapping EditStatusOptions func NewCmdEditStatus(streams genericclioptions.IOStreams) *cobra.Command { o := NewEditStatusOptions(streams) - cmd := &cobra.Command{ + o.cmd = &cobra.Command{ Use: fmt.Sprintf(fmtUsage, argEditStatus, argEditStatus), Short: "Edit /status subresource", Example: fmt.Sprintf(fmtEditStatusExample, argEditStatus, argEditStatus), @@ -146,8 +148,8 @@ func NewCmdEditStatus(streams genericclioptions.IOStreams) *cobra.Command { RunE: o.Run, } - cmd.Flags().BoolVar(&o.namespaced, "namespaced", true, "set to false for cluster-scoped resources") - cmd.Flags().StringVarP(&o.resourceEditor, "editor", "e", + o.cmd.Flags().BoolVar(&o.namespaced, flagNamespaced, defaultNamespaced, "set to false for cluster-scoped resources") + o.cmd.Flags().StringVarP(&o.resourceEditor, "editor", "e", fmt.Sprintf("${%s}%s${%s}%s%s", envKubeEditor, delimEditorNames, envEditor, delimEditorNames, defaultEditorName), fmt.Sprintf("editor to use. Either editor name in PATH or path to the editor executable. "+ @@ -155,8 +157,8 @@ func NewCmdEditStatus(streams genericclioptions.IOStreams) *cobra.Command { envKubeEditor, envEditor)) // add K8s generic client flags - o.configFlags.AddFlags(cmd.Flags()) - return cmd + o.configFlags.AddFlags(o.cmd.Flags()) + return o.cmd } // Init ensures that all required arguments and flag values are provided and fills the EditStatusOptions receiver arg @@ -248,25 +250,25 @@ func (o *EditStatusOptions) Run(_ *cobra.Command, _ []string) error { return o.writeResourceStatus(tmpEditFile) } -func (o *EditStatusOptions) storeResource(f *os.File) error { +func (o *EditStatusOptions) storeResourceWithGVR(f *os.File, gvr schema.GroupVersionResource) error { restMapper := restmapper.NewDeferredDiscoveryRESTMapper(o.discoveryClient) - resourceParts := strings.Split(o.resource, ".") - gvr := schema.GroupVersionResource{ - Resource: resourceParts[0], - Group: strings.Join(resourceParts[1:], "."), - } gvrs, err := restMapper.ResourcesFor(gvr) if err != nil { - return errors.Wrapf(err, "cannot get GVRs for partial specification: %s", gvr.String()) + return errors.Wrap(err, "no GVRs found") } + namespaced := defaultNamespaced for _, gvr := range gvrs { var obj *unstructured.Unstructured var ri dynamic.ResourceInterface = o.dynamicClient.Resource(gvr) - if o.namespaced { + namespaced, err = o.isNamespaced(gvr) + if err != nil { + return err + } + if namespaced { ri = ri.(dynamic.NamespaceableResourceInterface).Namespace(o.namespace) } if obj, err = ri.Get(context.TODO(), - o.resourceName, metaV1.GetOptions{}); kerrors.IsNotFound(err) { + o.resourceName, metav1.GetOptions{}); kerrors.IsNotFound(err) { // then resource with the given GVR is not found continue } @@ -291,7 +293,97 @@ func (o *EditStatusOptions) storeResource(f *os.File) error { o.gvk, err = o.restMapper.KindFor(gvr) return errors.Wrapf(err, "cannot get GVK for GVR: %s", gvr.String()) } - return errors.Errorf("resource %s %q not found", o.resource, o.resourceName) + scope := "cluster-scoped" + if namespaced { + scope = fmt.Sprintf("namespaced (in %q)", o.namespace) + } + return errors.Wrapf(err, "%s resource %s %q with GVR %q not found", + scope, o.resource, o.resourceName, gvr.String()) +} + +func (o *EditStatusOptions) storeResource(f *os.File) error { + resourceParts := strings.Split(o.resource, ".") + searchGVRs := []schema.GroupVersionResource{ + { + Resource: resourceParts[0], + Group: strings.Join(resourceParts[1:], "."), + }, + } + // extend list of GVRs to be searched by any matching short names + shortNameGVRs, err := o.resourcesForShortName(resourceParts[0]) + if err != nil { + return err + } + // short-name matching resources are considered with lower priority + searchGVRs = append(searchGVRs, shortNameGVRs...) + var aggregatedErr error + for _, gvr := range searchGVRs { + err := o.storeResourceWithGVR(f, gvr) + // as long as we have no resource match for + // the partial resource specification or + // no object with the given scope & name + // we will continue searching. + t := &meta.NoResourceMatchError{} + if errors.As(err, &t) || kerrors.IsNotFound(err) { + aggregatedErr = multierr.Append(aggregatedErr, err) + continue + } + if err != nil { + return err + } + return nil + } + return errors.Wrap(aggregatedErr, + "cannot find any GVRs for the partial specification, or no objects have been found") +} + +func (o *EditStatusOptions) resourcesForShortName(shortName string) ([]schema.GroupVersionResource, error) { + var result []schema.GroupVersionResource + _, arrResourceList, err := o.discoveryClient.ServerGroupsAndResources() + if err != nil { + return nil, errors.Wrapf(err, "cannot discover all server resources to search for the short name: %s", shortName) + } + for _, resourceList := range arrResourceList { + if resourceList == nil { + continue + } + for _, r := range resourceList.APIResources { + for _, sn := range r.ShortNames { + if sn == shortName { + result = append(result, schema.GroupVersionResource{ + Group: r.Group, + Version: r.Version, + Resource: r.Name, + }) + } + } + } + } + return result, nil +} + +// isNamespaced returns true if the resource is namespaced. +// if "--namespaced" is not explicitly set, then we try to infer +// the resource's scope. +func (o *EditStatusOptions) isNamespaced(gvr schema.GroupVersionResource) (bool, error) { + if o.cmd.Flags().Changed(flagNamespaced) { + return o.namespaced, nil + } + // try to infer resource scope from CRDs + gv := schema.GroupVersion{ + Group: gvr.Group, + Version: gvr.Version, + }.String() + resourceList, err := o.discoveryClient.ServerResourcesForGroupVersion(gv) + if err != nil { + return false, errors.Wrapf(err, "cannot discover server resources for GV: %q", gv) + } + for _, r := range resourceList.APIResources { + if r.Name == gvr.Resource { + return r.Namespaced, nil + } + } + return defaultNamespaced, nil } func (o *EditStatusOptions) editResource(f *os.File) error { @@ -345,7 +437,7 @@ func (o *EditStatusOptions) writeResourceStatus(f *os.File) error { Resource(restMapping.Resource.Resource). Name(o.resourceName). SubResource("status"). - VersionedParams(&metaV1.PatchOptions{}, metaV1.ParameterCodec). + VersionedParams(&metav1.PatchOptions{}, metav1.ParameterCodec). Body(patch). DoRaw(context.TODO()) return errors.Wrapf(err, "cannot merge patch object: GVK: %s, Name: %s", o.gvk.String(), o.resourceName) diff --git a/go.mod b/go.mod index ff5f903..7451c5f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 + go.uber.org/multierr v1.7.0 k8s.io/apimachinery v0.23.2 k8s.io/cli-runtime v0.23.2 k8s.io/client-go v0.23.2 diff --git a/go.sum b/go.sum index dc63b1d..c4125fb 100644 --- a/go.sum +++ b/go.sum @@ -524,8 +524,9 @@ go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpK go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=