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  }