Repository: kptdev/kpt

Issue:
Pipeline docker runtime should support windows
PR #1355 only considers *nix platform. Windows should be supported as well.

Discussion:
@CsatariGergely: Not a priority at the moment. 

Solution patch:
diff --git a/Makefile b/Makefile
index 0143f2dd..b4256c7f 100644
--- a/Makefile
+++ b/Makefile
@@ -69,6 +69,9 @@ license-check:
 test:
 	go test -cover ./...
 
+test-docker:
+	go test -cover --tags=docker ./...
+
 vet:
 	go vet ./...
 
diff --git a/internal/pipeline/executor.go b/internal/pipeline/executor.go
index 38702db0..7ff0de95 100644
--- a/internal/pipeline/executor.go
+++ b/internal/pipeline/executor.go
@@ -17,6 +17,7 @@ package pipeline
 import (
 	"fmt"
 	"path/filepath"
+	"strings"
 
 	"k8s.io/klog"
 	"sigs.k8s.io/kustomize/kyaml/kio"
@@ -267,9 +268,17 @@ func hydrate(p *pkg, hctx *hydrationContext) (resources []*yaml.RNode, err error
 	return resources, err
 }
 
-// TODO(droot): filter kpt metadata files such as pipeline, Kptfile, permissions etc.
+// filterMetaData filters kpt metadata files such as pipeline,
+// Kptfile, permissions etc.
 func filterMetaData(resources []*yaml.RNode) []*yaml.RNode {
-	return resources
+	var filtered []*yaml.RNode
+	for _, r := range resources {
+		meta, _ := r.GetMeta()
+		if !strings.Contains(meta.APIVersion, "kpt.dev") {
+			filtered = append(filtered, r)
+		}
+	}
+	return filtered
 }
 
 // fnFilters returns chain of functions that are applicable
diff --git a/internal/pipeline/runtime/container.go b/internal/pipeline/runtime/container.go
new file mode 100644
index 00000000..08ce1bae
--- /dev/null
+++ b/internal/pipeline/runtime/container.go
@@ -0,0 +1,111 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runtime
+
+// some of these codes are copied from
+// - https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/fn/runtime/container/container.go
+// - https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/fn/runtime/exec/exec.go
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"os/user"
+	"time"
+)
+
+// containerNetworkName is a type for network name used in container
+type containerNetworkName string
+
+const (
+	networkNameNone containerNetworkName = "none"
+	networkNameHost containerNetworkName = "host"
+	defaultTimeout  time.Duration        = 5 * time.Minute
+)
+
+// ContainerFnPermission contains the permission of container
+// function such as network access.
+type ContainerFnPermission struct {
+	AllowNetwork  bool
+	AsCurrentUser bool
+}
+
+// ContainerFn implements a KRMFn which run a containerized
+// KRM function
+type ContainerFn struct {
+	// Image is the container image to run
+	Image string
+	// Container function will be killed after this timeour.
+	// The default value is 5 minutes.
+	Timeout time.Duration
+	Perm    ContainerFnPermission
+}
+
+// Run implements KRMFn. It will run the container function with
+// stdin from r and write the output to w
+func (f *ContainerFn) Run(r io.Reader, w io.Writer) error {
+	// run the container using docker.  this is simpler than using the docker
+	// libraries, and ensures things like auth work the same as if the container
+	// was run from the cli.
+	path := "docker"
+
+	network := networkNameNone
+	if f.Perm.AllowNetwork {
+		network = networkNameHost
+	}
+	UIDGID, err := getUIDGID(f.Perm.AsCurrentUser)
+	if err != nil {
+		return fmt.Errorf("failed to get current UID and GID: %w", err)
+	}
+	args := []string{"run",
+		"--rm",                                              // delete the container afterward
+		"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", // attach stdin, stdout, stderr
+		"--network", string(network),
+		"--user", UIDGID,
+		"--security-opt=no-new-privileges", // don't allow the user to escalate privileges
+		// note: don't make fs readonly because things like heredoc rely on writing tmp files
+	}
+	args = append(args, f.Image)
+
+	timeout := defaultTimeout
+	if f.Timeout != 0 {
+		timeout = f.Timeout
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, path, args...)
+	cmd.Stdin = r
+	cmd.Stdout = w
+	cmd.Stderr = os.Stderr
+	return cmd.Run()
+}
+
+// getUIDGID will return "nobody" if asCurrentUser is false. Otherwise
+// return "uid:gid" according to the return from `user.Current()` function.
+func getUIDGID(asCurrentUser bool) (string, error) {
+	if !asCurrentUser {
+		return "nobody", nil
+	}
+
+	u, err := user.Current()
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%s:%s", u.Uid, u.Gid), nil
+}
+
+// TODO: Create ContainerFn instance from a pipeline.Function



