github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/builder/writer/human_readable.go (about) 1 package writer 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strings" 8 "text/tabwriter" 9 "text/template" 10 11 strs "github.com/buildpacks/pack/internal/strings" 12 "github.com/buildpacks/pack/pkg/client" 13 14 "github.com/buildpacks/pack/internal/style" 15 16 "github.com/buildpacks/pack/pkg/dist" 17 18 pubbldr "github.com/buildpacks/pack/builder" 19 20 "github.com/buildpacks/pack/internal/config" 21 22 "github.com/buildpacks/pack/internal/builder" 23 "github.com/buildpacks/pack/pkg/logging" 24 ) 25 26 const ( 27 writerMinWidth = 0 28 writerTabWidth = 0 29 buildpacksTabWidth = 8 30 extensionsTabWidth = 8 31 defaultTabWidth = 4 32 writerPadChar = ' ' 33 writerFlags = 0 34 none = "(none)" 35 36 outputTemplate = ` 37 {{ if ne .Info.Description "" -}} 38 Description: {{ .Info.Description }} 39 40 {{ end -}} 41 {{- if ne .Info.CreatedBy.Name "" -}} 42 Created By: 43 Name: {{ .Info.CreatedBy.Name }} 44 Version: {{ .Info.CreatedBy.Version }} 45 46 {{ end -}} 47 48 Trusted: {{.Trusted}} 49 50 {{ if ne .Info.Stack "" -}}Stack: 51 ID: {{ .Info.Stack }}{{ end -}} 52 {{- if .Verbose}} 53 {{- if ne (len .Info.Mixins) 0 }} 54 Mixins: 55 {{- end }} 56 {{- range $index, $mixin := .Info.Mixins }} 57 {{ $mixin }} 58 {{- end }} 59 {{- end }} 60 {{ .Lifecycle }} 61 {{ .RunImages }} 62 {{ .Buildpacks }} 63 {{ .Order }} 64 {{- if ne .Extensions "" }} 65 {{ .Extensions }} 66 {{- end }} 67 {{- if ne .OrderExtensions "" }} 68 {{ .OrderExtensions }} 69 {{- end }}` 70 ) 71 72 type HumanReadable struct{} 73 74 func NewHumanReadable() *HumanReadable { 75 return &HumanReadable{} 76 } 77 78 func (h *HumanReadable) Print( 79 logger logging.Logger, 80 localRunImages []config.RunImage, 81 local, remote *client.BuilderInfo, 82 localErr, remoteErr error, 83 builderInfo SharedBuilderInfo, 84 ) error { 85 if local == nil && remote == nil { 86 return fmt.Errorf("unable to find builder '%s' locally or remotely", builderInfo.Name) 87 } 88 89 if builderInfo.IsDefault { 90 logger.Infof("Inspecting default builder: %s\n", style.Symbol(builderInfo.Name)) 91 } else { 92 logger.Infof("Inspecting builder: %s\n", style.Symbol(builderInfo.Name)) 93 } 94 95 logger.Info("\nREMOTE:\n") 96 err := writeBuilderInfo(logger, localRunImages, remote, remoteErr, builderInfo) 97 if err != nil { 98 return fmt.Errorf("writing remote builder info: %w", err) 99 } 100 logger.Info("\nLOCAL:\n") 101 err = writeBuilderInfo(logger, localRunImages, local, localErr, builderInfo) 102 if err != nil { 103 return fmt.Errorf("writing local builder info: %w", err) 104 } 105 106 return nil 107 } 108 109 func writeBuilderInfo( 110 logger logging.Logger, 111 localRunImages []config.RunImage, 112 info *client.BuilderInfo, 113 err error, 114 sharedInfo SharedBuilderInfo, 115 ) error { 116 if err != nil { 117 logger.Errorf("%s\n", err) 118 return nil 119 } 120 121 if info == nil { 122 logger.Info("(not present)\n") 123 return nil 124 } 125 126 var warnings []string 127 128 runImagesString, runImagesWarnings, err := runImagesOutput(info.RunImages, localRunImages, sharedInfo.Name) 129 if err != nil { 130 return fmt.Errorf("compiling run images output: %w", err) 131 } 132 orderString, orderWarnings, err := detectionOrderOutput(info.Order, sharedInfo.Name) 133 if err != nil { 134 return fmt.Errorf("compiling detection order output: %w", err) 135 } 136 137 var orderExtString string 138 var orderExtWarnings []string 139 140 if info.Extensions != nil { 141 orderExtString, orderExtWarnings, err = detectionOrderExtOutput(info.OrderExtensions, sharedInfo.Name) 142 if err != nil { 143 return fmt.Errorf("compiling detection order extensions output: %w", err) 144 } 145 } 146 buildpacksString, buildpacksWarnings, err := buildpacksOutput(info.Buildpacks, sharedInfo.Name) 147 if err != nil { 148 return fmt.Errorf("compiling buildpacks output: %w", err) 149 } 150 lifecycleString, lifecycleWarnings := lifecycleOutput(info.Lifecycle, sharedInfo.Name) 151 152 var extensionsString string 153 var extensionsWarnings []string 154 155 if info.Extensions != nil { 156 extensionsString, extensionsWarnings, err = extensionsOutput(info.Extensions, sharedInfo.Name) 157 if err != nil { 158 return fmt.Errorf("compiling extensions output: %w", err) 159 } 160 } 161 162 warnings = append(warnings, runImagesWarnings...) 163 warnings = append(warnings, orderWarnings...) 164 warnings = append(warnings, buildpacksWarnings...) 165 warnings = append(warnings, lifecycleWarnings...) 166 if info.Extensions != nil { 167 warnings = append(warnings, extensionsWarnings...) 168 warnings = append(warnings, orderExtWarnings...) 169 } 170 outputTemplate, _ := template.New("").Parse(outputTemplate) 171 172 err = outputTemplate.Execute( 173 logger.Writer(), 174 &struct { 175 Info client.BuilderInfo 176 Verbose bool 177 Buildpacks string 178 RunImages string 179 Order string 180 Trusted string 181 Lifecycle string 182 Extensions string 183 OrderExtensions string 184 }{ 185 *info, 186 logger.IsVerbose(), 187 buildpacksString, 188 runImagesString, 189 orderString, 190 stringFromBool(sharedInfo.Trusted), 191 lifecycleString, 192 extensionsString, 193 orderExtString, 194 }, 195 ) 196 197 for _, warning := range warnings { 198 logger.Warn(warning) 199 } 200 201 return err 202 } 203 204 type trailingSpaceStrippingWriter struct { 205 output io.Writer 206 207 potentialDiscard []byte 208 } 209 210 func (w *trailingSpaceStrippingWriter) Write(p []byte) (n int, err error) { 211 var doWrite []byte 212 213 for _, b := range p { 214 switch b { 215 case writerPadChar: 216 w.potentialDiscard = append(w.potentialDiscard, b) 217 case '\n': 218 w.potentialDiscard = []byte{} 219 doWrite = append(doWrite, b) 220 default: 221 doWrite = append(doWrite, w.potentialDiscard...) 222 doWrite = append(doWrite, b) 223 w.potentialDiscard = []byte{} 224 } 225 } 226 227 if len(doWrite) > 0 { 228 actualWrote, err := w.output.Write(doWrite) 229 if err != nil { 230 return actualWrote, err 231 } 232 } 233 234 return len(p), nil 235 } 236 237 func stringFromBool(subject bool) string { 238 if subject { 239 return "Yes" 240 } 241 242 return "No" 243 } 244 245 func runImagesOutput( 246 runImages []pubbldr.RunImageConfig, 247 localRunImages []config.RunImage, 248 builderName string, 249 ) (string, []string, error) { 250 output := "Run Images:\n" 251 252 tabWriterBuf := bytes.Buffer{} 253 254 localMirrorTabWriter := tabwriter.NewWriter(&tabWriterBuf, writerMinWidth, writerTabWidth, defaultTabWidth, writerPadChar, writerFlags) 255 err := writeLocalMirrors(localMirrorTabWriter, runImages, localRunImages) 256 if err != nil { 257 return "", []string{}, fmt.Errorf("writing local mirrors: %w", err) 258 } 259 260 var warnings []string 261 262 if len(runImages) == 0 { 263 warnings = append( 264 warnings, 265 fmt.Sprintf("%s does not specify a run image", builderName), 266 "Users must build with an explicitly specified run image", 267 ) 268 } else { 269 for _, runImage := range runImages { 270 if runImage.Image != "" { 271 _, err = fmt.Fprintf(localMirrorTabWriter, " %s\n", runImage.Image) 272 if err != nil { 273 return "", []string{}, fmt.Errorf("writing to tabwriter: %w", err) 274 } 275 } 276 for _, m := range runImage.Mirrors { 277 _, err = fmt.Fprintf(localMirrorTabWriter, " %s\n", m) 278 if err != nil { 279 return "", []string{}, fmt.Errorf("writing to tab writer: %w", err) 280 } 281 } 282 err = localMirrorTabWriter.Flush() 283 if err != nil { 284 return "", []string{}, fmt.Errorf("flushing tab writer: %w", err) 285 } 286 } 287 } 288 runImageOutput := tabWriterBuf.String() 289 if runImageOutput == "" { 290 runImageOutput = fmt.Sprintf(" %s\n", none) 291 } 292 293 output += runImageOutput 294 295 return output, warnings, nil 296 } 297 298 func writeLocalMirrors(logWriter io.Writer, runImages []pubbldr.RunImageConfig, localRunImages []config.RunImage) error { 299 for _, i := range localRunImages { 300 for _, ri := range runImages { 301 if i.Image == ri.Image { 302 for _, m := range i.Mirrors { 303 _, err := fmt.Fprintf(logWriter, " %s\t(user-configured)\n", m) 304 if err != nil { 305 return fmt.Errorf("writing local mirror: %s: %w", m, err) 306 } 307 } 308 } 309 } 310 } 311 312 return nil 313 } 314 315 func extensionsOutput(extensions []dist.ModuleInfo, builderName string) (string, []string, error) { 316 output := "Extensions:\n" 317 318 if len(extensions) == 0 { 319 return fmt.Sprintf("%s %s\n", output, none), nil, nil 320 } 321 322 var ( 323 tabWriterBuf = bytes.Buffer{} 324 spaceStrippingWriter = &trailingSpaceStrippingWriter{ 325 output: &tabWriterBuf, 326 } 327 extensionsTabWriter = tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerPadChar, extensionsTabWidth, writerPadChar, writerFlags) 328 ) 329 330 _, err := fmt.Fprint(extensionsTabWriter, " ID\tNAME\tVERSION\tHOMEPAGE\n") 331 if err != nil { 332 return "", []string{}, fmt.Errorf("writing to tab writer: %w", err) 333 } 334 335 for _, b := range extensions { 336 _, err = fmt.Fprintf(extensionsTabWriter, " %s\t%s\t%s\t%s\n", b.ID, strs.ValueOrDefault(b.Name, "-"), b.Version, strs.ValueOrDefault(b.Homepage, "-")) 337 if err != nil { 338 return "", []string{}, fmt.Errorf("writing to tab writer: %w", err) 339 } 340 } 341 342 err = extensionsTabWriter.Flush() 343 if err != nil { 344 return "", []string{}, fmt.Errorf("flushing tab writer: %w", err) 345 } 346 347 output += tabWriterBuf.String() 348 return output, []string{}, nil 349 } 350 351 func buildpacksOutput(buildpacks []dist.ModuleInfo, builderName string) (string, []string, error) { 352 output := "Buildpacks:\n" 353 354 if len(buildpacks) == 0 { 355 warnings := []string{ 356 fmt.Sprintf("%s has no buildpacks", builderName), 357 "Users must supply buildpacks from the host machine", 358 } 359 360 return fmt.Sprintf("%s %s\n", output, none), warnings, nil 361 } 362 363 var ( 364 tabWriterBuf = bytes.Buffer{} 365 spaceStrippingWriter = &trailingSpaceStrippingWriter{ 366 output: &tabWriterBuf, 367 } 368 buildpacksTabWriter = tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerPadChar, buildpacksTabWidth, writerPadChar, writerFlags) 369 ) 370 371 _, err := fmt.Fprint(buildpacksTabWriter, " ID\tNAME\tVERSION\tHOMEPAGE\n") 372 if err != nil { 373 return "", []string{}, fmt.Errorf("writing to tab writer: %w", err) 374 } 375 376 for _, b := range buildpacks { 377 _, err = fmt.Fprintf(buildpacksTabWriter, " %s\t%s\t%s\t%s\n", b.ID, strs.ValueOrDefault(b.Name, "-"), b.Version, strs.ValueOrDefault(b.Homepage, "-")) 378 if err != nil { 379 return "", []string{}, fmt.Errorf("writing to tab writer: %w", err) 380 } 381 } 382 383 err = buildpacksTabWriter.Flush() 384 if err != nil { 385 return "", []string{}, fmt.Errorf("flushing tab writer: %w", err) 386 } 387 388 output += tabWriterBuf.String() 389 return output, []string{}, nil 390 } 391 392 const lifecycleFormat = ` 393 Lifecycle: 394 Version: %s 395 Buildpack APIs: 396 Deprecated: %s 397 Supported: %s 398 Platform APIs: 399 Deprecated: %s 400 Supported: %s 401 ` 402 403 func lifecycleOutput(lifecycleInfo builder.LifecycleDescriptor, builderName string) (string, []string) { 404 var warnings []string 405 406 version := none 407 if lifecycleInfo.Info.Version != nil { 408 version = lifecycleInfo.Info.Version.String() 409 } 410 411 if version == none { 412 warnings = append(warnings, fmt.Sprintf("%s does not specify a Lifecycle version", builderName)) 413 } 414 415 supportedBuildpackAPIs := stringFromAPISet(lifecycleInfo.APIs.Buildpack.Supported) 416 if supportedBuildpackAPIs == none { 417 warnings = append(warnings, fmt.Sprintf("%s does not specify supported Lifecycle Buildpack APIs", builderName)) 418 } 419 420 supportedPlatformAPIs := stringFromAPISet(lifecycleInfo.APIs.Platform.Supported) 421 if supportedPlatformAPIs == none { 422 warnings = append(warnings, fmt.Sprintf("%s does not specify supported Lifecycle Platform APIs", builderName)) 423 } 424 425 return fmt.Sprintf( 426 lifecycleFormat, 427 version, 428 stringFromAPISet(lifecycleInfo.APIs.Buildpack.Deprecated), 429 supportedBuildpackAPIs, 430 stringFromAPISet(lifecycleInfo.APIs.Platform.Deprecated), 431 supportedPlatformAPIs, 432 ), warnings 433 } 434 435 func stringFromAPISet(versions builder.APISet) string { 436 if len(versions) == 0 { 437 return none 438 } 439 440 return strings.Join(versions.AsStrings(), ", ") 441 } 442 443 const ( 444 branchPrefix = " ├ " 445 lastBranchPrefix = " └ " 446 trunkPrefix = " │ " 447 ) 448 449 func detectionOrderOutput(order pubbldr.DetectionOrder, builderName string) (string, []string, error) { 450 output := "Detection Order:\n" 451 452 if len(order) == 0 { 453 warnings := []string{ 454 fmt.Sprintf("%s has no buildpacks", builderName), 455 "Users must build with explicitly specified buildpacks", 456 } 457 458 return fmt.Sprintf("%s %s\n", output, none), warnings, nil 459 } 460 461 tabWriterBuf := bytes.Buffer{} 462 spaceStrippingWriter := &trailingSpaceStrippingWriter{ 463 output: &tabWriterBuf, 464 } 465 466 detectionOrderTabWriter := tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerTabWidth, defaultTabWidth, writerPadChar, writerFlags) 467 err := writeDetectionOrderGroup(detectionOrderTabWriter, order, "") 468 if err != nil { 469 return "", []string{}, fmt.Errorf("writing detection order group: %w", err) 470 } 471 err = detectionOrderTabWriter.Flush() 472 if err != nil { 473 return "", []string{}, fmt.Errorf("flushing tab writer: %w", err) 474 } 475 476 output += tabWriterBuf.String() 477 return output, []string{}, nil 478 } 479 480 func detectionOrderExtOutput(order pubbldr.DetectionOrder, builderName string) (string, []string, error) { 481 output := "Detection Order (Extensions):\n" 482 483 if len(order) == 0 { 484 return fmt.Sprintf("%s %s\n", output, none), nil, nil 485 } 486 487 tabWriterBuf := bytes.Buffer{} 488 spaceStrippingWriter := &trailingSpaceStrippingWriter{ 489 output: &tabWriterBuf, 490 } 491 492 detectionOrderExtTabWriter := tabwriter.NewWriter(spaceStrippingWriter, writerMinWidth, writerTabWidth, defaultTabWidth, writerPadChar, writerFlags) 493 err := writeDetectionOrderGroup(detectionOrderExtTabWriter, order, "") 494 if err != nil { 495 return "", []string{}, fmt.Errorf("writing detection order group: %w", err) 496 } 497 err = detectionOrderExtTabWriter.Flush() 498 if err != nil { 499 return "", []string{}, fmt.Errorf("flushing tab writer: %w", err) 500 } 501 502 output += tabWriterBuf.String() 503 return output, []string{}, nil 504 } 505 506 func writeDetectionOrderGroup(writer io.Writer, order pubbldr.DetectionOrder, prefix string) error { 507 groupNumber := 0 508 509 for i, orderEntry := range order { 510 lastInGroup := i == len(order)-1 511 includesSubGroup := len(orderEntry.GroupDetectionOrder) > 0 512 513 orderPrefix, err := writeAndUpdateEntryPrefix(writer, lastInGroup, prefix) 514 if err != nil { 515 return fmt.Errorf("writing detection group prefix: %w", err) 516 } 517 518 if includesSubGroup { 519 groupPrefix := orderPrefix 520 521 if orderEntry.ID != "" { 522 err = writeDetectionOrderBuildpack(writer, orderEntry) 523 if err != nil { 524 return fmt.Errorf("writing detection order buildpack: %w", err) 525 } 526 527 if lastInGroup { 528 _, err = fmt.Fprintf(writer, "%s%s", groupPrefix, lastBranchPrefix) 529 if err != nil { 530 return fmt.Errorf("writing to detection order group writer: %w", err) 531 } 532 groupPrefix = fmt.Sprintf("%s ", groupPrefix) 533 } else { 534 _, err = fmt.Fprintf(writer, "%s%s", orderPrefix, lastBranchPrefix) 535 if err != nil { 536 return fmt.Errorf("writing to detection order group writer: %w", err) 537 } 538 groupPrefix = fmt.Sprintf("%s ", groupPrefix) 539 } 540 } 541 542 groupNumber++ 543 _, err = fmt.Fprintf(writer, "Group #%d:\n", groupNumber) 544 if err != nil { 545 return fmt.Errorf("writing to detection order group writer: %w", err) 546 } 547 err = writeDetectionOrderGroup(writer, orderEntry.GroupDetectionOrder, groupPrefix) 548 if err != nil { 549 return fmt.Errorf("writing detection order group: %w", err) 550 } 551 } else { 552 err := writeDetectionOrderBuildpack(writer, orderEntry) 553 if err != nil { 554 return fmt.Errorf("writing detection order buildpack: %w", err) 555 } 556 } 557 } 558 559 return nil 560 } 561 562 func writeAndUpdateEntryPrefix(writer io.Writer, last bool, prefix string) (string, error) { 563 if last { 564 _, err := fmt.Fprintf(writer, "%s%s", prefix, lastBranchPrefix) 565 if err != nil { 566 return "", fmt.Errorf("writing detection order prefix: %w", err) 567 } 568 return fmt.Sprintf("%s%s", prefix, " "), nil 569 } 570 571 _, err := fmt.Fprintf(writer, "%s%s", prefix, branchPrefix) 572 if err != nil { 573 return "", fmt.Errorf("writing detection order prefix: %w", err) 574 } 575 return fmt.Sprintf("%s%s", prefix, trunkPrefix), nil 576 } 577 578 func writeDetectionOrderBuildpack(writer io.Writer, entry pubbldr.DetectionOrderEntry) error { 579 _, err := fmt.Fprintf( 580 writer, 581 "%s\t%s%s\n", 582 entry.FullName(), 583 stringFromOptional(entry.Optional), 584 stringFromCyclical(entry.Cyclical), 585 ) 586 587 if err != nil { 588 return fmt.Errorf("writing buildpack in detection order: %w", err) 589 } 590 591 return nil 592 } 593 594 func stringFromOptional(optional bool) string { 595 if optional { 596 return "(optional)" 597 } 598 599 return "" 600 } 601 602 func stringFromCyclical(cyclical bool) string { 603 if cyclical { 604 return "[cyclic]" 605 } 606 607 return "" 608 }