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 }