Skip to content
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

Merged
merged 6 commits into from
Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
174 changes: 174 additions & 0 deletions test/extended/apiserver/apply.go
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()))
Copy link
Contributor

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?

Copy link
Contributor Author

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!

})
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(
Copy link
Contributor

Choose a reason for hiding this comment

The 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?
is setting a FieldManager a good indicator?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this what it takes to do server-side apply?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the patch type especially.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, okay, it points to the client that made the update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 types.ApplyPatchType.

},
)
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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using a unique namespace - that's testNamespace. It only needs to be used when the resource itself is namespaced since many of the types are cluster-scoped. This minimizes the need to provision new namespaces since that is not cheap.

}

// 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
}
38 changes: 19 additions & 19 deletions test/extended/etcd/etcd_storage_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import (
)

// Etcd data for all persisted OpenShift objects.
var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageData{
// github.com/openshift/openshift-apiserver/pkg/authorization/apis/authorization/v1
var OpenshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageData{
// github.com/openshift/api/authorization/v1
gvr("authorization.openshift.io", "v1", "roles"): {
Stub: `{"metadata": {"name": "r1b1o2"}, "rules": [{"verbs": ["create"], "apiGroups": ["authorization.k8s.io"], "resources": ["selfsubjectaccessreviews"]}]}`,
ExpectedEtcdPath: "kubernetes.io/roles/etcdstoragepathtestnamespace/r1b1o2",
Expand All @@ -60,7 +60,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD
},
// --

// github.com/openshift/openshift-apiserver/pkg/build/apis/build/v1
// github.com/openshift/api/build/v1
gvr("build.openshift.io", "v1", "builds"): {
Stub: `{"metadata": {"name": "build1g"}, "spec": {"source": {"dockerfile": "Dockerfile1"}, "strategy": {"dockerStrategy": {"noCache": true}}}}`,
ExpectedEtcdPath: "openshift.io/builds/etcdstoragepathtestnamespace/build1g",
Expand All @@ -71,14 +71,14 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD
},
// --

// github.com/openshift/openshift-apiserver/pkg/apps/apis/apps/v1
// github.com/openshift/api/apps/v1
gvr("apps.openshift.io", "v1", "deploymentconfigs"): {
Stub: `{"metadata": {"name": "dc1g"}, "spec": {"selector": {"d": "c"}, "template": {"metadata": {"labels": {"d": "c"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container2"}]}}}}`,
ExpectedEtcdPath: "openshift.io/deploymentconfigs/etcdstoragepathtestnamespace/dc1g",
},
// --

// github.com/openshift/openshift-apiserver/pkg/image/apis/image/v1
// github.com/openshift/api/image/v1
gvr("image.openshift.io", "v1", "imagestreams"): {
Stub: `{"metadata": {"name": "is1g"}, "spec": {"dockerImageRepository": "docker"}}`,
ExpectedEtcdPath: "openshift.io/imagestreams/etcdstoragepathtestnamespace/is1g",
Expand All @@ -89,7 +89,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD
},
// --

// github.com/openshift/openshift-apiserver/pkg/oauth/apis/oauth/v1
// github.com/openshift/api/oauth/v1
gvr("oauth.openshift.io", "v1", "oauthclientauthorizations"): {
Stub: `{"clientName": "system:serviceaccount:etcdstoragepathtestnamespace:clientg", "metadata": {"name": "user:system:serviceaccount:etcdstoragepathtestnamespace:clientg"}, "scopes": ["user:info"], "userName": "user", "userUID": "cannot be empty"}`,
ExpectedEtcdPath: "openshift.io/oauth/clientauthorizations/user:system:serviceaccount:etcdstoragepathtestnamespace:clientg",
Expand Down Expand Up @@ -130,29 +130,29 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD
},
// --

// github.com/openshift/openshift-apiserver/pkg/project/apis/project/v1
// github.com/openshift/api/project/v1
gvr("project.openshift.io", "v1", "projects"): {
Stub: `{"metadata": {"name": "namespace2g"}, "spec": {"finalizers": ["kubernetes", "openshift.io/origin"]}}`,
ExpectedEtcdPath: "kubernetes.io/namespaces/namespace2g",
ExpectedGVK: gvkP("", "v1", "Namespace"), // project is a proxy for namespace
},
// --

// github.com/openshift/openshift-apiserver/pkg/route/apis/route/v1
// github.com/openshift/api/route/v1
gvr("route.openshift.io", "v1", "routes"): {
Stub: `{"metadata": {"name": "route1g"}, "spec": {"host": "hostname1", "to": {"name": "service1"}}}`,
ExpectedEtcdPath: "openshift.io/routes/etcdstoragepathtestnamespace/route1g",
},
// --

// github.com/openshift/openshift-apiserver/pkg/security/apis/security/v1
// github.com/openshift/api/security/v1
gvr("security.openshift.io", "v1", "rangeallocations"): {
Stub: `{"metadata": {"name": "scc2"}}`,
Stub: `{"metadata": {"name": "scc2"}, "range": "", "data": ""}`,
ExpectedEtcdPath: "openshift.io/rangeallocations/scc2",
},
// --

// github.com/openshift/openshift-apiserver/pkg/template/apis/template/v1
// github.com/openshift/api/template/v1
gvr("template.openshift.io", "v1", "templates"): {
Stub: `{"message": "Jenkins template", "metadata": {"name": "template1g"}}`,
ExpectedEtcdPath: "openshift.io/templates/etcdstoragepathtestnamespace/template1g",
Expand All @@ -167,7 +167,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD
},
// --

// github.com/openshift/openshift-apiserver/pkg/user/apis/user/v1
// github.com/openshift/api/user/v1
gvr("user.openshift.io", "v1", "groups"): {
Stub: `{"metadata": {"name": "groupg"}, "users": ["user1", "user2"]}`,
ExpectedEtcdPath: "openshift.io/groups/groupg",
Expand Down Expand Up @@ -197,7 +197,7 @@ var kindWhiteList = sets.NewString(
)

// namespace used for all tests, do not change this
const testNamespace = "etcdstoragepathtestnamespace"
const TestNamespace = "etcdstoragepathtestnamespace"

type helperT struct {
g.GinkgoTInterface
Expand Down Expand Up @@ -259,16 +259,16 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e

client := &allClient{dynamicClient: dynamic.NewForConfigOrDie(kubeConfig)}

if _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), &kapiv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
if _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), &kapiv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: TestNamespace}}, metav1.CreateOptions{}); err != nil {
t.Fatalf("error creating test namespace: %#v", err)
}
defer func() {
if err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), testNamespace, metav1.DeleteOptions{}); err != nil {
if err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), TestNamespace, metav1.DeleteOptions{}); err != nil {
t.Fatalf("error deleting test namespace: %#v", err)
}
}()

