github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/cmdutil/cmdutil.go (about)

     1  // Copyright 2019 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 cmdutil
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"strings"
    23  
    24  	"github.com/GoogleContainerTools/kpt/internal/util/function"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/httputil"
    26  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    27  	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    28  	"github.com/spf13/cobra"
    29  	"golang.org/x/mod/semver"
    30  	"sigs.k8s.io/kustomize/kyaml/kio"
    31  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    32  )
    33  
    34  const (
    35  	StackTraceOnErrors  = "COBRA_STACK_TRACE_ON_ERRORS"
    36  	trueString          = "true"
    37  	Stdout              = "stdout"
    38  	Unwrap              = "unwrap"
    39  	FunctionsCatalogURL = "https://catalog.kpt.dev/catalog-v2.json"
    40  )
    41  
    42  // FixDocs replaces instances of old with new in the docs for c
    43  func FixDocs(old, new string, c *cobra.Command) {
    44  	c.Use = strings.ReplaceAll(c.Use, old, new)
    45  	c.Short = strings.ReplaceAll(c.Short, old, new)
    46  	c.Long = strings.ReplaceAll(c.Long, old, new)
    47  	c.Example = strings.ReplaceAll(c.Example, old, new)
    48  }
    49  
    50  func PrintErrorStacktrace() bool {
    51  	e := os.Getenv(StackTraceOnErrors)
    52  	if StackOnError || e == trueString || e == "1" {
    53  		return true
    54  	}
    55  	return false
    56  }
    57  
    58  // StackOnError if true, will print a stack trace on failure.
    59  var StackOnError bool
    60  
    61  // WriteFnOutput writes the output resources of function commands to provided destination
    62  func WriteFnOutput(dest, content string, fromStdin bool, w io.Writer) error {
    63  	r := strings.NewReader(content)
    64  	switch dest {
    65  	case Stdout:
    66  		// if user specified dest is "stdout" directly write the content as it is already wrapped
    67  		_, err := w.Write([]byte(content))
    68  		return err
    69  	case Unwrap:
    70  		// if user specified dest is "unwrap", write the unwrapped content to the provided writer
    71  		return WriteToOutput(r, w, "")
    72  	case "":
    73  		if fromStdin {
    74  			// if user didn't specify dest, and if input is from STDIN, write the wrapped content provided writer
    75  			// this is same as "stdout" input above
    76  			_, err := w.Write([]byte(content))
    77  			return err
    78  		}
    79  	default:
    80  		// this means user specified a directory as dest, write the content to dest directory
    81  		return WriteToOutput(r, nil, dest)
    82  	}
    83  	return nil
    84  }
    85  
    86  // WriteToOutput reads the input from r and writes the output to either w or outDir
    87  func WriteToOutput(r io.Reader, w io.Writer, outDir string) error {
    88  	var outputs []kio.Writer
    89  	if outDir != "" {
    90  		err := os.MkdirAll(outDir, 0755)
    91  		if err != nil {
    92  			return fmt.Errorf("failed to create output directory %q: %q", outDir, err.Error())
    93  		}
    94  		outputs = []kio.Writer{&kio.LocalPackageWriter{PackagePath: outDir}}
    95  	} else {
    96  		outputs = []kio.Writer{&kio.ByteWriter{
    97  			Writer: w,
    98  			ClearAnnotations: []string{kioutil.IndexAnnotation, kioutil.PathAnnotation,
    99  				kioutil.LegacyIndexAnnotation, kioutil.LegacyPathAnnotation}}, // nolint:staticcheck
   100  		}
   101  	}
   102  
   103  	return kio.Pipeline{
   104  		Inputs:  []kio.Reader{&kio.ByteReader{Reader: r, PreserveSeqIndent: true, WrapBareSeqNode: true}},
   105  		Outputs: outputs}.Execute()
   106  }
   107  
   108  // CheckDirectoryNotPresent returns error if the directory already exists
   109  func CheckDirectoryNotPresent(outDir string) error {
   110  	_, err := os.Stat(outDir)
   111  	if err == nil || os.IsExist(err) {
   112  		return fmt.Errorf("directory %q already exists, please delete the directory and retry", outDir)
   113  	}
   114  	if !os.IsNotExist(err) {
   115  		return err
   116  	}
   117  	return nil
   118  }
   119  
   120  func GetKeywordsFromFlag(cmd *cobra.Command) []string {
   121  	flagVal := cmd.Flag("keywords").Value.String()
   122  	flagVal = strings.TrimPrefix(flagVal, "[")
   123  	flagVal = strings.TrimSuffix(flagVal, "]")
   124  	splitted := strings.Split(flagVal, ",")
   125  	var trimmed []string
   126  	for _, val := range splitted {
   127  		if strings.TrimSpace(val) == "" {
   128  			continue
   129  		}
   130  		trimmed = append(trimmed, strings.TrimSpace(val))
   131  	}
   132  	return trimmed
   133  }
   134  
   135  // SuggestFunctions looks for functions from kpt curated catalog list as well as the Porch
   136  // orchestrator to suggest functions.
   137  func SuggestFunctions(cmd *cobra.Command) []string {
   138  	matchers := []function.Matcher{
   139  		function.TypeMatcher{FnType: cmd.Flag("type").Value.String()},
   140  		function.KeywordsMatcher{Keywords: GetKeywordsFromFlag(cmd)},
   141  	}
   142  	functions := DiscoverFunctions(cmd)
   143  	matched := function.MatchFunctions(functions, matchers...)
   144  	return function.GetNames(matched)
   145  }
   146  
   147  // SuggestKeywords looks for all the unique keywords from Porch functions. This keywords
   148  // can later help users to select functions.
   149  func SuggestKeywords(cmd *cobra.Command) []string {
   150  	functions := DiscoverFunctions(cmd)
   151  	matched := function.MatchFunctions(functions, function.TypeMatcher{FnType: cmd.Flag("type").Value.String()})
   152  	return porch.UnifyKeywords(matched)
   153  }
   154  
   155  func DiscoverFunctions(cmd *cobra.Command) []v1alpha1.Function {
   156  	porchFns := porch.FunctionListGetter{}.Get(cmd.Context())
   157  	catalogV2Fns := fetchCatalogFunctions()
   158  	return append(porchFns, catalogV2Fns...)
   159  }
   160  
   161  // fetchCatalogFunctions returns the list of latest function images from catalog.kpt.dev.
   162  func fetchCatalogFunctions() []v1alpha1.Function {
   163  	content, err := httputil.FetchContent(FunctionsCatalogURL)
   164  	if err != nil {
   165  		return nil
   166  	}
   167  	return parseFunctions(content)
   168  }
   169  
   170  // fnName -> v<major>.<minor> -> catalogEntry
   171  type catalogV2 map[string]map[string]struct {
   172  	LatestPatchVersion string
   173  	Examples           interface{}
   174  	Types              []string
   175  	Keywords           []string
   176  }
   177  
   178  // listImages returns the list of latest images from the input catalog content
   179  func parseFunctions(content string) []v1alpha1.Function {
   180  	var jsonData catalogV2
   181  	err := json.Unmarshal([]byte(content), &jsonData)
   182  	if err != nil {
   183  		return nil
   184  	}
   185  	var functions []v1alpha1.Function
   186  	for fnName, fnInfo := range jsonData {
   187  		var latestVersion string
   188  		var keywords []string
   189  		var fnTypes []v1alpha1.FunctionType
   190  		for _, catalogEntry := range fnInfo {
   191  			version := catalogEntry.LatestPatchVersion
   192  			if semver.Compare(version, latestVersion) == 1 {
   193  				latestVersion = version
   194  				keywords = catalogEntry.Keywords
   195  				for _, tp := range catalogEntry.Types {
   196  					switch tp {
   197  					case "validator":
   198  						fnTypes = append(fnTypes, v1alpha1.FunctionTypeValidator)
   199  					case "mutator":
   200  						fnTypes = append(fnTypes, v1alpha1.FunctionTypeMutator)
   201  					}
   202  				}
   203  			}
   204  		}
   205  		fnName := fmt.Sprintf("%s:%s", fnName, latestVersion)
   206  		functions = append(functions, function.CatalogFunction(fnName, keywords, fnTypes))
   207  	}
   208  	return functions
   209  }