package kubecfg import ( "fmt" "io/ioutil" "path/filepath" "testing" pb_proto "github.com/golang/protobuf/proto" openapi_v2 "github.com/google/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.Skip("Skip test broken by kartongips fork.") 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.Skip("Skip test broken by kartongips fork.") 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.Skip("Skip test broken by kartongips fork.") 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.Skip("Skip test broken by kartongips fork.") 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.Skip("Skip test broken by kartongips fork.") 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) } }