github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/push/command.go (about) 1 // Copyright 2022 The kpt Authors 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 "strings" 27 28 "github.com/GoogleContainerTools/kpt/commands/alpha/rpkg/util" 29 "github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs" 30 "github.com/GoogleContainerTools/kpt/internal/errors" 31 "github.com/GoogleContainerTools/kpt/internal/fnruntime" 32 "github.com/GoogleContainerTools/kpt/internal/util/porch" 33 "github.com/GoogleContainerTools/kpt/pkg/printer" 34 porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" 35 "github.com/spf13/cobra" 36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 "k8s.io/apimachinery/pkg/runtime" 38 "k8s.io/cli-runtime/pkg/genericclioptions" 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 "sigs.k8s.io/kustomize/kyaml/kio" 41 "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 42 "sigs.k8s.io/kustomize/kyaml/yaml" 43 ) 44 45 const ( 46 command = "cmdrpkgpush" 47 ) 48 49 func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner { 50 r := &runner{ 51 ctx: ctx, 52 cfg: rcg, 53 } 54 c := &cobra.Command{ 55 Use: "push PACKAGE [DIR]", 56 Aliases: []string{"sink", "write"}, 57 SuggestFor: []string{}, 58 Short: rpkgdocs.PushShort, 59 Long: rpkgdocs.PushShort + "\n" + rpkgdocs.PushLong, 60 Example: rpkgdocs.PushExamples, 61 PreRunE: r.preRunE, 62 RunE: r.runE, 63 Hidden: porch.HidePorchCommands, 64 } 65 r.Command = c 66 return r 67 } 68 69 func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command { 70 return newRunner(ctx, rcg).Command 71 } 72 73 type runner struct { 74 ctx context.Context 75 cfg *genericclioptions.ConfigFlags 76 client client.Client 77 Command *cobra.Command 78 printer printer.Printer 79 } 80 81 func (r *runner) preRunE(_ *cobra.Command, _ []string) error { 82 const op errors.Op = command + ".preRunE" 83 config, err := r.cfg.ToRESTConfig() 84 if err != nil { 85 return errors.E(op, err) 86 } 87 88 scheme, err := createScheme() 89 if err != nil { 90 return errors.E(op, err) 91 } 92 93 c, err := client.New(config, client.Options{Scheme: scheme}) 94 if err != nil { 95 return errors.E(op, err) 96 } 97 98 r.client = c 99 r.printer = printer.FromContextOrDie(r.ctx) 100 return nil 101 } 102 103 func (r *runner) runE(cmd *cobra.Command, args []string) error { 104 const op errors.Op = command + ".runE" 105 106 if len(args) == 0 { 107 return errors.E(op, "PACKAGE is a required positional argument") 108 } 109 110 packageName := args[0] 111 var resources map[string]string 112 var err error 113 114 if len(args) > 1 { 115 resources, err = readFromDir(args[1]) 116 } else { 117 resources, err = readFromReader(cmd.InOrStdin()) 118 } 119 if err != nil { 120 return errors.E(op, err) 121 } 122 123 pkgResources := porchapi.PackageRevisionResources{ 124 TypeMeta: metav1.TypeMeta{ 125 Kind: "PackageRevisionResources", 126 APIVersion: porchapi.SchemeGroupVersion.Identifier(), 127 }, 128 ObjectMeta: metav1.ObjectMeta{ 129 Name: packageName, 130 Namespace: *r.cfg.Namespace, 131 }, 132 Spec: porchapi.PackageRevisionResourcesSpec{ 133 Resources: resources, 134 }, 135 } 136 137 rv, err := util.GetResourceVersion(&pkgResources) 138 if err != nil { 139 return errors.E(op, err) 140 } 141 pkgResources.ResourceVersion = rv 142 if err = util.RemoveRevisionMetadata(&pkgResources); err != nil { 143 return errors.E(op, err) 144 } 145 146 if err := r.client.Update(r.ctx, &pkgResources); err != nil { 147 return errors.E(op, err) 148 } 149 rs := pkgResources.Status.RenderStatus 150 if rs.Err != "" { 151 r.printer.Printf("Package is updated, but failed to render the package.\n") 152 r.printer.Printf("Error: %s\n", rs.Err) 153 } 154 if len(rs.Result.Items) > 0 { 155 for _, result := range rs.Result.Items { 156 r.printer.Printf("[RUNNING] %q \n", result.Image) 157 printOpt := printer.NewOpt() 158 if result.ExitCode != 0 { 159 r.printer.OptPrintf(printOpt, "[FAIL] %q\n", result.Image) 160 } else { 161 r.printer.OptPrintf(printOpt, "[PASS] %q\n", result.Image) 162 } 163 r.printFnResult(result, printOpt) 164 } 165 } 166 return nil 167 } 168 169 // printFnResult prints given function result in a user friendly 170 // format on kpt CLI. 171 func (r *runner) printFnResult(fnResult *porchapi.Result, opt *printer.Options) { 172 if len(fnResult.Results) > 0 { 173 // function returned structured results 174 var lines []string 175 for _, item := range fnResult.Results { 176 lines = append(lines, str(item)) 177 } 178 ri := &fnruntime.MultiLineFormatter{ 179 Title: "Results", 180 Lines: lines, 181 TruncateOutput: printer.TruncateOutput, 182 } 183 r.printer.OptPrintf(opt, "%s", ri.String()) 184 } 185 } 186 187 // String provides a human-readable message for the result item 188 func str(i porchapi.ResultItem) string { 189 identifier := i.ResourceRef 190 var idStringList []string 191 if identifier != nil { 192 if identifier.APIVersion != "" { 193 idStringList = append(idStringList, identifier.APIVersion) 194 } 195 if identifier.Kind != "" { 196 idStringList = append(idStringList, identifier.Kind) 197 } 198 if identifier.Namespace != "" { 199 idStringList = append(idStringList, identifier.Namespace) 200 } 201 if identifier.Name != "" { 202 idStringList = append(idStringList, identifier.Name) 203 } 204 } 205 formatString := "[%s]" 206 severity := i.Severity 207 // We default Severity to Info when converting a result to a message. 208 if i.Severity == "" { 209 severity = "info" 210 } 211 list := []interface{}{severity} 212 if len(idStringList) > 0 { 213 formatString += " %s" 214 list = append(list, strings.Join(idStringList, "/")) 215 } 216 if i.Field != nil { 217 formatString += " %s" 218 list = append(list, i.Field.Path) 219 } 220 formatString += ": %s" 221 list = append(list, i.Message) 222 return fmt.Sprintf(formatString, list...) 223 } 224 225 func readFromDir(dir string) (map[string]string, error) { 226 resources := map[string]string{} 227 if err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { 228 if err != nil { 229 return err 230 } 231 if !info.Mode().IsRegular() { 232 return nil 233 } 234 rel, err := filepath.Rel(dir, path) 235 if err != nil { 236 return err 237 } 238 contents, err := os.ReadFile(path) 239 if err != nil { 240 return err 241 } 242 resources[rel] = string(contents) 243 return nil 244 }); err != nil { 245 return nil, err 246 } 247 return resources, nil 248 } 249 250 func readFromReader(in io.Reader) (map[string]string, error) { 251 rw := &resourceWriter{ 252 resources: map[string]string{}, 253 } 254 255 if err := (kio.Pipeline{ 256 Inputs: []kio.Reader{&kio.ByteReader{ 257 Reader: in, 258 PreserveSeqIndent: true, 259 WrapBareSeqNode: true, 260 }}, 261 Outputs: []kio.Writer{rw}, 262 }.Execute()); err != nil { 263 return nil, err 264 } 265 return rw.resources, nil 266 } 267 268 func createScheme() (*runtime.Scheme, error) { 269 scheme := runtime.NewScheme() 270 271 for _, api := range (runtime.SchemeBuilder{ 272 porchapi.AddToScheme, 273 }) { 274 if err := api(scheme); err != nil { 275 return nil, err 276 } 277 } 278 return scheme, nil 279 } 280 281 type resourceWriter struct { 282 resources map[string]string 283 } 284 285 var _ kio.Writer = &resourceWriter{} 286 287 func (w *resourceWriter) Write(nodes []*yaml.RNode) error { 288 paths := map[string][]*yaml.RNode{} 289 for _, node := range nodes { 290 path := getPath(node) 291 paths[path] = append(paths[path], node) 292 } 293 294 buf := &bytes.Buffer{} 295 for path, nodes := range paths { 296 bw := kio.ByteWriter{ 297 Writer: buf, 298 ClearAnnotations: []string{ 299 kioutil.PathAnnotation, 300 kioutil.IndexAnnotation, 301 }, 302 } 303 if err := bw.Write(nodes); err != nil { 304 return err 305 } 306 w.resources[path] = buf.String() 307 buf.Reset() 308 } 309 return nil 310 } 311 312 func getPath(node *yaml.RNode) string { 313 ann := node.GetAnnotations() 314 if path, ok := ann[kioutil.PathAnnotation]; ok { 315 return path 316 } 317 ns := node.GetNamespace() 318 if ns == "" { 319 ns = "non-namespaced" 320 } 321 name := node.GetName() 322 if name == "" { 323 name = "unnamed" 324 } 325 // TODO: harden for escaping etc. 326 return path.Join(ns, fmt.Sprintf("%s.yaml", name)) 327 }