// Copyright 2021 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package command import ( "bytes" "fmt" "io" "io/ioutil" "os" "path/filepath" "github.com/spf13/cobra" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) type CLIMode byte const ( StandaloneEnabled CLIMode = iota StandaloneDisabled ) // Build returns a cobra.Command to run a function. // // The cobra.Command reads the input from STDIN, invokes the provided processor, // and then writes the output to STDOUT. // // The cobra.Command has a boolean `--stack` flag to print stack traces on failure. // // By default, invoking the returned cobra.Command with arguments triggers "standalone" mode. // In this mode: // - The first argument must be the name of a file containing the FunctionConfig. // - The remaining arguments must be filenames containing input resources for ResourceList.Items. // - The argument "-", if present, will cause resources to be read from STDIN as well. // The output will be a raw stream of resources (not wrapped in a List type). // Example usage: `cat input1.yaml | go run main.go config.yaml input2.yaml input3.yaml -` // // If mode is `StandaloneDisabled`, all arguments are ignored, and STDIN must contain // a Kubernetes List type. To pass a function config in this mode, use a ResourceList as the input. // The output will be of the same type as the input (e.g. ResourceList). // Example usage: `cat resource_list.yaml | go run main.go` // // By default, any error returned by the ResourceListProcessor will be printed to STDERR. // Set noPrintError to true to suppress this. func Build(p framework.ResourceListProcessor, mode CLIMode, noPrintError bool) *cobra.Command { cmd := cobra.Command{} var printStack bool cmd.Flags().BoolVar(&printStack, "stack", false, "print the stack trace on failure") cmd.Args = cobra.MinimumNArgs(0) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.RunE = func(cmd *cobra.Command, args []string) error { var readers []io.Reader rw := &kio.ByteReadWriter{ Writer: cmd.OutOrStdout(), KeepReaderAnnotations: true, } if len(args) > 0 && mode == StandaloneEnabled { // Don't keep the reader annotations if we are in standalone mode rw.KeepReaderAnnotations = false // Don't wrap the resources in a resourceList -- we are in // standalone mode and writing to stdout to be applied rw.NoWrap = true for i := range args { // the first argument is the resourceList.FunctionConfig if i == 0 { var err error if rw.FunctionConfig, err = functionConfigFromFile(args[0]); err != nil { return errors.Wrap(err) } continue } if args[i] == "-" { readers = append([]io.Reader{cmd.InOrStdin()}, readers...) } else { readers = append(readers, &deferredFileReader{path: args[i]}) } } } else { // legacy kustomize plugin input style legacyPlugin := os.Getenv("KUSTOMIZE_PLUGIN_CONFIG_STRING") if legacyPlugin != "" && rw.FunctionConfig != nil { if err := yaml.Unmarshal([]byte(legacyPlugin), rw.FunctionConfig); err != nil { return err } } readers = append(readers, cmd.InOrStdin()) } rw.Reader = io.MultiReader(readers...) err := framework.Execute(p, rw) if err != nil && !noPrintError { _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v", err) } // print the stack if requested if s := errors.GetStack(err); printStack && s != "" { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), s) } return err } return &cmd } // AddGenerateDockerfile adds a "gen" subcommand to create a Dockerfile for building // the function into a container image. // The gen command takes one argument: the directory where the Dockerfile will be created. // // go run main.go gen DIR/ func AddGenerateDockerfile(cmd *cobra.Command) { gen := &cobra.Command{ Use: "gen [DIR]", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return ioutil.WriteFile(filepath.Join(args[0], "Dockerfile"), []byte(`FROM golang:1.15-alpine as builder ENV CGO_ENABLED=0 WORKDIR /go/src/ COPY . . RUN go build -tags netgo -ldflags '-w' -v -o /usr/local/bin/function ./ FROM alpine:latest COPY --from=builder /usr/local/bin/function /usr/local/bin/function CMD ["function"] `), 0600) }, } cmd.AddCommand(gen) } func functionConfigFromFile(file string) (*yaml.RNode, error) { b, err := ioutil.ReadFile(file) if err != nil { return nil, errors.WrapPrefixf(err, "unable to read configuration file %q", file) } fc, err := yaml.Parse(string(b)) if err != nil { return nil, errors.WrapPrefixf(err, "unable to parse configuration file %q", file) } return fc, nil } type deferredFileReader struct { path string srcReader io.Reader } func (fr *deferredFileReader) Read(dest []byte) (int, error) { if fr.srcReader == nil { src, err := ioutil.ReadFile(fr.path) if err != nil { return 0, errors.WrapPrefixf(err, "unable to read input file %s", fr.path) } fr.srcReader = bytes.NewReader(src) } return fr.srcReader.Read(dest) }