diff --git a/kyaml/kio/kio.go b/kyaml/kio/kio.go index bb999a074..32bdebb32 100644 --- a/kyaml/kio/kio.go +++ b/kyaml/kio/kio.go @@ -80,6 +80,15 @@ type Pipeline struct { // Execute executes each step in the sequence, returning immediately after encountering // any error as part of the Pipeline. func (p Pipeline) Execute() error { + return p.ExecuteWithCallback(nil) +} + +// PipelineExecuteCallbackFunc defines a callback function that will be called each time a step in the pipeline succeeds. +type PipelineExecuteCallbackFunc = func(op Filter) + +// ExecuteWithCallback executes each step in the sequence, returning immediately after encountering +// any error as part of the Pipeline. The callback will be called each time a step succeeds. +func (p Pipeline) ExecuteWithCallback(callback PipelineExecuteCallbackFunc) error { var result []*yaml.RNode // read from the inputs @@ -99,6 +108,9 @@ func (p Pipeline) Execute() error { var err error for i := range p.Filters { op := p.Filters[i] + if callback != nil { + callback(op) + } result, err = op.Filter(result) if len(result) == 0 || err != nil { return errors.Wrap(err) diff --git a/kyaml/kio/kio_test.go b/kyaml/kio/kio_test.go index 870cc915a..d51301d9b 100644 --- a/kyaml/kio/kio_test.go +++ b/kyaml/kio/kio_test.go @@ -4,9 +4,14 @@ package kio_test import ( + "reflect" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "sigs.k8s.io/kustomize/kyaml/yaml" + . "sigs.k8s.io/kustomize/kyaml/kio" ) @@ -23,6 +28,50 @@ func TestPipe(t *testing.T) { } } -func TestSlice_Write(t *testing.T) { - +type mockCallback struct { + mock.Mock +} + +func (c *mockCallback) Callback(op Filter) { + c.Called(op) +} + +func TestPipelineWithCallback(t *testing.T) { + input := ResourceNodeSlice{yaml.MakeNullNode()} + noopFilter1 := func(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + return nodes, nil + } + noopFilter2 := func(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + return nodes, nil + } + filters := []Filter{ + FilterFunc(noopFilter1), + FilterFunc(noopFilter2), + } + p := Pipeline{ + Inputs: []Reader{input}, + Filters: filters, + Outputs: []Writer{}, + } + + callback := mockCallback{} + // setup expectations. `Times` means the function is called no more than `times`. + callback.On("Callback", mock.Anything).Times(len(filters)) + + err := p.ExecuteWithCallback(callback.Callback) + + if !assert.NoError(t, err) { + assert.FailNow(t, err.Error()) + } + + callback.AssertNumberOfCalls(t, "Callback", len(filters)) + + // assert filters are called in the order they are defined. + for i, filter := range filters { + assert.Equal( + t, + reflect.ValueOf(callback.Calls[i].Arguments[0]).Pointer(), + reflect.ValueOf(filter).Pointer(), + ) + } } diff --git a/kyaml/runfn/runfn.go b/kyaml/runfn/runfn.go index b6a5ce166..5c3b99fb7 100644 --- a/kyaml/runfn/runfn.go +++ b/kyaml/runfn/runfn.go @@ -74,6 +74,12 @@ type RunFns struct { // ResultsDir is where to write each functions results ResultsDir string + // LogSteps enables logging the function that is running. + LogSteps bool + + // LogWriter can be set to write the logs to LogWriter rather than stderr if LogSteps is enabled. + LogWriter io.Writer + // resultsCount is used to generate the results filename for each container resultsCount uint32 @@ -169,8 +175,33 @@ func (r RunFns) runFunctions( // the output is nil (reading from Input) outputs = append(outputs, kio.ByteWriter{Writer: r.Output}) } - err := kio.Pipeline{ - Inputs: []kio.Reader{input}, Filters: fltrs, Outputs: outputs}.Execute() + + var err error + pipeline := kio.Pipeline{ + Inputs: []kio.Reader{input}, + Filters: fltrs, + Outputs: outputs, + } + if r.LogSteps { + err = pipeline.ExecuteWithCallback(func(op kio.Filter) { + var identifier string + + switch filter := op.(type) { + case *container.Filter: + identifier = filter.Image + case *exec.Filter: + identifier = filter.Path + case *starlark.Filter: + identifier = filter.String() + default: + identifier = "unknown-type function" + } + + _, _ = fmt.Fprintf(r.LogWriter, "Running %s\n", identifier) + }) + } else { + err = pipeline.Execute() + } if err != nil { return err } @@ -333,6 +364,11 @@ func (r *RunFns) init() { if r.functionFilterProvider == nil { r.functionFilterProvider = r.ffp } + + // if LogSteps is enabled and LogWriter is not specified, use stderr + if r.LogSteps && r.LogWriter == nil { + r.LogWriter = os.Stderr + } } // ffp provides function filters diff --git a/kyaml/runfn/runfn_test.go b/kyaml/runfn/runfn_test.go index 860bae9a2..333b641be 100644 --- a/kyaml/runfn/runfn_test.go +++ b/kyaml/runfn/runfn_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/copyutil" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/fn/runtime/container" @@ -906,6 +907,36 @@ func TestCmd_Execute_setInput(t *testing.T) { assert.Contains(t, string(b), "kind: StatefulSet") } +// TestCmd_Execute_enableLogSteps tests the execution of a filter with LogSteps enabled. +func TestCmd_Execute_enableLogSteps(t *testing.T) { + dir := setupTest(t) + defer os.RemoveAll(dir) + + // write a test filter to the directory of configuration + if !assert.NoError(t, ioutil.WriteFile( + filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) { + return + } + + logs := &bytes.Buffer{} + instance := RunFns{ + Path: dir, + functionFilterProvider: getFilterProvider(t), + LogSteps: true, + LogWriter: logs, + } + if !assert.NoError(t, instance.Execute()) { + t.FailNow() + } + b, err := ioutil.ReadFile( + filepath.Join(dir, "java", "java-deployment.resource.yaml")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Contains(t, string(b), "kind: StatefulSet") + assert.Equal(t, "Running unknown-type function\n", logs.String()) +} + // setupTest initializes a temp test directory containing test data func setupTest(t *testing.T) string { dir, err := ioutil.TempDir("", "kustomize-kyaml-test")