github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/push/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 push
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/GoogleContainerTools/kpt/commands/alpha/rpkg/util"
    29  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    30  	"github.com/GoogleContainerTools/kpt/internal/errors"
    31  	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
    32  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    33  	"github.com/GoogleContainerTools/kpt/pkg/printer"
    34  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    35  	"github.com/spf13/cobra"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/cli-runtime/pkg/genericclioptions"
    39  	"sigs.k8s.io/controller-runtime/pkg/client"
    40  	"sigs.k8s.io/kustomize/kyaml/kio"
    41  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    42  	"sigs.k8s.io/kustomize/kyaml/yaml"
    43  )
    44  
    45  const (
    46  	command = "cmdrpkgpush"
    47  )
    48  
    49  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    50  	r := &runner{
    51  		ctx: ctx,
    52  		cfg: rcg,
    53  	}
    54  	c := &cobra.Command{
    55  		Use:        "push PACKAGE [DIR]",
    56  		Aliases:    []string{"sink", "write"},
    57  		SuggestFor: []string{},
    58  		Short:      rpkgdocs.PushShort,
    59  		Long:       rpkgdocs.PushShort + "\n" + rpkgdocs.PushLong,
    60  		Example:    rpkgdocs.PushExamples,
    61  		PreRunE:    r.preRunE,
    62  		RunE:       r.runE,
    63  		Hidden:     porch.HidePorchCommands,
    64  	}
    65  	r.Command = c
    66  	return r
    67  }
    68  
    69  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    70  	return newRunner(ctx, rcg).Command
    71  }
    72  
    73  type runner struct {
    74  	ctx     context.Context
    75  	cfg     *genericclioptions.ConfigFlags
    76  	client  client.Client
    77  	Command *cobra.Command
    78  	printer printer.Printer
    79  }
    80  
    81  func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
    82  	const op errors.Op = command + ".preRunE"
    83  	config, err := r.cfg.ToRESTConfig()
    84  	if err != nil {
    85  		return errors.E(op, err)
    86  	}
    87  
    88  	scheme, err := createScheme()
    89  	if err != nil {
    90  		return errors.E(op, err)
    91  	}
    92  
    93  	c, err := client.New(config, client.Options{Scheme: scheme})
    94  	if err != nil {
    95  		return errors.E(op, err)
    96  	}
    97  
    98  	r.client = c
    99  	r.printer = printer.FromContextOrDie(r.ctx)
   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  	if len(args) == 0 {
   107  		return errors.E(op, "PACKAGE is a required positional argument")
   108  	}
   109  
   110  	packageName := args[0]
   111  	var resources map[string]string
   112  	var err error
   113  
   114  	if len(args) > 1 {
   115  		resources, err = readFromDir(args[1])
   116  	} else {
   117  		resources, err = readFromReader(cmd.InOrStdin())
   118  	}
   119  	if err != nil {
   120  		return errors.E(op, err)
   121  	}
   122  
   123  	pkgResources := porchapi.PackageRevisionResources{
   124  		TypeMeta: metav1.TypeMeta{
   125  			Kind:       "PackageRevisionResources",
   126  			APIVersion: porchapi.SchemeGroupVersion.Identifier(),
   127  		},
   128  		ObjectMeta: metav1.ObjectMeta{
   129  			Name:      packageName,
   130  			Namespace: *r.cfg.Namespace,
   131  		},
   132  		Spec: porchapi.PackageRevisionResourcesSpec{
   133  			Resources: resources,
   134  		},
   135  	}
   136  
   137  	rv, err := util.GetResourceVersion(&pkgResources)
   138  	if err != nil {
   139  		return errors.E(op, err)
   140  	}
   141  	pkgResources.ResourceVersion = rv
   142  	if err = util.RemoveRevisionMetadata(&pkgResources); err != nil {
   143  		return errors.E(op, err)
   144  	}
   145  
   146  	if err := r.client.Update(r.ctx, &pkgResources); err != nil {
   147  		return errors.E(op, err)
   148  	}
   149  	rs := pkgResources.Status.RenderStatus
   150  	if rs.Err != "" {
   151  		r.printer.Printf("Package is updated, but failed to render the package.\n")
   152  		r.printer.Printf("Error: %s\n", rs.Err)
   153  	}
   154  	if len(rs.Result.Items) > 0 {
   155  		for _, result := range rs.Result.Items {
   156  			r.printer.Printf("[RUNNING] %q \n", result.Image)
   157  			printOpt := printer.NewOpt()
   158  			if result.ExitCode != 0 {
   159  				r.printer.OptPrintf(printOpt, "[FAIL] %q\n", result.Image)
   160  			} else {
   161  				r.printer.OptPrintf(printOpt, "[PASS] %q\n", result.Image)
   162  			}
   163  			r.printFnResult(result, printOpt)
   164  		}
   165  	}
   166  	return nil
   167  }
   168  
   169  // printFnResult prints given function result in a user friendly
   170  // format on kpt CLI.
   171  func (r *runner) printFnResult(fnResult *porchapi.Result, opt *printer.Options) {
   172  	if len(fnResult.Results) > 0 {
   173  		// function returned structured results
   174  		var lines []string
   175  		for _, item := range fnResult.Results {
   176  			lines = append(lines, str(item))
   177  		}
   178  		ri := &fnruntime.MultiLineFormatter{
   179  			Title:          "Results",
   180  			Lines:          lines,
   181  			TruncateOutput: printer.TruncateOutput,
   182  		}
   183  		r.printer.OptPrintf(opt, "%s", ri.String())
   184  	}
   185  }
   186  
   187  // String provides a human-readable message for the result item
   188  func str(i porchapi.ResultItem) string {
   189  	identifier := i.ResourceRef
   190  	var idStringList []string
   191  	if identifier != nil {
   192  		if identifier.APIVersion != "" {
   193  			idStringList = append(idStringList, identifier.APIVersion)
   194  		}
   195  		if identifier.Kind != "" {
   196  			idStringList = append(idStringList, identifier.Kind)
   197  		}
   198  		if identifier.Namespace != "" {
   199  			idStringList = append(idStringList, identifier.Namespace)
   200  		}
   201  		if identifier.Name != "" {
   202  			idStringList = append(idStringList, identifier.Name)
   203  		}
   204  	}
   205  	formatString := "[%s]"
   206  	severity := i.Severity
   207  	// We default Severity to Info when converting a result to a message.
   208  	if i.Severity == "" {
   209  		severity = "info"
   210  	}
   211  	list := []interface{}{severity}
   212  	if len(idStringList) > 0 {
   213  		formatString += " %s"
   214  		list = append(list, strings.Join(idStringList, "/"))
   215  	}
   216  	if i.Field != nil {
   217  		formatString += " %s"
   218  		list = append(list, i.Field.Path)
   219  	}
   220  	formatString += ": %s"
   221  	list = append(list, i.Message)
   222  	return fmt.Sprintf(formatString, list...)
   223  }
   224  
   225  func readFromDir(dir string) (map[string]string, error) {
   226  	resources := map[string]string{}
   227  	if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
   228  		if err != nil {
   229  			return err
   230  		}
   231  		if !info.Mode().IsRegular() {
   232  			return nil
   233  		}
   234  		rel, err := filepath.Rel(dir, path)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		contents, err := os.ReadFile(path)
   239  		if err != nil {
   240  			return err
   241  		}
   242  		resources[rel] = string(contents)
   243  		return nil
   244  	}); err != nil {
   245  		return nil, err
   246  	}
   247  	return resources, nil
   248  }
   249  
   250  func readFromReader(in io.Reader) (map[string]string, error) {
   251  	rw := &resourceWriter{
   252  		resources: map[string]string{},
   253  	}
   254  
   255  	if err := (kio.Pipeline{
   256  		Inputs: []kio.Reader{&kio.ByteReader{
   257  			Reader:            in,
   258  			PreserveSeqIndent: true,
   259  			WrapBareSeqNode:   true,
   260  		}},
   261  		Outputs: []kio.Writer{rw},
   262  	}.Execute()); err != nil {
   263  		return nil, err
   264  	}
   265  	return rw.resources, nil
   266  }
   267  
   268  func createScheme() (*runtime.Scheme, error) {
   269  	scheme := runtime.NewScheme()
   270  
   271  	for _, api := range (runtime.SchemeBuilder{
   272  		porchapi.AddToScheme,
   273  	}) {
   274  		if err := api(scheme); err != nil {
   275  			return nil, err
   276  		}
   277  	}
   278  	return scheme, nil
   279  }
   280  
   281  type resourceWriter struct {
   282  	resources map[string]string
   283  }
   284  
   285  var _ kio.Writer = &resourceWriter{}
   286  
   287  func (w *resourceWriter) Write(nodes []*yaml.RNode) error {
   288  	paths := map[string][]*yaml.RNode{}
   289  	for _, node := range nodes {
   290  		path := getPath(node)
   291  		paths[path] = append(paths[path], node)
   292  	}
   293  
   294  	buf := &bytes.Buffer{}
   295  	for path, nodes := range paths {
   296  		bw := kio.ByteWriter{
   297  			Writer: buf,
   298  			ClearAnnotations: []string{
   299  				kioutil.PathAnnotation,
   300  				kioutil.IndexAnnotation,
   301  			},
   302  		}
   303  		if err := bw.Write(nodes); err != nil {
   304  			return err
   305  		}
   306  		w.resources[path] = buf.String()
   307  		buf.Reset()
   308  	}
   309  	return nil
   310  }
   311  
   312  func getPath(node *yaml.RNode) string {
   313  	ann := node.GetAnnotations()
   314  	if path, ok := ann[kioutil.PathAnnotation]; ok {
   315  		return path
   316  	}
   317  	ns := node.GetNamespace()
   318  	if ns == "" {
   319  		ns = "non-namespaced"
   320  	}
   321  	name := node.GetName()
   322  	if name == "" {
   323  		name = "unnamed"
   324  	}
   325  	// TODO: harden for escaping etc.
   326  	return path.Join(ns, fmt.Sprintf("%s.yaml", name))
   327  }