-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add e2e testing of server-side apply for openshift types #25652
Changes from all commits
7c28ec0
016665d
be617ad
e022742
bbb743f
e933075
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package apiserver | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"strings" | ||
|
||
g "github.com/onsi/ginkgo" | ||
o "github.com/onsi/gomega" | ||
|
||
"k8s.io/apimachinery/pkg/api/meta" | ||
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/types" | ||
discocache "k8s.io/client-go/discovery/cached" | ||
"k8s.io/client-go/dynamic" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/restmapper" | ||
"k8s.io/kubernetes/test/e2e/framework" | ||
|
||
exetcd "github.com/openshift/origin/test/extended/etcd" | ||
exutil "github.com/openshift/origin/test/extended/util" | ||
) | ||
|
||
var _ = g.Describe("[sig-api-machinery][Feature:ServerSideApply] Server-Side Apply", func() { | ||
var ( | ||
// mapper is used to translate the gvr provided by etcd | ||
// storage data to the gvk required to create correct resource | ||
// yaml. | ||
mapper *restmapper.DeferredDiscoveryRESTMapper | ||
) | ||
|
||
defer g.GinkgoRecover() | ||
|
||
// Defer project creation to tests that require it by calling | ||
// NewCLIWithoutNamespace instead of NewCLI. | ||
oc := exutil.NewCLIWithoutNamespace("server-side-apply") | ||
|
||
g.BeforeEach(func() { | ||
// Only init once per worker | ||
if mapper != nil { | ||
return | ||
} | ||
kubeClient, err := kubernetes.NewForConfig(oc.AdminConfig()) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
mapper = restmapper.NewDeferredDiscoveryRESTMapper(discocache.NewMemCacheClient(kubeClient.Discovery())) | ||
}) | ||
storageData := exetcd.OpenshiftEtcdStorageData | ||
for key := range storageData { | ||
gvr := key | ||
data := storageData[gvr] | ||
|
||
// Apply for core types is already well-tested, so skip | ||
// openshift types that are just aliases. | ||
aliasToCoreType := data.ExpectedGVK != nil | ||
if aliasToCoreType { | ||
continue | ||
} | ||
|
||
g.It(fmt.Sprintf("should work for %s", gvr), func() { | ||
// Create at most one namespace only if needed. | ||
var testNamespace string | ||
|
||
for _, prerequisite := range data.Prerequisites { | ||
// The etcd storage test for oauthclientauthorizations needs to | ||
// manually create a service account secret but that is not | ||
// necessary (or possible) when interacting with an apiserver. | ||
// The service account secret will be created by the controller | ||
// manager. | ||
if gvr.Resource == "oauthclientauthorizations" && prerequisite.GvrData.Resource == "secrets" { | ||
continue | ||
} | ||
resourceClient, unstructuredObj, namespace := createResource(oc, mapper, prerequisite.GvrData, prerequisite.Stub, testNamespace) | ||
testNamespace = namespace | ||
defer deleteResource(resourceClient, unstructuredObj.GetName()) | ||
} | ||
|
||
resourceClient, unstructuredObj, namespace := createResource(oc, mapper, gvr, data.Stub, testNamespace) | ||
testNamespace = namespace | ||
defer deleteResource(resourceClient, unstructuredObj.GetName()) | ||
|
||
serializedObj, err := json.Marshal(unstructuredObj.Object) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
|
||
g.By(fmt.Sprintf("updating the %s via apply", unstructuredObj.GetKind())) | ||
obj, err := resourceClient.Patch( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't have to actually change the object to test the server-side apply logic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A good first step. It won't tell us that our schema is correct. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The server-side logic has reasonable integration tests in upstream. The goal of this test is validating that openshift types are compatible with server-side apply vs validating server-side apply itself. |
||
context.Background(), | ||
unstructuredObj.GetName(), | ||
types.ApplyPatchType, | ||
serializedObj, | ||
metav1.PatchOptions{ | ||
FieldManager: "apply_test", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this what it takes to do server-side apply? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the patch type especially. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh, okay, it points to the client that made the update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This just points to who made an update, and it can be supplied with CREATE, UPDATE or PATCH. The key to performing server-side apply is |
||
}, | ||
) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
|
||
g.By(fmt.Sprintf("checking that the field managers are as expected")) | ||
accessor, err := meta.Accessor(obj) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
managedFields := accessor.GetManagedFields() | ||
o.Expect(managedFields).NotTo(o.BeNil()) | ||
if !findManager(managedFields, "create_test") { | ||
g.Fail(fmt.Sprintf("Couldn't find create_test: %v", managedFields)) | ||
} | ||
if !findManager(managedFields, "apply_test") { | ||
g.Fail(fmt.Sprintf("Couldn't find apply_test: %v", managedFields)) | ||
} | ||
}) | ||
} | ||
}) | ||
|
||
func findManager(managedFields []metav1.ManagedFieldsEntry, manager string) bool { | ||
for _, entry := range managedFields { | ||
if entry.Manager == manager { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func deleteResource(resourceClient dynamic.ResourceInterface, name string) { | ||
err := resourceClient.Delete(context.Background(), name, metav1.DeleteOptions{}) | ||
if err != nil { | ||
framework.Logf("Unexpected error deleting resource: %v", err) | ||
} | ||
} | ||
|
||
func createResource( | ||
oc *exutil.CLI, | ||
mapper *restmapper.DeferredDiscoveryRESTMapper, | ||
gvr schema.GroupVersionResource, | ||
stub, testNamespace string) ( | ||
dynamic.ResourceInterface, *unstructured.Unstructured, string) { | ||
|
||
// Discover the gvk from the gvr | ||
gvk, err := mapper.KindFor(gvr) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
|
||
// Supply a value for namespace if the scope requires | ||
mapping, err := mapper.RESTMapping(gvk.GroupKind()) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
namespace := "" | ||
if mapping.Scope.Name() == meta.RESTScopeNameNamespace && len(testNamespace) == 0 { | ||
// Create the namespace on demand | ||
namespace = oc.SetupProject() | ||
testNamespace = namespace | ||
} | ||
|
||
// Ensure that any stub embedding the etcd test namespace | ||
// is updated to use local test namespace instead. | ||
if len(testNamespace) > 0 { | ||
stub = strings.Replace(stub, exetcd.TestNamespace, testNamespace, -1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a need to use the etcd storage test namespace? Would rather use a unique namespace here to avoid conflicts, and to reduce shared logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am using a unique namespace - that's |
||
} | ||
|
||
// Create unstructured object from stub | ||
unstructuredObj := unstructured.Unstructured{} | ||
err = json.Unmarshal([]byte(stub), &unstructuredObj.Object) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
unstructuredObj.SetGroupVersionKind(gvk) | ||
|
||
dynamicClient, err := dynamic.NewForConfig(oc.AdminConfig()) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
resourceClient := dynamicClient.Resource(gvr).Namespace(namespace) | ||
|
||
g.By(fmt.Sprintf("creating a %s", gvk.Kind)) | ||
_, err = resourceClient.Create(context.Background(), &unstructuredObj, metav1.CreateOptions{ | ||
FieldManager: "create_test", | ||
}) | ||
o.Expect(err).NotTo(o.HaveOccurred()) | ||
|
||
return resourceClient, &unstructuredObj, testNamespace | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why a new one for each subtest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the conditional check above. It's initialized once per worker, not once per subtest. My ginkgo-foo is weak, if there's a better way to accomplish this I'm all ears!