get.porter.sh/porter@v1.3.0/pkg/porter/explain.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"get.porter.sh/porter/pkg/cnab"
    11  	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
    12  	"get.porter.sh/porter/pkg/portercontext"
    13  	"get.porter.sh/porter/pkg/printer"
    14  	"github.com/cnabio/cnab-go/bundle"
    15  )
    16  
    17  type ExplainOpts struct {
    18  	BundleReferenceOptions
    19  	printer.PrintOptions
    20  
    21  	Action string
    22  }
    23  
    24  // PrintableBundle holds a subset of pertinent values to be explained from a bundle
    25  type PrintableBundle struct {
    26  	Name          string                 `json:"name" yaml:"name"`
    27  	Description   string                 `json:"description,omitempty" yaml:"description,omitempty"`
    28  	Version       string                 `json:"version" yaml:"version"`
    29  	PorterVersion string                 `json:"porterVersion,omitempty" yaml:"porterVersion,omitempty"`
    30  	Parameters    []PrintableParameter   `json:"parameters,omitempty" yaml:"parameters,omitempty"`
    31  	Credentials   []PrintableCredential  `json:"credentials,omitempty" yaml:"credentials,omitempty"`
    32  	Outputs       []PrintableOutput      `json:"outputs,omitempty" yaml:"outputs,omitempty"`
    33  	Actions       []PrintableAction      `json:"customActions,omitempty" yaml:"customActions,omitempty"`
    34  	Dependencies  []PrintableDependency  `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
    35  	Mixins        []string               `json:"mixins" yaml:"mixins"`
    36  	Custom        map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"`
    37  }
    38  
    39  type PrintableCredential struct {
    40  	Name        string `json:"name" yaml:"name"`
    41  	Description string `json:"description" yaml:"description"`
    42  	Required    bool   `json:"required" yaml:"required"`
    43  	ApplyTo     string `json:"applyTo" yaml:"applyTo"`
    44  }
    45  
    46  type SortPrintableCredential []PrintableCredential
    47  
    48  func (s SortPrintableCredential) Len() int {
    49  	return len(s)
    50  }
    51  
    52  func (s SortPrintableCredential) Less(i, j int) bool {
    53  	return s[i].Name < s[j].Name
    54  }
    55  
    56  func (s SortPrintableCredential) Swap(i, j int) {
    57  	s[i], s[j] = s[j], s[i]
    58  }
    59  
    60  type PrintableOutput struct {
    61  	Name        string      `json:"name" yaml:"name"`
    62  	Type        interface{} `json:"type" yaml:"type"`
    63  	ApplyTo     string      `json:"applyTo" yaml:"applyTo"`
    64  	Description string      `json:"description" yaml:"description"`
    65  }
    66  
    67  type SortPrintableOutput []PrintableOutput
    68  
    69  func (s SortPrintableOutput) Len() int {
    70  	return len(s)
    71  }
    72  
    73  func (s SortPrintableOutput) Less(i, j int) bool {
    74  	return s[i].Name < s[j].Name
    75  }
    76  
    77  func (s SortPrintableOutput) Swap(i, j int) {
    78  	s[i], s[j] = s[j], s[i]
    79  }
    80  
    81  type PrintableDependency struct {
    82  	Alias     string `json:"alias" yaml:"alias"`
    83  	Reference string `json:"reference" yaml:"reference"`
    84  }
    85  
    86  type PrintableParameter struct {
    87  	param       *bundle.Parameter
    88  	Name        string      `json:"name" yaml:"name"`
    89  	Type        interface{} `json:"type" yaml:"type"`
    90  	Default     interface{} `json:"default" yaml:"default"`
    91  	ApplyTo     string      `json:"applyTo" yaml:"applyTo"`
    92  	Description string      `json:"description" yaml:"description"`
    93  	Required    bool        `json:"required" yaml:"required"`
    94  	Sensitive   bool        `json:"sensitive" yaml:"sensitive"`
    95  }
    96  
    97  type SortPrintableParameter []PrintableParameter
    98  
    99  func (s SortPrintableParameter) Len() int {
   100  	return len(s)
   101  }
   102  
   103  func (s SortPrintableParameter) Less(i, j int) bool {
   104  	return s[i].Name < s[j].Name
   105  }
   106  
   107  func (s SortPrintableParameter) Swap(i, j int) {
   108  	s[i], s[j] = s[j], s[i]
   109  }
   110  
   111  type PrintableAction struct {
   112  	Name     string `json:"name" yaml:"name"`
   113  	Modifies bool   `json:"modifies" yaml:"modifies"`
   114  	// Stateless indicates that the action is purely informational, that credentials are not required, and that the runtime should not keep track of its invocation
   115  	Stateless bool `json:"stateless" yaml:"stateless"`
   116  	// Description describes the action as a user-readable string
   117  	Description string `json:"description" yaml:"description"`
   118  }
   119  
   120  type SortPrintableAction []PrintableAction
   121  
   122  func (s SortPrintableAction) Len() int {
   123  	return len(s)
   124  }
   125  
   126  func (s SortPrintableAction) Less(i, j int) bool {
   127  	return s[i].Name < s[j].Name
   128  }
   129  
   130  func (s SortPrintableAction) Swap(i, j int) {
   131  	s[i], s[j] = s[j], s[i]
   132  }
   133  
   134  func (o *ExplainOpts) Validate(args []string, pctx *portercontext.Context) error {
   135  	// Allow reference to be specified as a positional argument, or using --reference
   136  	if len(args) == 1 {
   137  		o.Reference = args[0]
   138  	} else if len(args) > 1 {
   139  		return fmt.Errorf("only one positional argument may be specified, the bundle reference, but multiple were received: %s", args)
   140  	}
   141  
   142  	err := o.BundleDefinitionOptions.Validate(pctx)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	err = o.ParseFormat()
   148  	if err != nil {
   149  		return err
   150  	}
   151  	if o.Reference != "" {
   152  		o.File = ""
   153  		o.CNABFile = ""
   154  
   155  		return o.validateReference()
   156  	}
   157  	return nil
   158  }
   159  
   160  func (p *Porter) Explain(ctx context.Context, o ExplainOpts) error {
   161  	bundleRef, err := o.GetBundleReference(ctx, p)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	pb, err := generatePrintable(bundleRef.Definition, o.Action)
   167  	if err != nil {
   168  		return fmt.Errorf("unable to print bundle: %w", err)
   169  	}
   170  	return p.printBundleExplain(o, pb, bundleRef.Definition)
   171  }
   172  
   173  func (p *Porter) printBundleExplain(o ExplainOpts, pb *PrintableBundle, bun cnab.ExtendedBundle) error {
   174  	switch o.Format {
   175  	case printer.FormatJson:
   176  		return printer.PrintJson(p.Out, pb)
   177  	case printer.FormatYaml:
   178  		return printer.PrintYaml(p.Out, pb)
   179  	case printer.FormatPlaintext:
   180  		return p.printBundleExplainTable(pb, o.Reference, bun)
   181  	default:
   182  		return fmt.Errorf("invalid format: %s", o.Format)
   183  	}
   184  }
   185  
   186  func generatePrintable(bun cnab.ExtendedBundle, action string) (*PrintableBundle, error) {
   187  	var stamp configadapter.Stamp
   188  
   189  	stamp, err := configadapter.LoadStamp(bun)
   190  	if err != nil {
   191  		stamp = configadapter.Stamp{}
   192  	}
   193  
   194  	deps, err := bun.ResolveDependencies(bun)
   195  	if err != nil {
   196  		return nil, fmt.Errorf("error resolving bundle dependencies: %w", err)
   197  	}
   198  
   199  	pb := PrintableBundle{
   200  		Name:          bun.Name,
   201  		Description:   bun.Description,
   202  		Version:       bun.Version,
   203  		PorterVersion: stamp.Version,
   204  		Actions:       make([]PrintableAction, 0, len(bun.Actions)),
   205  		Credentials:   make([]PrintableCredential, 0, len(bun.Credentials)),
   206  		Parameters:    make([]PrintableParameter, 0, len(bun.Parameters)),
   207  		Outputs:       make([]PrintableOutput, 0, len(bun.Outputs)),
   208  		Dependencies:  make([]PrintableDependency, 0, len(deps)),
   209  		Mixins:        make([]string, 0, len(stamp.Mixins)),
   210  		Custom:        make(map[string]interface{}),
   211  	}
   212  
   213  	for a, v := range bun.Actions {
   214  		pa := PrintableAction{}
   215  		pa.Name = a
   216  		pa.Description = v.Description
   217  		pa.Modifies = v.Modifies
   218  		pa.Stateless = v.Stateless
   219  		pb.Actions = append(pb.Actions, pa)
   220  	}
   221  	sort.Sort(SortPrintableAction(pb.Actions))
   222  
   223  	for c, v := range bun.Credentials {
   224  		pc := PrintableCredential{}
   225  		pc.Name = c
   226  		pc.Description = v.Description
   227  		pc.Required = v.Required
   228  		pc.ApplyTo = generateApplyToString(v.ApplyTo)
   229  
   230  		if shouldIncludeInExplainOutput(&v, action) {
   231  			pb.Credentials = append(pb.Credentials, pc)
   232  		}
   233  	}
   234  	sort.Sort(SortPrintableCredential(pb.Credentials))
   235  
   236  	for p, v := range bun.Parameters {
   237  		v := v // Go closures are funny like that
   238  		if bun.IsInternalParameter(p) || bun.ParameterHasSource(p) {
   239  			continue
   240  		}
   241  
   242  		def, ok := bun.Definitions[v.Definition]
   243  		if !ok {
   244  			return nil, fmt.Errorf("unable to find definition %s", v.Definition)
   245  		}
   246  		if def == nil {
   247  			return nil, fmt.Errorf("empty definition for %s", v.Definition)
   248  		}
   249  		pp := PrintableParameter{param: &v}
   250  		pp.Name = p
   251  		pp.Type = bun.GetParameterType(def)
   252  		pp.Default = def.Default
   253  		pp.ApplyTo = generateApplyToString(v.ApplyTo)
   254  		pp.Required = v.Required
   255  		pp.Description = v.Description
   256  		pp.Sensitive = bun.IsSensitiveParameter((p))
   257  
   258  		if shouldIncludeInExplainOutput(&v, action) {
   259  			pb.Parameters = append(pb.Parameters, pp)
   260  		}
   261  	}
   262  	sort.Sort(SortPrintableParameter(pb.Parameters))
   263  
   264  	for o, v := range bun.Outputs {
   265  		if bun.IsInternalOutput(o) {
   266  			continue
   267  		}
   268  
   269  		def, ok := bun.Definitions[v.Definition]
   270  		if !ok {
   271  			return nil, fmt.Errorf("unable to find definition %s", v.Definition)
   272  		}
   273  		if def == nil {
   274  			return nil, fmt.Errorf("empty definition for %s", v.Definition)
   275  		}
   276  		po := PrintableOutput{}
   277  		po.Name = o
   278  		po.Type = def.Type
   279  		po.ApplyTo = generateApplyToString(v.ApplyTo)
   280  		po.Description = v.Description
   281  
   282  		if shouldIncludeInExplainOutput(&v, action) {
   283  			pb.Outputs = append(pb.Outputs, po)
   284  		}
   285  	}
   286  	sort.Sort(SortPrintableOutput(pb.Outputs))
   287  
   288  	for _, dep := range deps {
   289  		pd := PrintableDependency{}
   290  		pd.Alias = dep.Alias
   291  		pd.Reference = dep.Reference
   292  
   293  		pb.Dependencies = append(pb.Dependencies, pd)
   294  	}
   295  	// dependencies are sorted by their dependency sequence already
   296  
   297  	for mixin := range stamp.Mixins {
   298  		pb.Mixins = append(pb.Mixins, mixin)
   299  	}
   300  	sort.Strings(pb.Mixins)
   301  
   302  	for key, value := range bun.Custom {
   303  		if isUserDefinedCustomSectionKey(key) {
   304  			pb.Custom[key] = value
   305  		}
   306  	}
   307  
   308  	return &pb, nil
   309  }
   310  
   311  // shouldIncludeInExplainOutput determine if a scoped item such as a credential, parameter or output
   312  // should be included in the explain output.
   313  func shouldIncludeInExplainOutput(scoped bundle.Scoped, action string) bool {
   314  	if action == "" {
   315  		return true
   316  	}
   317  
   318  	return bundle.AppliesTo(scoped, action)
   319  }
   320  
   321  // isUserDefinedCustomSectionKey returns true if the given key in the custom section data is
   322  // user-defined and not one that Porter uses for its own purposes.
   323  func isUserDefinedCustomSectionKey(key string) bool {
   324  	porterKeyPrefixes := []string{
   325  		"io.cnab",
   326  		"sh.porter",
   327  	}
   328  
   329  	for _, keyPrefix := range porterKeyPrefixes {
   330  		if strings.HasPrefix(key, keyPrefix) {
   331  			return false
   332  		}
   333  	}
   334  
   335  	return true
   336  }
   337  
   338  func generateApplyToString(appliesTo []string) string {
   339  	if len(appliesTo) == 0 {
   340  		return "All Actions"
   341  	}
   342  	return strings.Join(appliesTo, ",")
   343  
   344  }
   345  
   346  func (p *Porter) printBundleExplainTable(bun *PrintableBundle, bundleReference string, extendedBundle cnab.ExtendedBundle) error {
   347  	fmt.Fprintf(p.Out, "Name: %s\n", bun.Name)
   348  	fmt.Fprintf(p.Out, "Description: %s\n", bun.Description)
   349  	fmt.Fprintf(p.Out, "Version: %s\n", bun.Version)
   350  	if bun.PorterVersion != "" {
   351  		fmt.Fprintf(p.Out, "Porter Version: %s\n", bun.PorterVersion)
   352  	}
   353  	fmt.Fprintln(p.Out, "")
   354  
   355  	err := p.printCredentialsExplainBlock(bun)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	err = p.printParametersExplainBlock(bun)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	err = p.printOutputsExplainBlock(bun)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	err = p.printActionsExplainBlock(bun)
   368  	if err != nil {
   369  		return err
   370  	}
   371  	err = p.printDependenciesExplainBlock(bun)
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	if extendedBundle.IsPorterBundle() && len(bun.Mixins) > 0 {
   377  		fmt.Fprintf(p.Out, "This bundle uses the following tools: %s.\n", strings.Join(bun.Mixins, ", "))
   378  	}
   379  
   380  	if extendedBundle.SupportsDocker() {
   381  		fmt.Fprintln(p.Out, "") // force a blank line before this block
   382  		fmt.Fprintf(p.Out, "🚨 This bundle will grant docker access to the host, make sure the publisher of this bundle is trusted.")
   383  		fmt.Fprintln(p.Out, "") // force a blank line after this block
   384  	}
   385  
   386  	err = p.printInstallationInstructionBlock(bun, bundleReference, extendedBundle)
   387  	if err != nil {
   388  		return err
   389  	}
   390  	return nil
   391  }
   392  
   393  func (p *Porter) printCredentialsExplainBlock(bun *PrintableBundle) error {
   394  	if len(bun.Credentials) == 0 {
   395  		return nil
   396  	}
   397  
   398  	fmt.Fprintln(p.Out, "Credentials:")
   399  	err := p.printCredentialsExplainTable(bun)
   400  	if err != nil {
   401  		return fmt.Errorf("unable to print credentials table: %w", err)
   402  	}
   403  
   404  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   405  	return nil
   406  }
   407  func (p *Porter) printCredentialsExplainTable(bun *PrintableBundle) error {
   408  	printCredRow :=
   409  		func(v interface{}) []string {
   410  			c, ok := v.(PrintableCredential)
   411  			if !ok {
   412  				return nil
   413  			}
   414  			return []string{c.Name, c.Description, strconv.FormatBool(c.Required), c.ApplyTo}
   415  		}
   416  	return printer.PrintTable(p.Out, bun.Credentials, printCredRow, "Name", "Description", "Required", "Applies To")
   417  }
   418  
   419  func (p *Porter) printParametersExplainBlock(bun *PrintableBundle) error {
   420  	if len(bun.Parameters) == 0 {
   421  		return nil
   422  	}
   423  
   424  	fmt.Fprintln(p.Out, "Parameters:")
   425  	err := p.printParametersExplainTable(bun)
   426  	if err != nil {
   427  		return fmt.Errorf("unable to print parameters table: %w", err)
   428  	}
   429  
   430  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   431  	return nil
   432  }
   433  func (p *Porter) printParametersExplainTable(bun *PrintableBundle) error {
   434  	printParamRow :=
   435  		func(v interface{}) []string {
   436  			p, ok := v.(PrintableParameter)
   437  			if !ok {
   438  				return nil
   439  			}
   440  			return []string{p.Name, p.Description, fmt.Sprintf("%v", p.Type), fmt.Sprintf("%v", p.Default), strconv.FormatBool(p.Required), p.ApplyTo}
   441  		}
   442  	return printer.PrintTable(p.Out, bun.Parameters, printParamRow, "Name", "Description", "Type", "Default", "Required", "Applies To")
   443  }
   444  
   445  func (p *Porter) printOutputsExplainBlock(bun *PrintableBundle) error {
   446  	if len(bun.Outputs) == 0 {
   447  		return nil
   448  	}
   449  
   450  	fmt.Fprintln(p.Out, "Outputs:")
   451  	err := p.printOutputsExplainTable(bun)
   452  	if err != nil {
   453  		return fmt.Errorf("unable to print outputs table: %w", err)
   454  	}
   455  
   456  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   457  	return nil
   458  }
   459  
   460  func (p *Porter) printOutputsExplainTable(bun *PrintableBundle) error {
   461  	printOutputRow :=
   462  		func(v interface{}) []string {
   463  			o, ok := v.(PrintableOutput)
   464  			if !ok {
   465  				return nil
   466  			}
   467  			return []string{o.Name, o.Description, fmt.Sprintf("%v", o.Type), o.ApplyTo}
   468  		}
   469  	return printer.PrintTable(p.Out, bun.Outputs, printOutputRow, "Name", "Description", "Type", "Applies To")
   470  }
   471  
   472  func (p *Porter) printActionsExplainBlock(bun *PrintableBundle) error {
   473  	if len(bun.Actions) == 0 {
   474  		return nil
   475  	}
   476  
   477  	fmt.Fprintln(p.Out, "Actions:")
   478  	err := p.printActionsExplainTable(bun)
   479  	if err != nil {
   480  		return fmt.Errorf("unable to print actions block: %w", err)
   481  	}
   482  
   483  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   484  	return nil
   485  }
   486  
   487  func (p *Porter) printActionsExplainTable(bun *PrintableBundle) error {
   488  	printActionRow :=
   489  		func(v interface{}) []string {
   490  			a, ok := v.(PrintableAction)
   491  			if !ok {
   492  				return nil
   493  			}
   494  			return []string{a.Name, a.Description, strconv.FormatBool(a.Modifies), strconv.FormatBool(a.Stateless)}
   495  		}
   496  	return printer.PrintTable(p.Out, bun.Actions, printActionRow, "Name", "Description", "Modifies Installation", "Stateless")
   497  }
   498  
   499  // Dependencies
   500  func (p *Porter) printDependenciesExplainBlock(bun *PrintableBundle) error {
   501  	if len(bun.Dependencies) == 0 {
   502  		return nil
   503  	}
   504  
   505  	fmt.Fprintln(p.Out, "Dependencies:")
   506  	err := p.printDependenciesExplainTable(bun)
   507  	if err != nil {
   508  		return fmt.Errorf("unable to print dependencies table: %w", err)
   509  	}
   510  
   511  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   512  	return nil
   513  }
   514  
   515  func (p *Porter) printDependenciesExplainTable(bun *PrintableBundle) error {
   516  	printDependencyRow :=
   517  		func(v interface{}) []string {
   518  			o, ok := v.(PrintableDependency)
   519  			if !ok {
   520  				return nil
   521  			}
   522  			return []string{o.Alias, o.Reference}
   523  		}
   524  	return printer.PrintTable(p.Out, bun.Dependencies, printDependencyRow, "Alias", "Reference")
   525  }
   526  
   527  func (p *Porter) printInstallationInstructionBlock(bun *PrintableBundle, bundleReference string, extendedBundle cnab.ExtendedBundle) error {
   528  	fmt.Fprintln(p.Out)
   529  	fmt.Fprint(p.Out, "To install this bundle run the following command, passing --param KEY=VALUE for any parameters you want to customize:\n")
   530  
   531  	var bundleReferenceFlag string
   532  	if bundleReference != "" {
   533  		bundleReferenceFlag += " --reference " + bundleReference
   534  	}
   535  
   536  	// Generate predefined credential set first.
   537  	if len(bun.Credentials) > 0 {
   538  		fmt.Fprintf(p.Out, "porter credentials generate mycreds%s\n", bundleReferenceFlag)
   539  	}
   540  
   541  	// Bundle installation instruction
   542  	var requiredParameterFlags string
   543  	for _, parameter := range bun.Parameters {
   544  		// Only include parameters required for install
   545  		if parameter.Required && shouldIncludeInExplainOutput(parameter.param, cnab.ActionInstall) {
   546  			requiredParameterFlags += parameter.Name + "=TODO "
   547  		}
   548  	}
   549  
   550  	if requiredParameterFlags != "" {
   551  		requiredParameterFlags = " --param " + requiredParameterFlags
   552  	}
   553  
   554  	var credentialFlags string
   555  	if len(bun.Credentials) > 0 {
   556  		credentialFlags += " --credential-set mycreds"
   557  	}
   558  
   559  	porterInstallCommand := fmt.Sprintf("porter install%s%s%s", bundleReferenceFlag, requiredParameterFlags, credentialFlags)
   560  
   561  	// Check whether the bundle requires docker socket to be mounted into the bundle.
   562  	// Add flag for docker host access for install command if it requires to do so.
   563  	if extendedBundle.SupportsDocker() {
   564  		porterInstallCommand += " --allow-docker-host-access"
   565  	}
   566  
   567  	fmt.Fprint(p.Out, porterInstallCommand)
   568  	fmt.Fprintln(p.Out, "") // force a blank line after this block
   569  
   570  	return nil
   571  }