github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/push/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 push
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  
    27  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    28  	"github.com/GoogleContainerTools/kpt/internal/errors"
    29  	"github.com/GoogleContainerTools/kpt/internal/printer"
    30  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    31  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    32  	"github.com/spf13/cobra"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/cli-runtime/pkg/genericclioptions"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/kustomize/kyaml/kio"
    38  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    39  	"sigs.k8s.io/kustomize/kyaml/yaml"
    40  )
    41  
    42  const (
    43  	command = "cmdrpkgpush"
    44  )
    45  
    46  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    47  	r := &runner{
    48  		ctx: ctx,
    49  		cfg: rcg,
    50  	}
    51  	c := &cobra.Command{
    52  		Use:        "push PACKAGE [DIR]",
    53  		Aliases:    []string{"sink", "write"},
    54  		SuggestFor: []string{},
    55  		Short:      rpkgdocs.PushShort,
    56  		Long:       rpkgdocs.PushShort + "\n" + rpkgdocs.PushLong,
    57  		Example:    rpkgdocs.PushExamples,
    58  		PreRunE:    r.preRunE,
    59  		RunE:       r.runE,
    60  		Hidden:     porch.HidePorchCommands,
    61  	}
    62  	r.Command = c
    63  	return r
    64  }
    65  
    66  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    67  	return newRunner(ctx, rcg).Command
    68  }
    69  
    70  type runner struct {
    71  	ctx     context.Context
    72  	cfg     *genericclioptions.ConfigFlags
    73  	client  client.Client
    74  	Command *cobra.Command
    75  	printer printer.Printer
    76  }
    77  
    78  func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
    79  	const op errors.Op = command + ".preRunE"
    80  	config, err := r.cfg.ToRESTConfig()
    81  	if err != nil {
    82  		return errors.E(op, err)
    83  	}
    84  
    85  	scheme, err := createScheme()
    86  	if err != nil {
    87  		return errors.E(op, err)
    88  	}
    89  
    90  	c, err := client.New(config, client.Options{Scheme: scheme})
    91  	if err != nil {
    92  		return errors.E(op, err)
    93  	}
    94  
    95  	r.client = c
    96  	r.printer = printer.FromContextOrDie(r.ctx)
    97  	return nil
    98  }
    99  
   100  func (r *runner) runE(cmd *cobra.Command, args []string) error {
   101  	const op errors.Op = command + ".runE"
   102  
   103  	if len(args) == 0 {
   104  		return errors.E(op, "PACKAGE is a required positional argument")
   105  	}
   106  
   107  	packageName := args[0]
   108  	var resources map[string]string
   109  	var err error
   110  
   111  	if len(args) > 1 {
   112  		resources, err = readFromDir(args[1])
   113  	} else {
   114  		resources, err = readFromReader(cmd.InOrStdin())
   115  	}
   116  	if err != nil {
   117  		return errors.E(op, err)
   118  	}
   119  
   120  	if err := r.client.Update(r.ctx, &porchapi.PackageRevisionResources{
   121  		TypeMeta: metav1.TypeMeta{
   122  			Kind:       "PackageRevisionResources",
   123  			APIVersion: porchapi.SchemeGroupVersion.Identifier(),
   124  		},
   125  		ObjectMeta: metav1.ObjectMeta{
   126  			Name:      packageName,
   127  			Namespace: *r.cfg.Namespace,
   128  		},
   129  		Spec: porchapi.PackageRevisionResourcesSpec{
   130  			Resources: resources,
   131  		},
   132  	}); err != nil {
   133  		return errors.E(op, err)
   134  	}
   135  	return nil
   136  }
   137  
   138  func readFromDir(dir string) (map[string]string, error) {
   139  	resources := map[string]string{}
   140  	if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
   141  		if err != nil {
   142  			return err
   143  		}
   144  		if !info.Mode().IsRegular() {
   145  			return nil
   146  		}
   147  		rel, err := filepath.Rel(dir, path)
   148  		if err != nil {
   149  			return err
   150  		}
   151  		contents, err := os.ReadFile(path)
   152  		if err != nil {
   153  			return err
   154  		}
   155  		resources[rel] = string(contents)
   156  		return nil
   157  	}); err != nil {
   158  		return nil, err
   159  	}
   160  	return resources, nil
   161  }
   162  
   163  func readFromReader(in io.Reader) (map[string]string, error) {
   164  	rw := &resourceWriter{
   165  		resources: map[string]string{},
   166  	}
   167  
   168  	if err := (kio.Pipeline{
   169  		Inputs: []kio.Reader{&kio.ByteReader{
   170  			Reader:            in,
   171  			PreserveSeqIndent: true,
   172  			WrapBareSeqNode:   true,
   173  		}},
   174  		Outputs: []kio.Writer{rw},
   175  	}.Execute()); err != nil {
   176  		return nil, err
   177  	}
   178  	return rw.resources, nil
   179  }
   180  
   181  func createScheme() (*runtime.Scheme, error) {
   182  	scheme := runtime.NewScheme()
   183  
   184  	for _, api := range (runtime.SchemeBuilder{
   185  		porchapi.AddToScheme,
   186  	}) {
   187  		if err := api(scheme); err != nil {
   188  			return nil, err
   189  		}
   190  	}
   191  	return scheme, nil
   192  }
   193  
   194  type resourceWriter struct {
   195  	resources map[string]string
   196  }
   197  
   198  var _ kio.Writer = &resourceWriter{}
   199  
   200  func (w *resourceWriter) Write(nodes []*yaml.RNode) error {
   201  	paths := map[string][]*yaml.RNode{}
   202  	for _, node := range nodes {
   203  		path := getPath(node)
   204  		paths[path] = append(paths[path], node)
   205  	}
   206  
   207  	buf := &bytes.Buffer{}
   208  	for path, nodes := range paths {
   209  		bw := kio.ByteWriter{
   210  			Writer: buf,
   211  			ClearAnnotations: []string{
   212  				kioutil.PathAnnotation,
   213  				kioutil.IndexAnnotation,
   214  			},
   215  		}
   216  		if err := bw.Write(nodes); err != nil {
   217  			return err
   218  		}
   219  		w.resources[path] = buf.String()
   220  		buf.Reset()
   221  	}
   222  	return nil
   223  }
   224  
   225  func getPath(node *yaml.RNode) string {
   226  	ann := node.GetAnnotations()
   227  	if path, ok := ann[kioutil.PathAnnotation]; ok {
   228  		return path
   229  	}
   230  	ns := node.GetNamespace()
   231  	if ns == "" {
   232  		ns = "non-namespaced"
   233  	}
   234  	name := node.GetName()
   235  	if name == "" {
   236  		name = "unnamed"
   237  	}
   238  	// TODO: harden for escaping etc.
   239  	return path.Join(ns, fmt.Sprintf("%s.yaml", name))
   240  }