...

Source file src/sigs.k8s.io/kustomize/kyaml/fn/framework/command/command.go

Documentation: sigs.k8s.io/kustomize/kyaml/fn/framework/command

     1  // Copyright 2021 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/spf13/cobra"
    14  
    15  	"sigs.k8s.io/kustomize/kyaml/errors"
    16  	"sigs.k8s.io/kustomize/kyaml/fn/framework"
    17  	"sigs.k8s.io/kustomize/kyaml/kio"
    18  	"sigs.k8s.io/kustomize/kyaml/yaml"
    19  )
    20  
    21  type CLIMode byte
    22  
    23  const (
    24  	StandaloneEnabled CLIMode = iota
    25  	StandaloneDisabled
    26  )
    27  
    28  // Build returns a cobra.Command to run a function.
    29  //
    30  // The cobra.Command reads the input from STDIN, invokes the provided processor,
    31  // and then writes the output to STDOUT.
    32  //
    33  // The cobra.Command has a boolean `--stack` flag to print stack traces on failure.
    34  //
    35  // By default, invoking the returned cobra.Command with arguments triggers "standalone" mode.
    36  // In this mode:
    37  // - The first argument must be the name of a file containing the FunctionConfig.
    38  // - The remaining arguments must be filenames containing input resources for ResourceList.Items.
    39  // - The argument "-", if present, will cause resources to be read from STDIN as well.
    40  // The output will be a raw stream of resources (not wrapped in a List type).
    41  // Example usage: `cat input1.yaml | go run main.go config.yaml input2.yaml input3.yaml -`
    42  //
    43  // If mode is `StandaloneDisabled`, all arguments are ignored, and STDIN must contain
    44  // a Kubernetes List type. To pass a function config in this mode, use a ResourceList as the input.
    45  // The output will be of the same type as the input (e.g. ResourceList).
    46  // Example usage: `cat resource_list.yaml | go run main.go`
    47  //
    48  // By default, any error returned by the ResourceListProcessor will be printed to STDERR.
    49  // Set noPrintError to true to suppress this.
    50  func Build(p framework.ResourceListProcessor, mode CLIMode, noPrintError bool) *cobra.Command {
    51  	cmd := cobra.Command{}
    52  
    53  	var printStack bool
    54  	cmd.Flags().BoolVar(&printStack, "stack", false, "print the stack trace on failure")
    55  	cmd.Args = cobra.MinimumNArgs(0)
    56  	cmd.SilenceErrors = true
    57  	cmd.SilenceUsage = true
    58  
    59  	cmd.RunE = func(cmd *cobra.Command, args []string) error {
    60  		var readers []io.Reader
    61  		rw := &kio.ByteReadWriter{
    62  			Writer:                cmd.OutOrStdout(),
    63  			KeepReaderAnnotations: true,
    64  		}
    65  
    66  		if len(args) > 0 && mode == StandaloneEnabled {
    67  			// Don't keep the reader annotations if we are in standalone mode
    68  			rw.KeepReaderAnnotations = false
    69  			// Don't wrap the resources in a resourceList -- we are in
    70  			// standalone mode and writing to stdout to be applied
    71  			rw.NoWrap = true
    72  
    73  			for i := range args {
    74  				// the first argument is the resourceList.FunctionConfig
    75  				if i == 0 {
    76  					var err error
    77  					if rw.FunctionConfig, err = functionConfigFromFile(args[0]); err != nil {
    78  						return errors.Wrap(err)
    79  					}
    80  					continue
    81  				}
    82  				if args[i] == "-" {
    83  					readers = append([]io.Reader{cmd.InOrStdin()}, readers...)
    84  				} else {
    85  					readers = append(readers, &deferredFileReader{path: args[i]})
    86  				}
    87  			}
    88  		} else {
    89  			// legacy kustomize plugin input style
    90  			legacyPlugin := os.Getenv("KUSTOMIZE_PLUGIN_CONFIG_STRING")
    91  			if legacyPlugin != "" && rw.FunctionConfig != nil {
    92  				if err := yaml.Unmarshal([]byte(legacyPlugin), rw.FunctionConfig); err != nil {
    93  					return err
    94  				}
    95  			}
    96  			readers = append(readers, cmd.InOrStdin())
    97  		}
    98  		rw.Reader = io.MultiReader(readers...)
    99  
   100  		err := framework.Execute(p, rw)
   101  		if err != nil && !noPrintError {
   102  			_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v", err)
   103  		}
   104  		// print the stack if requested
   105  		if s := errors.GetStack(err); printStack && s != "" {
   106  			_, _ = fmt.Fprintln(cmd.ErrOrStderr(), s)
   107  		}
   108  		return err
   109  	}
   110  
   111  	return &cmd
   112  }
   113  
   114  // AddGenerateDockerfile adds a "gen" subcommand to create a Dockerfile for building
   115  // the function into a container image.
   116  // The gen command takes one argument: the directory where the Dockerfile will be created.
   117  //
   118  //	go run main.go gen DIR/
   119  func AddGenerateDockerfile(cmd *cobra.Command) {
   120  	gen := &cobra.Command{
   121  		Use:  "gen [DIR]",
   122  		Args: cobra.ExactArgs(1),
   123  		RunE: func(cmd *cobra.Command, args []string) error {
   124  			if err := os.WriteFile(filepath.Join(args[0], "Dockerfile"), []byte(`FROM golang:1.21-alpine as builder
   125  ENV CGO_ENABLED=0
   126  WORKDIR /go/src/
   127  COPY go.mod go.sum ./
   128  RUN go mod download
   129  COPY . .
   130  RUN go build -ldflags '-w -s' -v -o /usr/local/bin/function ./
   131  
   132  FROM alpine:latest
   133  COPY --from=builder /usr/local/bin/function /usr/local/bin/function
   134  ENTRYPOINT ["function"]
   135  `), 0600); err != nil {
   136  				return fmt.Errorf("%w", err)
   137  			}
   138  			return nil
   139  		},
   140  	}
   141  	cmd.AddCommand(gen)
   142  }
   143  
   144  func functionConfigFromFile(file string) (*yaml.RNode, error) {
   145  	b, err := os.ReadFile(file)
   146  	if err != nil {
   147  		return nil, errors.WrapPrefixf(err, "unable to read configuration file %q", file)
   148  	}
   149  	fc, err := yaml.Parse(string(b))
   150  	if err != nil {
   151  		return nil, errors.WrapPrefixf(err, "unable to parse configuration file %q", file)
   152  	}
   153  	return fc, nil
   154  }
   155  
   156  type deferredFileReader struct {
   157  	path      string
   158  	srcReader io.Reader
   159  }
   160  
   161  func (fr *deferredFileReader) Read(dest []byte) (int, error) {
   162  	if fr.srcReader == nil {
   163  		src, err := os.ReadFile(fr.path)
   164  		if err != nil {
   165  			return 0, errors.WrapPrefixf(err, "unable to read input file %s", fr.path)
   166  		}
   167  		fr.srcReader = bytes.NewReader(src)
   168  	}
   169  	return fr.srcReader.Read(dest)
   170  }
   171  

View as plain text