github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/get/command.go (about)

     1  // Copyright 2022 The kpt Authors
     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  	cmd.Flags().StringVar(&r.workspace, "workspace", "",
    66  		"WorkspaceName of the packages to get. Any package whose workspaceName matches this value will be included in the results.")
    67  
    68  	r.getFlags.AddFlags(cmd)
    69  	r.printFlags.AddFlags(cmd)
    70  	return r
    71  }
    72  
    73  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    74  	return newRunner(ctx, rcg).Command
    75  }
    76  
    77  type runner struct {
    78  	ctx      context.Context
    79  	getFlags options.Get
    80  	Command  *cobra.Command
    81  
    82  	// Flags
    83  	packageName string
    84  	revision    string
    85  	workspace   string
    86  	printFlags  *get.PrintFlags
    87  
    88  	requestTable bool
    89  }
    90  
    91  func (r *runner) preRunE(cmd *cobra.Command, _ []string) error {
    92  	// Print the namespace if we're spanning namespaces
    93  	if r.getFlags.AllNamespaces {
    94  		r.printFlags.HumanReadableFlags.WithNamespace = true
    95  	}
    96  
    97  	outputOption := cmd.Flags().Lookup("output").Value.String()
    98  	if strings.Contains(outputOption, "custom-columns") || outputOption == "yaml" || strings.Contains(outputOption, "json") {
    99  		r.requestTable = false
   100  	} else {
   101  		r.requestTable = true
   102  	}
   103  	return nil
   104  }
   105  
   106  func (r *runner) runE(cmd *cobra.Command, args []string) error {
   107  	const op errors.Op = command + ".runE"
   108  
   109  	var objs []runtime.Object
   110  	b, err := r.getFlags.ResourceBuilder()
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	if r.requestTable {
   116  		scheme := runtime.NewScheme()
   117  		// Accept PartialObjectMetadata and Table
   118  		if err := metav1.AddMetaToScheme(scheme); err != nil {
   119  			return fmt.Errorf("error building runtime.Scheme: %w", err)
   120  		}
   121  		b = b.WithScheme(scheme, schema.GroupVersion{Version: "v1"})
   122  	} else {
   123  		// We want to print the server version, not whatever version we happen to have compiled in
   124  		b = b.Unstructured()
   125  	}
   126  
   127  	useSelectors := true
   128  	if len(args) > 0 {
   129  		b = b.ResourceNames("packagerevisions", args...)
   130  		// We can't pass selectors here, get an error "Error: selectors and the all flag cannot be used when passing resource/name arguments"
   131  		// TODO: cli-utils bug?  I think there is a metadata.name field selector (used for single object watch)
   132  		useSelectors = false
   133  	} else {
   134  		b = b.ResourceTypes("packagerevisions")
   135  	}
   136  
   137  	if useSelectors {
   138  		fieldSelector := fields.Everything()
   139  		if r.revision != "" {
   140  			fieldSelector = fields.OneTermEqualSelector("spec.revision", r.revision)
   141  		}
   142  		if r.workspace != "" {
   143  			fieldSelector = fields.OneTermEqualSelector("spec.workspaceName", r.workspace)
   144  		}
   145  		if r.packageName != "" {
   146  			fieldSelector = fields.OneTermEqualSelector("spec.packageName", r.packageName)
   147  		}
   148  		if s := fieldSelector.String(); s != "" {
   149  			b = b.FieldSelectorParam(s)
   150  		} else {
   151  			b = b.SelectAllParam(true)
   152  		}
   153  	}
   154  
   155  	b = b.ContinueOnError().
   156  		Latest().
   157  		Flatten()
   158  
   159  	if r.requestTable {
   160  		b = b.TransformRequests(func(req *rest.Request) {
   161  			req.SetHeader("Accept", strings.Join([]string{
   162  				"application/json;as=Table;g=meta.k8s.io;v=v1",
   163  				"application/json",
   164  			}, ","))
   165  		})
   166  	}
   167  
   168  	res := b.Do()
   169  	if err := res.Err(); err != nil {
   170  		return errors.E(op, err)
   171  	}
   172  
   173  	infos, err := res.Infos()
   174  	if err != nil {
   175  		return errors.E(op, err)
   176  	}
   177  
   178  	// Decode json objects in tables (likely PartialObjectMetadata)
   179  	for _, i := range infos {
   180  		if table, ok := i.Object.(*metav1.Table); ok {
   181  			for i := range table.Rows {
   182  				row := &table.Rows[i]
   183  				if row.Object.Object == nil && row.Object.Raw != nil {
   184  					u := &unstructured.Unstructured{}
   185  					if err := u.UnmarshalJSON(row.Object.Raw); err != nil {
   186  						klog.Warningf("error parsing raw object: %v", err)
   187  					}
   188  					row.Object.Object = u
   189  				}
   190  			}
   191  		}
   192  	}
   193  
   194  	// Apply any filters we couldn't pass down as field selectors
   195  	for _, i := range infos {
   196  		switch obj := i.Object.(type) {
   197  		case *unstructured.Unstructured:
   198  			match, err := r.packageRevisionMatches(obj)
   199  			if err != nil {
   200  				return errors.E(op, err)
   201  			}
   202  			if match {
   203  				objs = append(objs, obj)
   204  			}
   205  		case *metav1.Table:
   206  			// Technically we should have applied this as a field-selector, so this might not be necessary
   207  			if err := r.filterTableRows(obj); err != nil {
   208  				return err
   209  			}
   210  			objs = append(objs, obj)
   211  		default:
   212  			return errors.E(op, fmt.Sprintf("Unrecognized response %T", obj))
   213  		}
   214  	}
   215  
   216  	printer, err := r.printFlags.ToPrinter()
   217  	if err != nil {
   218  		return errors.E(op, err)
   219  	}
   220  
   221  	w := printers.GetNewTabWriter(cmd.OutOrStdout())
   222  	for _, obj := range objs {
   223  		if err := printer.PrintObj(obj, w); err != nil {
   224  			return errors.E(op, err)
   225  		}
   226  	}
   227  	if err := w.Flush(); err != nil {
   228  		return errors.E(op, err)
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  func (r *runner) packageRevisionMatches(o *unstructured.Unstructured) (bool, error) {
   235  	packageName, _, err := unstructured.NestedString(o.Object, "spec", "packageName")
   236  	if err != nil {
   237  		return false, err
   238  	}
   239  	revision, _, err := unstructured.NestedString(o.Object, "spec", "revision")
   240  	if err != nil {
   241  		return false, err
   242  	}
   243  	workspace, _, err := unstructured.NestedString(o.Object, "spec", "workspaceName")
   244  	if err != nil {
   245  		return false, err
   246  	}
   247  	if r.packageName != "" && r.packageName != packageName {
   248  		return false, nil
   249  	}
   250  	if r.revision != "" && r.revision != revision {
   251  		return false, nil
   252  	}
   253  	if r.workspace != "" && r.workspace != workspace {
   254  		return false, nil
   255  	}
   256  	return true, nil
   257  }
   258  
   259  func findColumn(cols []metav1.TableColumnDefinition, name string) int {
   260  	for i := range cols {
   261  		if cols[i].Name == name {
   262  			return i
   263  		}
   264  	}
   265  	return -1
   266  }
   267  
   268  func getStringCell(cells []interface{}, col int) (string, bool) {
   269  	if col < 0 {
   270  		return "", false
   271  	}
   272  	s, ok := cells[col].(string)
   273  	return s, ok
   274  }
   275  
   276  func (r *runner) filterTableRows(table *metav1.Table) error {
   277  	filtered := make([]metav1.TableRow, 0, len(table.Rows))
   278  	packageNameCol := findColumn(table.ColumnDefinitions, "Package")
   279  	revisionCol := findColumn(table.ColumnDefinitions, "Revision")
   280  	workspaceCol := findColumn(table.ColumnDefinitions, "WorkspaceName")
   281  
   282  	for i := range table.Rows {
   283  		row := &table.Rows[i]
   284  
   285  		if packageName, ok := getStringCell(row.Cells, packageNameCol); ok {
   286  			if r.packageName != "" && r.packageName != packageName {
   287  				continue
   288  			}
   289  		}
   290  		if revision, ok := getStringCell(row.Cells, revisionCol); ok {
   291  			if r.revision != "" && r.revision != revision {
   292  				continue
   293  			}
   294  		}
   295  		if workspace, ok := getStringCell(row.Cells, workspaceCol); ok {
   296  			if r.workspace != "" && r.workspace != workspace {
   297  				continue
   298  			}
   299  		}
   300  
   301  		// Row matches
   302  		filtered = append(filtered, *row)
   303  	}
   304  	table.Rows = filtered
   305  	return nil
   306  }