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

     1  package query
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  
     9  	"get.porter.sh/porter/pkg/pkgmgmt"
    10  	"get.porter.sh/porter/pkg/portercontext"
    11  	"get.porter.sh/porter/pkg/tracing"
    12  	"github.com/hashicorp/go-multierror"
    13  	"golang.org/x/sync/errgroup"
    14  )
    15  
    16  // MixinQuery allows us to send a command to a bunch of mixins and collect their response.
    17  type MixinQuery struct {
    18  	*portercontext.Context
    19  
    20  	// RequireMixinResponse indicates if every mixin must return a successful response.
    21  	// Set to true for required mixin commands that need every mixin to respond to.
    22  	// Set to false for optional mixin commands that every mixin may not have implemented.
    23  	RequireAllMixinResponses bool
    24  
    25  	// LogMixinStderr to the supplied contexts Stdout (Context.Out)
    26  	LogMixinErrors bool
    27  
    28  	Mixins pkgmgmt.PackageManager
    29  }
    30  
    31  // New creates a new instance of a MixinQuery.
    32  func New(cxt *portercontext.Context, mixins pkgmgmt.PackageManager) *MixinQuery {
    33  	return &MixinQuery{
    34  		Context: cxt,
    35  		Mixins:  mixins,
    36  	}
    37  }
    38  
    39  // MixinInputGenerator provides data about the mixins to the MixinQuery
    40  // for it to execute upon.
    41  type MixinInputGenerator interface {
    42  	// ListMixins provides the list of mixin names to query over.
    43  	ListMixins() []string
    44  
    45  	// BuildInput generates the input to send to the specified mixin given its name.
    46  	BuildInput(mixinName string) ([]byte, error)
    47  }
    48  
    49  type MixinBuildOutput struct {
    50  	// Name of the mixin.
    51  	Name string
    52  
    53  	// Stdout is the contents of stdout from calling the mixin.
    54  	Stdout string
    55  
    56  	// Error returned when the mixin was called.
    57  	Error error
    58  }
    59  
    60  // Execute the specified command using an input generator.
    61  // For example, the ManifestGenerator will iterate over the mixins in a manifest and send
    62  // them their config and the steps associated with their mixin.
    63  // The mixins are queried in parallel but the results are sorted in the order that the mixins were defined in the manifest.
    64  func (q *MixinQuery) Execute(ctx context.Context, cmd string, inputGenerator MixinInputGenerator) ([]MixinBuildOutput, error) {
    65  	ctx, span := tracing.StartSpan(ctx)
    66  	defer span.EndSpan()
    67  
    68  	mixinNames := inputGenerator.ListMixins()
    69  	results := make([]MixinBuildOutput, len(mixinNames))
    70  	gerr := errgroup.Group{}
    71  
    72  	for i, mn := range mixinNames {
    73  		// Force variables to be in the go routine's closure below
    74  		i := i
    75  		mn := mn
    76  		results[i].Name = mn
    77  
    78  		gerr.Go(func() error {
    79  			// Copy the existing context and tweak to pipe the output differently
    80  			mixinStdout := &bytes.Buffer{}
    81  			mixinContext := *q.Context
    82  			mixinContext.Out = mixinStdout // mixin stdout -> mixin result
    83  
    84  			if q.LogMixinErrors {
    85  				mixinContext.Err = q.Context.Out // mixin stderr -> porter logs
    86  			} else {
    87  				mixinContext.Err = io.Discard
    88  			}
    89  
    90  			inputB, err := inputGenerator.BuildInput(mn)
    91  			if err != nil {
    92  				return err
    93  			}
    94  
    95  			cmd := pkgmgmt.CommandOptions{
    96  				Command: cmd,
    97  				Input:   string(inputB),
    98  			}
    99  			runErr := q.Mixins.Run(ctx, &mixinContext, mn, cmd)
   100  
   101  			results[i].Stdout = mixinStdout.String()
   102  			results[i].Error = runErr
   103  			return nil
   104  		})
   105  	}
   106  
   107  	err := gerr.Wait()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// Collect responses and errors
   113  	var runErr error
   114  	for _, result := range results {
   115  		if result.Error != nil {
   116  			runErr = multierror.Append(runErr,
   117  				fmt.Errorf("error encountered from mixin %q: %w", result.Name, result.Error))
   118  		}
   119  	}
   120  
   121  	if runErr != nil {
   122  		if q.RequireAllMixinResponses {
   123  			return nil, span.Error(runErr)
   124  		}
   125  
   126  		// This is a debug because we expect not all mixins to implement some
   127  		// optional commands, like lint and don't want to print their error
   128  		// message when we query them with a command they don't support.
   129  		span.Debugf(fmt.Errorf("not all mixins responded successfully: %w", runErr).Error())
   130  	}
   131  
   132  	return results, nil
   133  }