E2E testing blog post (#13280)
* E2E testing blog post * E2E testing blog post: add information about optional command line flags * Update and rename 2019-03-30-e2e-testing-for-everyone.md to 2019-03-22-e2e-testing-for-everyone.md
This commit is contained in:
committed by
Kubernetes Prow Robot
parent
f9166a844c
commit
bb7abf59cd
@@ -0,0 +1,366 @@
|
|||||||
|
---
|
||||||
|
title: Kubernetes End-to-end Testing for Everyone
|
||||||
|
date: 2019-03-22
|
||||||
|
---
|
||||||
|
|
||||||
|
**Author:** Patrick Ohly (Intel)
|
||||||
|
|
||||||
|
More and more components that used to be part of Kubernetes are now
|
||||||
|
being developed outside of Kubernetes. For example, storage drivers
|
||||||
|
used to be compiled into Kubernetes binaries, then were moved into
|
||||||
|
[stand-alone Flexvolume
|
||||||
|
binaries](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-storage/flexvolume.md)
|
||||||
|
on the host, and now are delivered as [Container Storage Interface
|
||||||
|
(CSI) drivers](https://github.com/container-storage-interface/spec)
|
||||||
|
that get deployed in pods inside the Kubernetes cluster itself.
|
||||||
|
|
||||||
|
This poses a challenge for developers who work on such components: how
|
||||||
|
can end-to-end (E2E) testing on a Kubernetes cluster be done for such
|
||||||
|
external components? The E2E framework that is used for testing
|
||||||
|
Kubernetes itself has all the necessary functionality. However, trying
|
||||||
|
to use it outside of Kubernetes was difficult and only possible by
|
||||||
|
carefully selecting the right versions of a large number of
|
||||||
|
dependencies. E2E testing has become a lot simpler in Kubernetes 1.13.
|
||||||
|
|
||||||
|
This blog post summarizes the changes that went into Kubernetes
|
||||||
|
1.13. For CSI driver developers, it will cover the ongoing effort to
|
||||||
|
also make the storage tests available for testing of third-party CSI
|
||||||
|
drivers. How to use them will be shown based on two Intel CSI drivers:
|
||||||
|
|
||||||
|
* [Open Infrastructure Manager (OIM)](https://github.com/intel/oim/)
|
||||||
|
* [PMEM-CSI](https://github.com/intel/pmem-csi)
|
||||||
|
|
||||||
|
Testing those drivers was the main motivation behind most of these
|
||||||
|
enhancements.
|
||||||
|
|
||||||
|
## E2E overview
|
||||||
|
|
||||||
|
E2E testing consists of several phases:
|
||||||
|
|
||||||
|
* Implementing a test suite. This is the main focus of this blog
|
||||||
|
post. The Kubernetes E2E framework is written in Go. It relies on
|
||||||
|
[Ginkgo](https://onsi.github.io/ginkgo/) for managing tests and
|
||||||
|
[Gomega](http://onsi.github.io/gomega/) for assertions. These tools
|
||||||
|
support “behavior driven development”, which describes expected
|
||||||
|
behavior in “specs”. In this blog post, “test” is used to reference
|
||||||
|
an individual `Ginkgo.It` spec. Tests interact with the Kubernetes
|
||||||
|
cluster using
|
||||||
|
[client-go](https://godoc.org/k8s.io/client-go/kubernetes).
|
||||||
|
* Bringing up a test cluster. Tools like
|
||||||
|
[kubetest](https://github.com/kubernetes/test-infra/blob/master/kubetest/README.md)
|
||||||
|
can help here.
|
||||||
|
* Running an E2E test suite against that cluster. Ginkgo test suites
|
||||||
|
can be run with the `ginkgo` tool or as a normal Go test with `go
|
||||||
|
test`. Without any parameters, a Kubernetes E2E test suite will
|
||||||
|
connect to the default cluster based on environment variables like
|
||||||
|
KUBECONFIG, exactly like kubectl. Kubetest also knows how to run the
|
||||||
|
Kubernetes E2E suite.
|
||||||
|
|
||||||
|
## E2E framework enhancements in Kubernetes 1.13
|
||||||
|
|
||||||
|
All of the following enhancements follow the same basic pattern: they
|
||||||
|
make the E2E framework more useful and easier to use outside of
|
||||||
|
Kubernetes, without changing the behavior of the original Kubernetes
|
||||||
|
e2e.test binary.
|
||||||
|
|
||||||
|
### Splitting out provider support
|
||||||
|
|
||||||
|
The main reason why using the E2E framework from Kubernetes <= 1.12
|
||||||
|
was difficult were the dependencies on provider-specific SDKs, which
|
||||||
|
pulled in a large number of packages. Just getting it compiled was
|
||||||
|
non-trivial.
|
||||||
|
|
||||||
|
Many of these packages are only needed for certain tests. For example,
|
||||||
|
testing the mounting of a pre-provisioned volume must first provision
|
||||||
|
such a volume the same way as an administrator would, by talking
|
||||||
|
directly to a specific storage backend via some non-Kubernetes API.
|
||||||
|
|
||||||
|
There is an effort to [remove cloud provider-specific
|
||||||
|
tests](https://github.com/kubernetes/kubernetes/issues/70194) from
|
||||||
|
core Kubernetes. The approach taken in [PR
|
||||||
|
#68483](https://github.com/kubernetes/kubernetes/pull/68483) can be
|
||||||
|
seen as an incremental step towards that goal: instead of ripping out
|
||||||
|
the code immediately and breaking all tests that depend on it, all
|
||||||
|
cloud provider-specific code was moved into optional packages under
|
||||||
|
[test/e2e/framework/providers](https://github.com/kubernetes/kubernetes/tree/release-1.13/test/e2e/framework/providers). The
|
||||||
|
E2E framework then accesses it via [an
|
||||||
|
interface](https://github.com/kubernetes/kubernetes/blob/6c1e64b94a3e111199c934c39a0c25bc219ed5f9/test/e2e/framework/provider.go#L79-L99)
|
||||||
|
that gets implemented separately by each vendor package.
|
||||||
|
|
||||||
|
The author of a E2E test suite decides which of these packages get
|
||||||
|
imported into the test suite. The vendor support is then activated via
|
||||||
|
the `--provider` command line flag. The Kubernetes e2e.test binary in
|
||||||
|
1.13 and 1.14 still contains support for the same providers as in
|
||||||
|
1.12. It is also okay to include no packages, which means that only
|
||||||
|
the generic providers will be available:
|
||||||
|
|
||||||
|
* “skeleton”: cluster is accessed via the Kubernetes API and nothing
|
||||||
|
else
|
||||||
|
* “local”: like “skeleton”, but in addition the scripts in
|
||||||
|
kubernetes/kubernetes/cluster can retrieve logs via ssh after a test
|
||||||
|
suite is run
|
||||||
|
|
||||||
|
### External files
|
||||||
|
|
||||||
|
Tests may have to read additional files at runtime, like .yaml
|
||||||
|
manifests. But the Kubernetes e2e.test binary is supposed to be usable
|
||||||
|
and entirely stand-alone because that simplifies shipping and running
|
||||||
|
it. The solution in the Kubernetes build system is to link all files
|
||||||
|
under `test/e2e/testing-manifests` into the binary with
|
||||||
|
[go-bindata](https://github.com/jteeuwen/go-bindata/go-bindata). The
|
||||||
|
E2E framework used to have a hard dependency on the output of
|
||||||
|
`go-bindata`, now [bindata support is
|
||||||
|
optional](https://github.com/kubernetes/kubernetes/pull/69103). When
|
||||||
|
accessing a file via the [testfiles
|
||||||
|
package](https://github.com/kubernetes/kubernetes/blob/v1.13.0/test/e2e/framework/testfiles/testfiles.go),
|
||||||
|
files will be retrieved from different sources:
|
||||||
|
|
||||||
|
* relative to the directory specified with `--repo-root` parameter
|
||||||
|
* zero or more bindata chunks
|
||||||
|
|
||||||
|
### Test parameters
|
||||||
|
|
||||||
|
The e2e.test binary takes additional parameters which control test
|
||||||
|
execution. In 2016, an effort was started to replace all E2E command
|
||||||
|
line parameters with a Viper configuration file. But that effort
|
||||||
|
[stalled](https://github.com/kubernetes/kubernetes/blob/0ed33881dc4355495f623c6f22e7dd0b7632b7c0/test/e2e/framework/test_context.go#L318-L319
|
||||||
|
), which left developers without clear guidance how they should handle
|
||||||
|
test-specific parameters.
|
||||||
|
|
||||||
|
The approach in v1.12 was to add all flags to the central
|
||||||
|
[test/e2e/framework/test_context.go](https://github.com/kubernetes/kubernetes/blob/v1.12.0/test/e2e/framework/test_context.go),
|
||||||
|
which does not work for tests developed independently from the
|
||||||
|
framework. Since [PR
|
||||||
|
#69105](https://github.com/kubernetes/kubernetes/pull/69105) the
|
||||||
|
recommendation has been to use the normal `flag` package to
|
||||||
|
define its parameters, in its own source code. Flag names must be
|
||||||
|
hierarchical with dots separating different levels, for example
|
||||||
|
`my.test.parameter`, and must be unique. Uniqueness is enforced by the
|
||||||
|
`flag` package which panics when registering a flag a second time. The
|
||||||
|
new
|
||||||
|
[config](https://github.com/kubernetes/kubernetes/blob/v1.13.0/test/e2e/framework/config/config.go)
|
||||||
|
package simplifies the definition of multiple options, which are
|
||||||
|
stored in a single struct.
|
||||||
|
|
||||||
|
To summarize, this is how parameters are handled now:
|
||||||
|
|
||||||
|
* The init code in test packages defines tests and parameters. The
|
||||||
|
actual parameter *values* are not available yet, so test definitions
|
||||||
|
cannot use them.
|
||||||
|
* The init code of the test suite parses parameters and (optionally)
|
||||||
|
the configuration file.
|
||||||
|
* The tests run and now can use parameter values.
|
||||||
|
|
||||||
|
However, recently it [was pointed
|
||||||
|
out](https://github.com/kubernetes/kubernetes/pull/69105#discussion_r267960062)
|
||||||
|
that it is desirable and was possible to not expose test settings as
|
||||||
|
command line flags and only set them via a configuration file. There
|
||||||
|
is an [open bug](https://github.com/kubernetes/kubernetes/issues/75590) and a
|
||||||
|
[pending PR](https://github.com/kubernetes/kubernetes/pull/75593)
|
||||||
|
about this.
|
||||||
|
|
||||||
|
Viper support has been enhanced. Like the provider support, it is
|
||||||
|
completely optional. It gets pulled into a e2e.test binary by
|
||||||
|
importing the `viperconfig` package and [calling
|
||||||
|
it](https://github.com/kubernetes/kubernetes/blob/ddf47ac13c1a9483ea035a79cd7c10005ff21a6d/test/e2e/e2e_test.go#L49-L57)
|
||||||
|
after parsing the normal command line flags. This has been implemented
|
||||||
|
so that all variables which can be set via command line flags are also
|
||||||
|
set when the flag appears in a Viper config file. For example, the
|
||||||
|
Kubernetes v1.13 `e2e.test` binary accepts
|
||||||
|
`--viper-config=/tmp/my-config.yaml` and that file will set the
|
||||||
|
`my.test.parameter` to `value` when it has this content: my: test:
|
||||||
|
parameter: value
|
||||||
|
|
||||||
|
In older Kubernetes releases, that option could only load a file from
|
||||||
|
the current directory, the suffix had to be left out, and only a few
|
||||||
|
parameters actually could be set this way. Beware that one limitation
|
||||||
|
of Viper still exists: it works by matching config file entries
|
||||||
|
against known flags, without warning about unknown config file entries
|
||||||
|
and thus leaving typos undetected. A [better config file
|
||||||
|
parser](https://github.com/kubernetes/kubeadm/issues/1040) for
|
||||||
|
Kubernetes is still work in progress.
|
||||||
|
|
||||||
|
### Creating items from .yaml manifests
|
||||||
|
|
||||||
|
In Kubernetes 1.12, there was some support for loading individual
|
||||||
|
items from a .yaml file, but then creating that item had to be done by
|
||||||
|
hand-written code. Now the framework has [new
|
||||||
|
methods](https://github.com/kubernetes/kubernetes/blob/v1.13.0/test/e2e/framework/create.go)
|
||||||
|
for loading a .yaml file that has multiple items, patching those items
|
||||||
|
(for example, setting the namespace created for the current test), and
|
||||||
|
creating them. This is currently [used to deploy CSI
|
||||||
|
drivers](https://github.com/kubernetes/kubernetes/blob/ddf47ac13c1a9483ea035a79cd7c10005ff21a6d/test/e2e/storage/drivers/csi.go#L192-L209
|
||||||
|
) anew for each test from exactly the same .yaml files that are also
|
||||||
|
used for deployment via kubectl. If the CSI driver supports running
|
||||||
|
under different names, then tests are completely independent and can
|
||||||
|
run in parallel.
|
||||||
|
|
||||||
|
However, redeploying a driver slows down test execution and it does
|
||||||
|
not cover concurrent operations against the driver. A more realistic
|
||||||
|
test scenario is to deploy a driver once when bringing up the test
|
||||||
|
cluster, then run all tests against that deployment. Eventually the
|
||||||
|
Kubernetes E2E testing will move to that model, once it is clearer how
|
||||||
|
test cluster bringup can be extended such that it also includes
|
||||||
|
installing additional entities like CSI drivers.
|
||||||
|
|
||||||
|
## Upcoming enhancements in Kubernetes 1.14
|
||||||
|
|
||||||
|
### Reusing storage tests
|
||||||
|
|
||||||
|
Being able to use the framework outside of Kubernetes enables building
|
||||||
|
a custom test suite. But a test suite without tests is still
|
||||||
|
useless. Several of the existing tests, in particular for storage, can
|
||||||
|
also be applied to out-of-tree components. Thanks to the work done by
|
||||||
|
Masaki Kimura, [storage
|
||||||
|
tests](https://github.com/kubernetes/kubernetes/tree/v1.13.0/test/e2e/storage/testsuites)
|
||||||
|
in Kubernetes 1.13 are defined such that they can be instantiated
|
||||||
|
multiple times for different drivers.
|
||||||
|
|
||||||
|
But history has a habit of repeating itself. As with providers, the
|
||||||
|
package defining these tests also pulled in driver definitions for all
|
||||||
|
in-tree storage backends, which in turn pulled in more additional
|
||||||
|
packages than were needed. This has been
|
||||||
|
[fixed](https://github.com/kubernetes/kubernetes/pull/70862) for the
|
||||||
|
upcoming Kubernetes 1.14.
|
||||||
|
|
||||||
|
### Skipping unsupported tests
|
||||||
|
|
||||||
|
Some of the storage tests depend on features of the cluster (like
|
||||||
|
running on a host that supports XFS) or of the driver (like supporting
|
||||||
|
block volumes). These conditions are checked while the test runs,
|
||||||
|
leading to skipped tests when they are not satisfied. The good thing
|
||||||
|
is that this records an explanation why the test did not run.
|
||||||
|
|
||||||
|
Starting a test is slow, in particular when it must first deploy the
|
||||||
|
CSI driver, but also in other scenarios. Creating the namespace for a
|
||||||
|
test has been measured at 5 seconds on a fast cluster, and it produces
|
||||||
|
a lot of noisy test output. It would have been possible to address
|
||||||
|
that by [skipping the definition of unsupported
|
||||||
|
tests](https://github.com/kubernetes/kubernetes/pull/70992), but then
|
||||||
|
reporting why a test isn’t even part of the test suite becomes
|
||||||
|
tricky. This approach has been dropped in favor of reorganizing the
|
||||||
|
storage test suite such that it [first checks
|
||||||
|
conditions](https://github.com/kubernetes/kubernetes/pull/72434)
|
||||||
|
before doing the more expensive test setup steps.
|
||||||
|
|
||||||
|
### More readable test definitions
|
||||||
|
|
||||||
|
The same PR also rewrites the tests to operate like conventional
|
||||||
|
Ginkgo tests, with test cases and their local variables in [a single
|
||||||
|
function](https://github.com/pohly/kubernetes/blob/ec3655a1d40ced6b1873e627b736aae1cf242477/test/e2e/storage/testsuites/provisioning.go#L82).
|
||||||
|
|
||||||
|
### Testing external drivers
|
||||||
|
|
||||||
|
Building a custom E2E test suite is still quite a bit of work. The
|
||||||
|
e2e.test binary that will get distributed in the [Kubernetes 1.14 test
|
||||||
|
archive](https://dl.k8s.io/v1.14.0/kubernetes-test.tar.gz) will have
|
||||||
|
the [ability to
|
||||||
|
test](https://github.com/kubernetes/kubernetes/pull/72836) already
|
||||||
|
installed storage drivers without rebuilding the test suite. See this
|
||||||
|
[README](https://github.com/pohly/kubernetes/blob/6644db9914379a4a7b3d3487b41b2010f226e4dc/test/e2e/storage/external/README.md)
|
||||||
|
for further instructions.
|
||||||
|
|
||||||
|
## E2E test suite HOWTO
|
||||||
|
|
||||||
|
### Test suite initialization
|
||||||
|
|
||||||
|
The first step is to set up the necessary boilerplate code that
|
||||||
|
defines the test suite. [In Kubernetes
|
||||||
|
E2E](https://github.com/kubernetes/kubernetes/tree/v1.13.0/test/e2e),
|
||||||
|
this is done in the `e2e.go` and `e2e_test.go` files. It could also be
|
||||||
|
done in a single `e2e_test.go` file. Kubernetes imports all of the
|
||||||
|
various providers, in-tree tests, Viper configuration support, and
|
||||||
|
bindata file lookup in `e2e_test.go`. `e2e.go` controls the actual
|
||||||
|
execution, including some cluster preparations and metrics collection.
|
||||||
|
|
||||||
|
A simpler starting point are the `e2e_[test].go` files [from
|
||||||
|
PMEM-CSI](https://github.com/intel/pmem-csi/tree/586ae281ac2810cb4da6f1e160cf165c7daf0d80/test/e2e). It
|
||||||
|
doesn’t use any providers, no Viper, no bindata, and imports just the
|
||||||
|
storage tests.
|
||||||
|
|
||||||
|
Like PMEM-CSI, OIM drops all of the extra features, but is a bit more
|
||||||
|
complex because it integrates a custom cluster startup directly into
|
||||||
|
the [test
|
||||||
|
suite](https://github.com/intel/pmem-csi/blob/a7b0d66b59771bf615e07fcd3d4f0ba08cfdf90f/test/e2e/e2e.go),
|
||||||
|
which was useful in this case because some additional components have
|
||||||
|
to run on the host side. By running them directly in the E2E binary,
|
||||||
|
interactive debugging with `dlv` becomes easier.
|
||||||
|
|
||||||
|
Both CSI drivers follow the Kubernetes example and use the `test/e2e`
|
||||||
|
directory for their test suites, but any other directory and other
|
||||||
|
file names would also work.
|
||||||
|
|
||||||
|
### Adding E2E storage tests
|
||||||
|
|
||||||
|
Tests are defined by packages that get imported into a test suite. The
|
||||||
|
only thing specific to E2E tests is that they instantiate a
|
||||||
|
`framework.Framework` pointer (usually called `f`) with
|
||||||
|
`framework.NewDefaultFramework`. This variable gets initialized anew
|
||||||
|
in a `BeforeEach` for each test and freed in an `AfterEach`. It has a
|
||||||
|
`f.ClientSet` and `f.Namespace` at runtime (and only at runtime!)
|
||||||
|
which can be used by a test.
|
||||||
|
|
||||||
|
The [PMEM-CSI storage
|
||||||
|
test](https://github.com/intel/pmem-csi/blob/586ae281ac2810cb4da6f1e160cf165c7daf0d80/storage/csi_volumes.go#L51)
|
||||||
|
imports the Kubernetes storage test suite and sets up one instance of
|
||||||
|
the provisioning tests for a PMEM-CSI driver which must be already
|
||||||
|
installed in the test cluster. The storage test suite changes the
|
||||||
|
storage class to run tests with different filesystem types. Because of
|
||||||
|
this requirement, the storage class is created from a .yaml file.
|
||||||
|
|
||||||
|
Explaining all the various utility methods available in the framework
|
||||||
|
is out of scope for this blog post. Reading existing tests and the
|
||||||
|
source code of the framework is a good way to get started.
|
||||||
|
|
||||||
|
### Vendoring
|
||||||
|
|
||||||
|
Vendoring Kubernetes code is still not trivial, even after eliminating
|
||||||
|
many of the unnecessary dependencies. `k8s.io/kubernetes` is not meant
|
||||||
|
to be included in other projects and does not define its dependencies
|
||||||
|
in a way that is understood by tools like `dep`. The other `k8s.io`
|
||||||
|
packages are meant to be included, but [don’t follow semantic
|
||||||
|
versioning
|
||||||
|
yet]([https://github.com/kubernetes/kubernetes/issues/72638) or don’t
|
||||||
|
tag any releases (`k8s.io/kube-openapi`, `k8s.io/utils`).
|
||||||
|
|
||||||
|
PMEM-CSI uses [dep](https://golang.github.io/dep/). It’s
|
||||||
|
[Gopkg.toml](https://github.com/intel/pmem-csi/blob/0ad8251c064b1010c91e7fc1dd423b95d5594bba/Gopkg.toml)
|
||||||
|
file is a good starting point. It enables pruning (not enabled in dep
|
||||||
|
by default) and locks certain projects onto versions that are
|
||||||
|
compatible with the Kubernetes version that is used. When `dep`
|
||||||
|
doesn’t pick a compatible version, then checking Kubernetes’
|
||||||
|
[Godeps.json](https://github.com/kubernetes/kubernetes/blob/master/Godeps/Godeps.json)
|
||||||
|
helps to determine which revision might be the right one.
|
||||||
|
|
||||||
|
### Compiling and running the test suite
|
||||||
|
|
||||||
|
`go test ./test/e2e -args -help` is the fastest way to test that the
|
||||||
|
test suite compiles.
|
||||||
|
|
||||||
|
Once it does compile and a cluster has been set up, the command `go
|
||||||
|
test -timeout=0 -v ./test/e2e -ginkgo.v` runs all tests. In order to
|
||||||
|
run tests in parallel, use the `ginkgo -p ./test/e2e` command instead.
|
||||||
|
|
||||||
|
## Getting involved
|
||||||
|
|
||||||
|
The Kubernetes E2E framework is owned by the testing-commons
|
||||||
|
sub-project in
|
||||||
|
[SIG-testing](https://github.com/kubernetes/community/tree/master/sig-testing). See
|
||||||
|
that page for contact information.
|
||||||
|
|
||||||
|
There are various tasks that could be worked on, including but not
|
||||||
|
limited to:
|
||||||
|
|
||||||
|
* Moving test/e2e/framework into a staging repo and restructuring it
|
||||||
|
so that it is more modular
|
||||||
|
([#74352](https://github.com/kubernetes/kubernetes/issues/74352)).
|
||||||
|
* Simplifying `e2e.go` by moving more of its code into
|
||||||
|
`test/e2e/framework`
|
||||||
|
([#74353](https://github.com/kubernetes/kubernetes/issues/74353)).
|
||||||
|
* Removing provider-specific code from the Kubernetes E2E test suite
|
||||||
|
([#70194](https://github.com/kubernetes/kubernetes/issues/70194)).
|
||||||
|
|
||||||
|
Special thanks to the reviewers of this article:
|
||||||
|
|
||||||
|
- Olev Kartau (https://github.com/okartau)
|
||||||
|
- Mary Camp (https://github.com/MCamp859)
|
||||||
Reference in New Issue
Block a user