Test patch:
diff --git a/internal/pipeline/executor_test.go b/internal/pipeline/executor_test.go
new file mode 100644
index 00000000..78fa3482
--- /dev/null
+++ b/internal/pipeline/executor_test.go
@@ -0,0 +1,130 @@
+package pipeline
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+func TestFilterMetaData(t *testing.T) {
+	tests := map[string]struct {
+		resources    []string
+		expected     []string
+	}{
+		"no resources": {
+			resources: nil,
+			expected: nil,
+		},
+
+		"nothing to filter": {
+			resources: []string{`
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3 # {"$kpt-set":"replicas"}`,
+				`
+apiVersion: custom.io/v1
+kind: Custom
+spec:
+  image: nginx:1.2.3`,
+			},
+			expected: []string{
+				`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3 # {"$kpt-set":"replicas"}
+`,
+				`apiVersion: custom.io/v1
+kind: Custom
+spec:
+  image: nginx:1.2.3
+`,
+			},
+		},
+
+		"filter out metadata": {
+			resources: []string{`
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3 # {"$kpt-set":"replicas"}`,
+				`
+apiVersion: config.kpt.dev/v1
+Kind: FunctionPermission
+Metadata:
+  Name: functionPermission
+Spec:
+  Allow:
+  - imageName: gcr.io/my-project/*…..
+  Permissions:
+  - network
+  - mount
+  Disallow:
+  - Name: gcr.io/my-project/*`,
+				`
+apiVersion: custom.io/v1
+kind: Custom
+spec:
+  image: nginx:1.2.3`,
+				`
+apiVersion: kpt.dev/v1alpha1
+kind: Kptfile
+metadata:
+  name: mysql
+setterDefinitions:
+  replicas:
+    description: "replica setter"
+    type: integer
+setterValues:
+  replicas: 5`,
+				`
+apiVersion: kpt.dev/v1alpha1
+kind: Pipeline
+sources:
+  - "."`,
+			},
+			expected: []string{
+				`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3 # {"$kpt-set":"replicas"}
+`,
+				`apiVersion: custom.io/v1
+kind: Custom
+spec:
+  image: nginx:1.2.3
+`,
+			},
+		},
+	}
+
+	for _, test := range tests {
+		var nodes []*yaml.RNode
+
+		for _, r := range test.resources {
+			res, err := yaml.Parse(r)
+			assert.NoError(t, err)
+			nodes = append(nodes, res)
+		}
+
+		filteredRes := filterMetaData(nodes)
+		if len(filteredRes) != len(test.expected) {
+			t.Fatal("length of filtered resources not equal to expected")
+		}
+
+		for i, r := range filteredRes {
+			res, err := r.String()
+			assert.NoError(t, err)
+			assert.Equal(t, test.expected[i], res)
+		}
+	}
+}
diff --git a/internal/pipeline/runtime/container_test.go b/internal/pipeline/runtime/container_test.go
new file mode 100644
index 00000000..cb20854d
--- /dev/null
+++ b/internal/pipeline/runtime/container_test.go
@@ -0,0 +1,65 @@
+// +build docker
+
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runtime_test
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/GoogleContainerTools/kpt/internal/pipeline/runtime"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestContainerFn(t *testing.T) {
+	var tests = []struct {
+		input  string
+		image  string
+		output string
+		name   string
+		err    bool
+	}{
+		{
+			name:  "simple busybox",
+			image: "gcr.io/google-containers/busybox",
+		},
+		{
+			name:  "non-existing image",
+			image: "foobar",
+			err:   true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			instance := runtime.ContainerFn{}
+			instance.Image = tt.image
+			input := bytes.NewBufferString(tt.input)
+			output := &bytes.Buffer{}
+			err := instance.Run(input, output)
+			if tt.err && !assert.Error(t, err) {
+				t.FailNow()
+			}
+			if !tt.err && !assert.NoError(t, err) {
+				t.FailNow()
+			}
+			if !assert.Equal(t, tt.output, output.String()) {
+				t.FailNow()
+			}
+		})
+	}
+}