From 3de8b5b2b2620419643a534484244cdece6e41b3 Mon Sep 17 00:00:00 2001
From: Vladislav Kuzmin <vkuzmin@mirantis.com>
Date: Fri, 4 Dec 2020 17:51:18 +0400
Subject: [PATCH] Implement kustomize sink in generic container executor

Sink function allows to write configurations to an external system.

Change-Id: If9c6904239a542ea4c2bef2920965b6d87feb1e6
Relates-To: #202
Relates-To: #369
---
 manifests/phases/executors.yaml            |  2 +-
 pkg/api/v1alpha1/genericcontainer_types.go |  6 +-
 pkg/phase/executors/container.go           | 40 ++++++++++---
 pkg/phase/executors/container_test.go      | 70 ++++++++++++++++++++++
 4 files changed, 107 insertions(+), 11 deletions(-)

diff --git a/manifests/phases/executors.yaml b/manifests/phases/executors.yaml
index 42fd390f5..7e1a13cec 100644
--- a/manifests/phases/executors.yaml
+++ b/manifests/phases/executors.yaml
@@ -54,7 +54,7 @@ metadata:
   name: generic-container
   labels:
     airshipit.org/deploy-k8s: "false"
-outputToStdout: true
+kustomizeSinkOutputDir: ""
 spec:
   container:
       image: quay.io/sample/image:v0.0.1
diff --git a/pkg/api/v1alpha1/genericcontainer_types.go b/pkg/api/v1alpha1/genericcontainer_types.go
index d0ef9ef2f..b0c5716cb 100644
--- a/pkg/api/v1alpha1/genericcontainer_types.go
+++ b/pkg/api/v1alpha1/genericcontainer_types.go
@@ -25,8 +25,10 @@ import (
 type GenericContainer struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
-	// If set to will print output of RunFns to Stdout
-	PrintOutput bool `json:"printOutput,omitempty"`
+	// Executor will write output using kustomize sink if this parameter is specified.
+	// Else it will write output to STDOUT.
+	// This path relative to current site root.
+	KustomizeSinkOutputDir string `json:"kustomizeSinkOutputDir,omitempty"`
 	// Settings for for a container
 	Spec runtimeutil.FunctionSpec `json:"spec,omitempty"`
 	// Config for the RunFns function in a custom format
diff --git a/pkg/phase/executors/container.go b/pkg/phase/executors/container.go
index d24d45822..07219d554 100644
--- a/pkg/phase/executors/container.go
+++ b/pkg/phase/executors/container.go
@@ -22,6 +22,7 @@ import (
 	"path/filepath"
 
 	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
+	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/runfn"
 	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
 	"sigs.k8s.io/yaml"
@@ -37,12 +38,13 @@ var _ ifc.Executor = &ContainerExecutor{}
 
 // ContainerExecutor contains resources for generic container executor
 type ContainerExecutor struct {
-	ExecutorBundle   document.Bundle
-	ExecutorDocument document.Document
+	PhaseEntryPointBasePath string
+	ExecutorBundle          document.Bundle
+	ExecutorDocument        document.Document
 
 	ContConf   *v1alpha1.GenericContainer
 	RunFns     runfn.RunFns
-	targetPath string
+	TargetPath string
 }
 
 // NewContainerExecutor creates instance of phase executor
@@ -61,14 +63,15 @@ func NewContainerExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) {
 	}
 
 	return &ContainerExecutor{
-		ExecutorBundle:   bundle,
-		ExecutorDocument: cfg.ExecutorDocument,
+		PhaseEntryPointBasePath: cfg.Helper.PhaseEntryPointBasePath(),
+		ExecutorBundle:          bundle,
+		ExecutorDocument:        cfg.ExecutorDocument,
 
 		ContConf: apiObj,
 		RunFns: runfn.RunFns{
 			Functions: []*kyaml.RNode{},
 		},
-		targetPath: cfg.Helper.TargetPath(),
+		TargetPath: cfg.Helper.TargetPath(),
 	}, nil
 }
 