if err := exutil.WaitForServiceAccount(kubeClient.CoreV1().ServiceAccounts(testNamespace), "default"); err != nil {
if err := exutil.WaitForServiceAccount(kubeClient.CoreV1().ServiceAccounts(TestNamespace), "default"); err != nil {
t.Fatalf("error waiting for the default service account: %v", err)
}

Expand Down Expand Up @@ -346,7 +346,7 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e
}

// add openshift specific data
for gvr, data := range openshiftEtcdStorageData {
for gvr, data := range OpenshiftEtcdStorageData {
if _, ok := etcdStorageData[gvr]; ok {
t.Errorf("%s exists in both Kube and OpenShift ETCD data, data=%#v", gvr.String(), data)
}
Expand Down Expand Up @@ -424,13 +424,13 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e
}
}()

if err := client.createPrerequisites(mapper, testNamespace, testData.Prerequisites, all); err != nil {
if err := client.createPrerequisites(mapper, TestNamespace, testData.Prerequisites, all); err != nil {
t.Errorf("failed to create prerequisites for %v: %#v", gvk, err)
return
}

if shouldCreate { // do not try to create items with no stub
if err := client.create(testData.Stub, testNamespace, mapping, all); err != nil {
if err := client.create(testData.Stub, TestNamespace, mapping, all); err != nil {
t.Errorf("failed to create stub for %v: %#v", gvk, err)
return
}
Expand Down
34 changes: 34 additions & 0 deletions test/extended/util/annotate/generated/zz_generated.annotations.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions test/extended/util/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func NewCLI(project string) *CLI {
cli := NewCLIWithoutNamespace(project)
cli.withoutNamespace = false
// create our own project
g.BeforeEach(cli.SetupProject)
g.BeforeEach(func() { _ = cli.SetupProject() })
return cli
}

Expand Down Expand Up @@ -210,7 +210,8 @@ func (c CLI) WithToken(token string) *CLI {

// SetupProject creates a new project and assign a random user to the project.
// All resources will be then created within this project.
func (c *CLI) SetupProject() {
// Returns the name of the new project.
func (c *CLI) SetupProject() string {
requiresTestStart()
newNamespace := names.SimpleNameGenerator.GenerateName(fmt.Sprintf("e2e-test-%s-", c.kubeFramework.BaseName))
c.SetNamespace(newNamespace).ChangeUser(fmt.Sprintf("%s-user", newNamespace))
Expand Down Expand Up @@ -291,6 +292,7 @@ func (c *CLI) SetupProject() {
WaitForNamespaceSCCAnnotations(c.ProjectClient().ProjectV1(), newNamespace)

framework.Logf("Project %q has been fully provisioned.", newNamespace)
return newNamespace
}

func (c *CLI) setupSelfProvisionerRoleBinding() error {
Expand Down