github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/get/command.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package get
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    23  	"github.com/GoogleContainerTools/kpt/internal/errors"
    24  	"github.com/GoogleContainerTools/kpt/internal/options"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    26  	"github.com/spf13/cobra"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/fields"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/cli-runtime/pkg/genericclioptions"
    33  	"k8s.io/cli-runtime/pkg/printers"
    34  	"k8s.io/client-go/rest"
    35  	"k8s.io/klog/v2"
    36  	"k8s.io/kubectl/pkg/cmd/get"
    37  )
    38  
    39  const (
    40  	command = "cmdrpkgget"
    41  )
    42  
    43  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    44  	r := &runner{
    45  		ctx:        ctx,
    46  		getFlags:   options.Get{ConfigFlags: rcg},
    47  		printFlags: get.NewGetPrintFlags(),
    48  	}
    49  	cmd := &cobra.Command{
    50  		Use:        "get",
    51  		Aliases:    []string{"list"},
    52  		SuggestFor: []string{},
    53  		Short:      rpkgdocs.GetShort,
    54  		Long:       rpkgdocs.GetShort + "\n" + rpkgdocs.GetLong,
    55  		Example:    rpkgdocs.GetExamples,
    56  		PreRunE:    r.preRunE,
    57  		RunE:       r.runE,
    58  		Hidden:     porch.HidePorchCommands,
    59  	}
    60  	r.Command = cmd
    61  
    62  	// Create flags
    63  	cmd.Flags().StringVar(&r.packageName, "name", "", "Name of the packages to get. Any package whose name contains this value will be included in the results.")
    64  	cmd.Flags().StringVar(&r.revision, "revision", "", "Revision of the packages to get. Any package whose revision matches this value will be included in the results.")
    65  
    66  	r.getFlags.AddFlags(cmd)
    67  	r.printFlags.AddFlags(cmd)
    68  	return r
    69  }
    70  
    71  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    72  	return newRunner(ctx, rcg).Command
    73  }
    74  
    75  type runner struct {
    76  	ctx      context.Context
    77  	getFlags options.Get
    78  	Command  *cobra.Command
    79  
    80  	// Flags
    81  	packageName string
    82  	revision    string
    83  	printFlags  *get.PrintFlags
    84  
    85  	requestTable bool
    86  }
    87  
    88  func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
    89  	// Print the namespace if we're spanning namespaces
    90  	if r.getFlags.AllNamespaces {
    91  		r.printFlags.HumanReadableFlags.WithNamespace = true
    92  	}
    93  
    94  	outputOption := cmd.Flags().Lookup("output").Value.String()
    95  	if strings.Contains(outputOption, "custom-columns") || outputOption == "yaml" || strings.Contains(outputOption, "json") {
    96  		r.requestTable = false
    97  	} else {
    98  		r.requestTable = true
    99  	}
   100  	return nil
   101  }
   102  
   103  func (r *runner) runE(cmd *cobra.Command, args []string) error {
   104  	const op errors.Op = command + ".runE"
   105  
   106  	var objs []runtime.Object
   107  	b, err := r.getFlags.ResourceBuilder()
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if r.requestTable {
   113  		scheme := runtime.NewScheme()
   114  		// Accept PartialObjectMetadata and Table
   115  		if err := metav1.AddMetaToScheme(scheme); err != nil {
   116  			return fmt.Errorf("error building runtime.Scheme: %w", err)
   117  		}
   118  		b = b.WithScheme(scheme, schema.GroupVersion{Version: "v1"})
   119  	} else {
   120  		// We want to print the server version, not whatever version we happen to have compiled in
   121  		b = b.Unstructured()
   122  	}
   123  
   124  	useSelectors := true
   125  	if len(args) > 0 {
   126  		b = b.ResourceNames("packagerevisions", args...)
   127  		// We can't pass selectors here, get an error "Error: selectors and the all flag cannot be used when passing resource/name arguments"
   128  		// TODO: cli-utils bug?  I think there is a metadata.name field selector (used for single object watch)
   129  		useSelectors = false
   130  	} else {
   131  		b = b.ResourceTypes("packagerevisions")
   132  	}
   133  
   134  	if useSelectors {
   135  		fieldSelector := fields.Everything()
   136  		if r.revision != "" {
   137  			fieldSelector = fields.OneTermEqualSelector("spec.revision", r.revision)
   138  		}
   139  		if r.packageName != "" {
   140  			fieldSelector = fields.OneTermEqualSelector("spec.packageName", r.packageName)
   141  		}
   142  		if s := fieldSelector.String(); s != "" {
   143  			b = b.FieldSelectorParam(s)
   144  		} else {
   145  			b = b.SelectAllParam(true)
   146  		}
   147  	}
   148  
   149  	b = b.ContinueOnError().
   150  		Latest().
   151  		Flatten()
   152  
   153  	if r.requestTable {
   154  		b = b.TransformRequests(func(req *rest.Request) {
   155  			req.SetHeader("Accept", strings.Join([]string{
   156  				"application/json;as=Table;g=meta.k8s.io;v=v1",
   157  				"application/json",
   158  			}, ","))
   159  		})
   160  	}
   161  
   162  	res := b.Do()
   163  	if err := res.Err(); err != nil {
   164  		return errors.E(op, err)
   165  	}
   166  
   167  	infos, err := res.Infos()
   168  	if err != nil {
   169  		return errors.E(op, err)
   170  	}
   171  
   172  	// Decode json objects in tables (likely PartialObjectMetadata)
   173  	for _, i := range infos {
   174  		if table, ok := i.Object.(*metav1.Table); ok {
   175  			for i := range table.Rows {
   176  				row := &table.Rows[i]
   177  				if row.Object.Object == nil && row.Object.Raw != nil {
   178  					u := &unstructured.Unstructured{}
   179  					if err := u.UnmarshalJSON(row.Object.Raw); err != nil {
   180  						klog.Warningf("error parsing raw object: %v", err)
   181  					}
   182  					row.Object.Object = u
   183  				}
   184  			}
   185  		}
   186  	}
   187  
   188  	// Apply any filters we couldn't pass down as field selectors
   189  	for _, i := range infos {
   190  		switch obj := i.Object.(type) {
   191  		case *unstructured.Unstructured:
   192  			match, err := r.packageRevisionMatches(obj)
   193  			if err != nil {
   194  				return errors.E(op, err)
   195  			}
   196  			if match {
   197  				objs = append(objs, obj)
   198  			}
   199  		case *metav1.Table:
   200  			// Technically we should have applied this as a field-selector, so this might not be necessary
   201  			if err := r.filterTableRows(obj); err != nil {
   202  				return err
   203  			}
   204  			objs = append(objs, obj)
   205  		default:
   206  			return errors.E(op, fmt.Sprintf("Unrecognized response %T", obj))
   207  		}
   208  	}
   209  
   210  	printer, err := r.printFlags.ToPrinter()
   211  	if err != nil {
   212  		return errors.E(op, err)
   213  	}
   214  
   215  	w := printers.GetNewTabWriter(cmd.OutOrStdout())
   216  	for _, obj := range objs {
   217  		if err := printer.PrintObj(obj, w); err != nil {
   218  			return errors.E(op, err)
   219  		}
   220  	}
   221  	if err := w.Flush(); err != nil {
   222  		return errors.E(op, err)
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func (r *runner) packageRevisionMatches(o *unstructured.Unstructured) (bool, error) {
   229  	packageName, _, err := unstructured.NestedString(o.Object, "spec", "packageName")
   230  	if err != nil {
   231  		return false, err
   232  	}
   233  	revision, _, err := unstructured.NestedString(o.Object, "spec", "revision")
   234  	if err != nil {
   235  		return false, err
   236  	}
   237  	if r.packageName != "" && r.packageName != packageName {
   238  		return false, nil
   239  	}
   240  	if r.revision != "" && r.revision != revision {
   241  		return false, nil
   242  	}
   243  	return true, nil
   244  }
   245  
   246  func findColumn(cols []metav1.TableColumnDefinition, name string) int {
   247  	for i := range cols {
   248  		if cols[i].Name == name {
   249  			return i
   250  		}
   251  	}
   252  	return -1
   253  }
   254  
   255  func getStringCell(cells []interface{}, col int) (string, bool) {
   256  	if col < 0 {
   257  		return "", false
   258  	}
   259  	s, ok := cells[col].(string)
   260  	return s, ok
   261  }
   262  
   263  func (r *runner) filterTableRows(table *metav1.Table) error {
   264  	filtered := make([]metav1.TableRow, 0, len(table.Rows))
   265  	packageNameCol := findColumn(table.ColumnDefinitions, "Package")
   266  	revisionCol := findColumn(table.ColumnDefinitions, "Revision")
   267  
   268  	for i := range table.Rows {
   269  		row := &table.Rows[i]
   270  
   271  		if packageName, ok := getStringCell(row.Cells, packageNameCol); ok {
   272  			if r.packageName != "" && r.packageName != packageName {
   273  				continue
   274  			}
   275  		}
   276  		if revision, ok := getStringCell(row.Cells, revisionCol); ok {
   277  			if r.revision != "" && r.revision != revision {
   278  				continue
   279  			}
   280  		}
   281  
   282  		// Row matches
   283  		filtered = append(filtered, *row)
   284  	}
   285  	table.Rows = filtered
   286  	return nil
   287  }