cluster/tools/kartongips: init

This forks bitnami/kubecfg into kartongips. The rationale is that we
want to implement hscloud-specific functionality that wouldn't really be
upstreamable into kubecfg (like secret support, mulit-cluster support).

We forked off from github.com/q3k/kubecfg at commit b6817a94492c561ed61a44eeea2d92dcf2e6b8c0.

Change-Id: If5ba513905e0a86f971576fe7061a471c1d8b398
diff --git a/cluster/tools/kartongips/pkg/kubecfg/diff_test.go b/cluster/tools/kartongips/pkg/kubecfg/diff_test.go
new file mode 100644
index 0000000..cb95123
--- /dev/null
+++ b/cluster/tools/kartongips/pkg/kubecfg/diff_test.go
@@ -0,0 +1,198 @@
+// Copyright 2017 The kubecfg authors
+//
+//
+//    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 kubecfg
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRemoveListFields(t *testing.T) {
+	for _, tc := range []struct {
+		config, live, expected []interface{}
+	}{
+		{
+			config:   []interface{}{"a"},
+			live:     []interface{}{"a"},
+			expected: []interface{}{"a"},
+		},
+
+		// Check that extra fields in config are not propagated.
+		{
+			config:   []interface{}{"a", "b"},
+			live:     []interface{}{"a"},
+			expected: []interface{}{"a"},
+		},
+
+		// Check that extra entries in live are propagated.
+		{
+			config:   []interface{}{"a"},
+			live:     []interface{}{"a", "b"},
+			expected: []interface{}{"a", "b"},
+		},
+	} {
+		require.EqualValues(t, tc.expected, removeListFields(tc.config, tc.live))
+	}
+}
+
+func TestRemoveMapFields(t *testing.T) {
+	for _, tc := range []struct {
+		config, live, expected map[string]interface{}
+	}{
+		{
+			config:   map[string]interface{}{"foo": "bar"},
+			live:     map[string]interface{}{"foo": "bar"},
+			expected: map[string]interface{}{"foo": "bar"},
+		},
+
+		{
+			config:   map[string]interface{}{"foo": "bar", "bar": "baz"},
+			live:     map[string]interface{}{"foo": "bar"},
+			expected: map[string]interface{}{"foo": "bar"},
+		},
+
+		{
+			config:   map[string]interface{}{"foo": "bar"},
+			live:     map[string]interface{}{"foo": "bar", "bar": "baz"},
+			expected: map[string]interface{}{"foo": "bar"},
+		},
+	} {
+		require.Equal(t, tc.expected, removeMapFields(tc.config, tc.live))
+	}
+}
+
+func TestRemoveFields(t *testing.T) {
+	emptyVal := map[string]interface{}{
+		"args":    map[string]interface{}{},
+		"volumes": []string{},
+		"stdin":   false,
+	}
+	for _, tc := range []struct {
+		config, live, expected interface{}
+	}{
+		// Check we can handle embedded structs.
+		{
+			config:   map[string]interface{}{"foo": "bar", "bar": "baz"},
+			live:     map[string]interface{}{"foo": "bar"},
+			expected: map[string]interface{}{"foo": "bar"},
+		},
+		// JSON unmarshalling can return int64 for numbers
+		// https://golang.org/pkg/encoding/json/#Number
+		{
+			config:   map[string]interface{}{"foo": (int64)(10)},
+			live:     map[string]interface{}{},
+			expected: map[string]interface{}{},
+		},
+
+		// Check we can handle embedded lists.
+		{
+			config:   []interface{}{"a", "b"},
+			live:     []interface{}{"a"},
+			expected: []interface{}{"a"},
+		},
+
+		// Check we can handle arbitrary types.
+		{
+			config:   "a",
+			live:     "b",
+			expected: "b",
+		},
+		// Check we can handle mismatched types.
+		{
+			config:   map[string]interface{}{"foo": "bar"},
+			live:     []interface{}{"foo", "bar"},
+			expected: []interface{}{"foo", "bar"},
+		},
+		{
+			config:   []interface{}{"foo", "bar"},
+			live:     map[string]interface{}{"foo": "bar"},
+			expected: map[string]interface{}{"foo": "bar"},
+		},
+		// Check we handle empty configs by copying them as if were live
+		// (API won't return them)
+		{
+			config:   emptyVal,
+			live:     map[string]interface{}{},
+			expected: emptyVal,
+		},
+
+		// Check we can handle combinations.
+		{
+			config: map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "Service",
+				"metadata": map[string]interface{}{
+					"name":      "foo",
+					"namespace": "default",
+				},
+				"spec": map[string]interface{}{
+					"selector": map[string]interface{}{
+						"name": "foo",
+					},
+					"ports": []interface{}{
+						map[string]interface{}{
+							"name": "http",
+							"port": 80,
+						},
+						map[string]interface{}{
+							"name": "https",
+							"port": 443,
+						},
+					},
+				},
+			},
+			live: map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "Service",
+				"metadata": map[string]interface{}{
+					"name": "foo",
+					// NB Namespace missing.
+				},
+				"spec": map[string]interface{}{
+					"selector": map[string]interface{}{
+						"bar": "foo",
+					},
+					"ports": []interface{}{
+						// NB HTTP port missing.
+						map[string]interface{}{
+							"name": "https",
+							"port": 443,
+						},
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"apiVersion": "v1",
+				"kind":       "Service",
+				"metadata": map[string]interface{}{
+					"name": "foo",
+				},
+				"spec": map[string]interface{}{
+					"selector": map[string]interface{}{},
+					"ports": []interface{}{
+						map[string]interface{}{
+							"name": "https",
+							"port": 443,
+						},
+					},
+				},
+			},
+		},
+	} {
+		require.Equal(t, tc.expected, removeFields(tc.config, tc.live))
+	}
+}