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/update_test.go b/cluster/tools/kartongips/pkg/kubecfg/update_test.go
new file mode 100644
index 0000000..47c8389
--- /dev/null
+++ b/cluster/tools/kartongips/pkg/kubecfg/update_test.go
@@ -0,0 +1,287 @@
+package kubecfg
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ pb_proto "github.com/golang/protobuf/proto"
+ openapi_v2 "github.com/googleapis/gnostic/openapiv2"
+ apiequality "k8s.io/apimachinery/pkg/api/equality"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/util/diff"
+ "k8s.io/apimachinery/pkg/util/strategicpatch"
+ "k8s.io/kube-openapi/pkg/util/proto"
+ "k8s.io/kubectl/pkg/util/openapi"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
+)
+
+func TestStringListContains(t *testing.T) {
+ t.Parallel()
+ foobar := []string{"foo", "bar"}
+ if stringListContains([]string{}, "") {
+ t.Error("Empty list was not empty")
+ }
+ if !stringListContains(foobar, "foo") {
+ t.Error("Failed to find foo")
+ }
+ if stringListContains(foobar, "baz") {
+ t.Error("Should not contain baz")
+ }
+}
+
+func TestIsValidKindSchema(t *testing.T) {
+ t.Parallel()
+ schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
+
+ cmgvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}
+ if !isValidKindSchema(schemaResources.LookupResource(cmgvk)) {
+ t.Errorf("%s should have a valid schema", cmgvk)
+ }
+
+ if isValidKindSchema(nil) {
+ t.Error("nil should not be a valid schema")
+ }
+
+ // This is what a schema-less CRD appears as in k8s >= 1.15
+ mapSchema := &proto.Map{
+ BaseSchema: proto.BaseSchema{
+ Extensions: map[string]interface{}{
+ "x-kubernetes-group-version-kind": []interface{}{
+ map[interface{}]interface{}{"group": "bitnami.com", "kind": "SealedSecret", "version": "v1alpha1"},
+ },
+ },
+ },
+ SubType: &proto.Arbitrary{},
+ }
+ if isValidKindSchema(mapSchema) {
+ t.Error("Trivial type:object schema should be invalid")
+ }
+}
+
+func TestEligibleForGc(t *testing.T) {
+ t.Parallel()
+ const myTag = "my-gctag"
+ boolTrue := true
+ o := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "tests/v1alpha1",
+ "kind": "Dummy",
+ },
+ }
+
+ if eligibleForGc(o, myTag) {
+ t.Errorf("%v should not be eligible (no tag)", o)
+ }
+
+ // [gctag-migration]: Remove annotation in phase2
+ utils.SetMetaDataAnnotation(o, AnnotationGcTag, "unknowntag")
+ utils.SetMetaDataLabel(o, LabelGcTag, "unknowntag")
+ if eligibleForGc(o, myTag) {
+ t.Errorf("%v should not be eligible (wrong tag)", o)
+ }
+
+ // [gctag-migration]: Remove annotation in phase2
+ utils.SetMetaDataAnnotation(o, AnnotationGcTag, myTag)
+ utils.SetMetaDataLabel(o, LabelGcTag, myTag)
+ if !eligibleForGc(o, myTag) {
+ t.Errorf("%v should be eligible", o)
+ }
+
+ // [gctag-migration]: Remove testcase in phase2
+ utils.SetMetaDataAnnotation(o, AnnotationGcTag, myTag)
+ utils.DeleteMetaDataLabel(o, LabelGcTag) // no label. ie: pre-migration
+ if !eligibleForGc(o, myTag) {
+ t.Errorf("%v should be eligible (gctag-migration phase1)", o)
+ }
+
+ utils.SetMetaDataAnnotation(o, AnnotationGcStrategy, GcStrategyIgnore)
+ if eligibleForGc(o, myTag) {
+ t.Errorf("%v should not be eligible (strategy=ignore)", o)
+ }
+
+ utils.SetMetaDataAnnotation(o, AnnotationGcStrategy, GcStrategyAuto)
+ if !eligibleForGc(o, myTag) {
+ t.Errorf("%v should be eligible (strategy=auto)", o)
+ }
+
+ // Unstructured.SetOwnerReferences is broken in apimachinery release-1.6
+ // See kubernetes/kubernetes#46817
+ setOwnerRef := func(u *unstructured.Unstructured, ref metav1.OwnerReference) {
+ // This is not a complete nor robust reimplementation
+ c := map[string]interface{}{
+ "kind": ref.Kind,
+ "name": ref.Name,
+ }
+ if ref.Controller != nil {
+ c["controller"] = *ref.Controller
+ }
+ u.Object["metadata"].(map[string]interface{})["ownerReferences"] = []interface{}{c}
+ }
+ setOwnerRef(o, metav1.OwnerReference{Kind: "foo", Name: "bar"})
+ if !eligibleForGc(o, myTag) {
+ t.Errorf("%v should be eligible (non-controller ownerref)", o)
+ }
+
+ setOwnerRef(o, metav1.OwnerReference{Kind: "foo", Name: "bar", Controller: &boolTrue})
+ if eligibleForGc(o, myTag) {
+ t.Errorf("%v should not be eligible (controller ownerref)", o)
+ }
+}
+
+func exampleConfigMap() *unstructured.Unstructured {
+ result := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "ConfigMap",
+ "metadata": map[string]interface{}{
+ "name": "myname",
+ "namespace": "mynamespace",
+ "annotations": map[string]interface{}{
+ "myannotation": "somevalue",
+ },
+ },
+ "data": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ }
+
+ return result
+}
+
+func addOrigAnnotation(obj *unstructured.Unstructured) {
+ data, err := utils.CompactEncodeObject(obj)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to serialise object: %v", err))
+ }
+ utils.SetMetaDataAnnotation(obj, AnnotationOrigObject, data)
+}
+
+func newPatchMetaFromStructOrDie(dataStruct interface{}) strategicpatch.PatchMetaFromStruct {
+ t, err := strategicpatch.NewPatchMetaFromStruct(dataStruct)
+ if err != nil {
+ panic(fmt.Sprintf("NewPatchMetaFromStruct(%t) failed: %v", dataStruct, err))
+ }
+ return t
+}
+
+func readSchemaOrDie(path string) openapi.Resources {
+ var doc openapi_v2.Document
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to read %s: %v", path, err))
+ }
+ if err := pb_proto.Unmarshal(b, &doc); err != nil {
+ panic(fmt.Sprintf("Unable to unmarshal %s: %v", path, err))
+ }
+ schemaResources, err := openapi.NewOpenAPIData(&doc)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to parse openapi doc: %v", err))
+ }
+ return schemaResources
+}
+
+func TestPatchNoop(t *testing.T) {
+ t.Parallel()
+ schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
+
+ existing := exampleConfigMap()
+ new := existing.DeepCopy()
+ addOrigAnnotation(existing)
+
+ result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
+ if err != nil {
+ t.Errorf("patch() returned error: %v", err)
+ }
+
+ t.Logf("existing: %#v", existing)
+ t.Logf("result: %#v", result)
+ if !apiequality.Semantic.DeepEqual(existing, result) {
+ t.Error("Objects differed: ", diff.ObjectDiff(existing, result))
+ }
+}
+
+func TestPatchNoopNoAnnotation(t *testing.T) {
+ t.Parallel()
+ schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
+
+ existing := exampleConfigMap()
+ new := existing.DeepCopy()
+ // Note: no addOrigAnnotation(existing)
+
+ result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
+ if err != nil {
+ t.Errorf("patch() returned error: %v", err)
+ }
+
+ // result should == existing, except for annotation
+
+ if result.GetAnnotations()[AnnotationOrigObject] == "" {
+ t.Errorf("result lacks last-applied annotation")
+ }
+
+ utils.DeleteMetaDataAnnotation(result, AnnotationOrigObject)
+ if !apiequality.Semantic.DeepEqual(existing, result) {
+ t.Error("Objects differed: ", diff.ObjectDiff(existing, result))
+ }
+}
+
+func TestPatchNoConflict(t *testing.T) {
+ t.Parallel()
+ schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
+
+ existing := exampleConfigMap()
+ utils.SetMetaDataAnnotation(existing, "someanno", "origvalue")
+ addOrigAnnotation(existing)
+ utils.SetMetaDataAnnotation(existing, "otheranno", "existingvalue")
+ new := exampleConfigMap()
+ utils.SetMetaDataAnnotation(new, "someanno", "newvalue")
+
+ result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
+ if err != nil {
+ t.Errorf("patch() returned error: %v", err)
+ }
+
+ t.Logf("existing: %#v", existing)
+ t.Logf("result: %#v", result)
+ someanno := result.GetAnnotations()["someanno"]
+ if someanno != "newvalue" {
+ t.Errorf("someanno was %q", someanno)
+ }
+
+ otheranno := result.GetAnnotations()["otheranno"]
+ if otheranno != "existingvalue" {
+ t.Errorf("otheranno was %q", otheranno)
+ }
+}
+
+func TestPatchConflict(t *testing.T) {
+ t.Parallel()
+ schemaResources := readSchemaOrDie(filepath.FromSlash("../../testdata/schema.pb"))
+
+ existing := exampleConfigMap()
+ utils.SetMetaDataAnnotation(existing, "someanno", "origvalue")
+ addOrigAnnotation(existing)
+ utils.SetMetaDataAnnotation(existing, "someanno", "existingvalue")
+ new := exampleConfigMap()
+ utils.SetMetaDataAnnotation(new, "someanno", "newvalue")
+
+ result, err := patch(existing, new, schemaResources.LookupResource(existing.GroupVersionKind()))
+ if err != nil {
+ t.Errorf("patch() returned error: %v", err)
+ }
+
+ // `new` should win conflicts
+
+ t.Logf("existing: %#v", existing)
+ t.Logf("result: %#v", result)
+ value := result.GetAnnotations()["someanno"]
+ if value != "newvalue" {
+ t.Errorf("annotation was %q", value)
+ }
+}