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 }