@@ -102,7 +105,11 @@ func (c *ContainerExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
 
 	c.SetMounts()
 
-	if c.ContConf.PrintOutput {
+	var fnsOutputBuffer bytes.Buffer
+
+	if c.ContConf.KustomizeSinkOutputDir != "" {
+		c.RunFns.Output = &fnsOutputBuffer
+	} else {
 		c.RunFns.Output = os.Stdout
 	}
 
@@ -111,6 +118,13 @@ func (c *ContainerExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
 		return
 	}
 
+	if c.ContConf.KustomizeSinkOutputDir != "" {
+		if err := c.WriteKustomizeSink(&fnsOutputBuffer); err != nil {
+			handleError(evtCh, err)
+			return
+		}
+	}
+
 	evtCh <- events.NewEvent().WithGenericContainerEvent(events.GenericContainerEvent{
 		Operation: events.GenericContainerStop,
 		Message:   "execution of the generic container finished",
@@ -159,11 +173,21 @@ func (c *ContainerExecutor) SetMounts() {
 	}
 	storageMounts := c.ContConf.Spec.Container.StorageMounts
 	for i, mount := range storageMounts {
-		storageMounts[i].Src = filepath.Join(c.targetPath, mount.Src)
+		storageMounts[i].Src = filepath.Join(c.TargetPath, mount.Src)
 	}
 	c.RunFns.StorageMounts = storageMounts
 }
 
+// WriteKustomizeSink writes output to kustomize sink
+func (c *ContainerExecutor) WriteKustomizeSink(fnsOutputBuffer *bytes.Buffer) error {
+	outputDirPath := filepath.Join(c.PhaseEntryPointBasePath, c.ContConf.KustomizeSinkOutputDir)
+	sinkOutputs := []kio.Writer{&kio.LocalPackageWriter{PackagePath: outputDirPath}}
+	err := kio.Pipeline{
+		Inputs:  []kio.Reader{&kio.ByteReader{Reader: fnsOutputBuffer}},
+		Outputs: sinkOutputs}.Execute()
+	return err
+}
+
 // Validate executor configuration and documents
 func (c *ContainerExecutor) Validate() error {
 	return errors.ErrNotImplemented{}
diff --git a/pkg/phase/executors/container_test.go b/pkg/phase/executors/container_test.go
index cc8e69d03..ce06d7df5 100644
--- a/pkg/phase/executors/container_test.go
+++ b/pkg/phase/executors/container_test.go
@@ -219,3 +219,73 @@ func TestPrepareFunctions(t *testing.T) {
 
 	assert.Equal(t, transformedFunction, strFuncs)
 }
+
+func TestSetMounts(t *testing.T) {
+	testCases := []struct {
+		name        string
+		targetPath  string
+		in          []runtimeutil.StorageMount
+		expectedOut []runtimeutil.StorageMount
+	}{
+		{
+			name:        "Empty TargetPath and mounts",
+			targetPath:  "",
+			in:          nil,
+			expectedOut: nil,
+		},
+		{
+			name:       "Empty TargetPath with Src and DstPath",
+			targetPath: "",
+			in: []runtimeutil.StorageMount{
+				{
+					MountType: "bind",
+					Src:       "src",
+					DstPath:   "dst",
+				},
+			},
+			expectedOut: []runtimeutil.StorageMount{
+				{
+					MountType: "bind",
+					Src:       "src",
+					DstPath:   "dst",
+				},
+			},
+		},
+		{
+			name:       "Not empty TargetPath with Src and DstPath",
+			targetPath: "target_path",
+			in: []runtimeutil.StorageMount{
+				{
+					MountType: "bind",
+					Src:       "src",
+					DstPath:   "dst",
+				},
+			},
+			expectedOut: []runtimeutil.StorageMount{
+				{
+					MountType: "bind",
+					Src:       "target_path/src",
+					DstPath:   "dst",
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			c := executors.ContainerExecutor{
+				ContConf: &v1alpha1.GenericContainer{
+					Spec: runtimeutil.FunctionSpec{
+						Container: runtimeutil.ContainerSpec{
+							StorageMounts: tt.in,
+						},
+					},
+				},
+				TargetPath: tt.targetPath,
+			}
+			c.SetMounts()
+			assert.Equal(t, c.RunFns.StorageMounts, tt.expectedOut)
+		})
+	}
+}