---
title: Testing of CSI drivers
date: 2020-01-08
---

**Author:** Patrick Ohly (Intel)

When developing a [Container Storage Interface (CSI)
driver](https://kubernetes-csi.github.io/docs/), it is useful to leverage
as much prior work as possible. This includes source code (like the
[sample CSI hostpath
driver](https://github.com/kubernetes-csi/csi-driver-host-path/)) but
also existing tests. Besides saving time, using tests written by
someone else has the advantage that it can point out aspects of the
specification that might have been overlooked otherwise.

An earlier blog post about [end-to-end
testing](https://kubernetes.io/blog/2019/03/22/kubernetes-end-to-end-testing-for-everyone/)
already showed how to use the [Kubernetes storage
tests](https://github.com/kubernetes/kubernetes/tree/master/test/e2e/storage/testsuites)
for testing of a third-party CSI driver. That
approach makes sense when the goal to also add custom E2E tests, but
depends on quite a bit of effort for setting up and maintaining a test
suite.

When the goal is to merely run the existing tests, then there are
simpler approaches. This blog post introduces those.

## Sanity testing

[csi-test
sanity](https://github.com/kubernetes-csi/csi-test/tree/master/pkg/sanity)
ensures that a CSI driver conforms to the CSI specification by calling
the gRPC methods in various ways and checking that the outcome is as
required. Despite
its current hosting under the Kubernetes-CSI organization, it is
completely independent of Kubernetes. Tests connect to a running CSI
driver through its Unix domain socket, so although the tests are
written in Go, the driver itself can be implemented in any language.

The main
[README](https://github.com/kubernetes-csi/csi-test/blob/master/pkg/sanity/README.md)
explains how to include those tests into an existing Go test
suite. The simpler alternative is to just invoke the [csi-sanity](
https://github.com/kubernetes-csi/csi-test/tree/master/cmd/csi-sanity)
command.

### Installation

Starting with csi-test v3.0.0, you can build the `csi-sanity` command
with `go get github.com/kubernetes-csi/csi-test/cmd/csi-sanity` and
you'll find the compiled binary in `$GOPATH/bin/csi-sanity`.

`go get` always builds the latest revision from the master branch. To
build a certain release, [get the source
code](https://github.com/kubernetes-csi/csi-test/releases) and run
`make -C cmd/csi-sanity`. This produces `cmd/csi-sanity/csi-sanity`.

### Usage

The `csi-sanity` binary is a full [Ginkgo test
suite](http://onsi.github.io/ginkgo/) and thus has the usual `-gingko`
command line flags. In particular, `-ginkgo.focus` and
`-ginkgo.skip` can be used to select which tests are run resp. not
run.

During a test run, `csi-sanity` simulates the behavior of a container
orchestrator (CO) by creating staging and target directories as required by the CSI spec
and calling a CSI driver via gRPC. The driver must be started before
invoking `csi-sanity`. Although the tests currently only check the gRPC
return codes, that might change and so the driver really should make
the changes requested by a call, like mounting a filesystem. That may
mean that it has to run as root.

At least one [gRPC
endpoint](https://github.com/grpc/grpc/blob/master/doc/naming.md) must
be specified via the `-csi.endpoint` parameter when invoking
`csi-sanity`, either as absolute path (`unix:/tmp/csi.sock`) for a Unix
domain socket or as host name plus port (`dns:///my-machine:9000`) for
TCP. `csi-sanity` then uses that endpoint for both node and controller
operations. A separate endpoint for controller operations can be
specified with `-csi.controllerendpoint`. Directories are created in
`/tmp` by default. This can be changed via `-csi.mountdir` and
`-csi.stagingdir`.

Some drivers cannot be deployed such that everything is guaranteed to
run on the same host. In such a case, custom scripts have to be used
to handle directories: they log into the host where the CSI node
controller runs and create or remove the directories there.

For example, during CI testing the [CSI hostpath example
driver](https://github.com/kubernetes-csi/csi-driver-host-path) gets
deployed on a real Kubernetes cluster before invoking `csi-sanity` and then
`csi-sanity` connects to it through port forwarding provided by
[`socat`](https://github.com/kubernetes-csi/csi-driver-host-path/blob/v1.2.0/deploy/kubernetes-1.16/hostpath/csi-hostpath-testing.yaml).
[Scripts](https://github.com/kubernetes-csi/csi-driver-host-path/blob/v1.2.0/release-tools/prow.sh#L808-L859)
are used to create and remove the directories.

Here's how one can replicate that, using the v1.2.0 release of the CSI hostpath driver:

```
$ cd csi-driver-host-path
$ git describe --tags HEAD
v1.2.0
$ kubectl get nodes
NAME        STATUS   ROLES    AGE   VERSION
127.0.0.1   Ready    <none>   42m   v1.16.0

$ deploy/kubernetes-1.16/deploy-hostpath.sh 
applying RBAC rules
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-provisioner/v1.4.0/deploy/kubernetes/rbac.yaml
...
deploying hostpath components
   deploy/kubernetes-1.16/hostpath/csi-hostpath-attacher.yaml
        using           image: quay.io/k8scsi/csi-attacher:v2.0.0
service/csi-hostpath-attacher created
statefulset.apps/csi-hostpath-attacher created
   deploy/kubernetes-1.16/hostpath/csi-hostpath-driverinfo.yaml
csidriver.storage.k8s.io/hostpath.csi.k8s.io created
   deploy/kubernetes-1.16/hostpath/csi-hostpath-plugin.yaml
        using           image: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0
        using           image: quay.io/k8scsi/hostpathplugin:v1.2.0
        using           image: quay.io/k8scsi/livenessprobe:v1.1.0
...
service/hostpath-service created
statefulset.apps/csi-hostpath-socat created
07:38:46 waiting for hostpath deployment to complete, attempt #0
deploying snapshotclass
volumesnapshotclass.snapshot.storage.k8s.io/csi-hostpath-snapclass created

$ cat >mkdir_in_pod.sh <<EOF
#!/bin/sh
kubectl exec csi-hostpathplugin-0 -c hostpath -- mktemp -d /tmp/csi-sanity.XXXXXX
EOF

$ cat >rmdir_in_pod.sh <<EOF
#!/bin/sh
kubectl exec csi-hostpathplugin-0 -c hostpath -- rmdir "\$@"
EOF

$ chmod u+x *_in_pod.sh
$ csi-sanity -ginkgo.v \
             -csi.endpoint dns:///127.0.0.1:$(kubectl get "services/hostpath-service" -o "jsonpath={..nodePort}") \
             -csi.createstagingpathcmd ./mkdir_in_pod.sh \
             -csi.createmountpathcmd ./mkdir_in_pod.sh \
             -csi.removestagingpathcmd ./rmdir_in_pod.sh \
             -csi.removemountpathcmd ./rmdir_in_pod.sh

Running Suite: CSI Driver Test Suite
====================================
Random Seed: 1570540138
Will run 72 of 72 specs
...
Controller Service [Controller Server] ControllerGetCapabilities 
  should return appropriate capabilities
  /nvme/gopath/src/github.com/kubernetes-csi/csi-test/pkg/sanity/controller.go:111
STEP: connecting to CSI driver
STEP: creating mount and staging directories
STEP: checking successful response
•
------------------------------
Controller Service [Controller Server] GetCapacity 
  should return capacity (no optional values added)
  /nvme/gopath/src/github.com/kubernetes-csi/csi-test/pkg/sanity/controller.go:149
STEP: reusing connection to CSI driver at dns:///127.0.0.1:30056
STEP: creating mount and staging directories
...
Ran 53 of 72 Specs in 148.206 seconds
SUCCESS! -- 53 Passed | 0 Failed | 0 Pending | 19 Skipped
PASS
```

Some comments:

* The source code of these tests is in the [`pkg/sanity`
  package](https://github.com/kubernetes-csi/csi-test/tree/master/pkg/sanity).
* How to determine the external IP address of the node depends on the
  cluster. In this example, the cluster was brought up with
  `hack/local-up-cluster.sh` and thus runs on the local host (`127.0.0.1`).
  It uses a port allocated by Kubernetes, obtained above with `kubectl get "services/hostpath-service"`.
  The Kubernetes-CSI CI uses
  [kind](https://kind.sigs.k8s.io/docs/user/quick-start/) and there [a
  Docker
  command](https://github.com/kubernetes-csi/csi-driver-host-path/blob/3488dc7f994e33485629b86b69a6f34ebb7ef2d9/release-tools/prow.sh#L850)
  can be used.
* The create script must print the final directory. Using a
  unique directory for each test case has the advantage that if
  something goes wrong in one test case, others still start with a
  clean slate.
* The "staging directory", aka `NodePublishVolumeRequest.target_path`
  in the CSI spec, must be created and deleted by the CSI driver while
  the CO is responsible for the parent directory. `csi-sanity` handles
  that by creating a directory and then giving the CSI driver that
  directory path with `/target` appended at the end. Kubernetes [got
  this wrong](https://github.com/kubernetes/kubernetes/issues/75535)
  and creates the actual `target_path` directory, so CSI drivers which
  want to work with Kubernetes currently have to be lenient and must
  not fail when that directory already exists.
* The "mount directory" corresponds to
  `NodeStageVolumeRequest.staging_target_path` and really gets created
  by the CO, i.e. `csi-sanity`.

## End-to-end testing

In contrast to `csi-sanity`, end-to-end testing interacts with the CSI
driver through the Kubernetes API, i.e. it simulates operations from a
normal user, like creating a PersistentVolumeClaim. Support for testing external CSI
drivers was
[added](https://github.com/kubernetes/kubernetes/commit/6644db9914379a4a7b3d3487b41b2010f226e4dc#diff-5b2d9461c960bc9b146c4ab3d77bcaa5)
in Kubernetes 1.14.0.

### Installation

For each Kubernetes release, a test tar archive is published. It's not
listed in the release notes (for example, the ones for
[1.16](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.16.md#downloads-for-v1161)),
so one has to know that the full URL is
`https://dl.k8s.io/<version>/kubernetes-test-linux-amd64.tar.gz` (like
for
[v1.16.0](https://dl.k8s.io/v1.16.0/kubernetes-test-linux-amd64.tar.gz)).

These include a `e2e.test` binary for Linux on x86-64. Archives for
other platforms are also available, see [this
KEP](https://github.com/kubernetes/enhancements/blob/master/keps/sig-testing/20190118-breaking-apart-the-kubernetes-test-tarball.md#proposal). The
`e2e.test` binary is completely self-contained, so one can "install"
it and the [`ginkgo` test runner](https://onsi.github.io/ginkgo/) with:

```
curl --location https://dl.k8s.io/v1.16.0/kubernetes-test-linux-amd64.tar.gz | \
  tar --strip-components=3 -zxf - kubernetes/test/bin/e2e.test kubernetes/test/bin/ginkgo
```

Each `e2e.test` binary contains tests that match the features
available in the corresponding release. In particular, the `[Feature:
xyz]` tags change between releases: they separate tests of alpha
features from tests of non-alpha features. Also, the tests from an
older release might rely on APIs that were removed in more recent
Kubernetes releases. To avoid problems, it's best to simply use the
`e2e.test` binary that matches the Kubernetes release that is used for
testing.

### Usage

Not all features of a CSI driver can be discovered through the
Kubernetes API. Therefore a configuration file in YAML or JSON format
is needed which describes the driver that is to be tested. That file
is used to populate [the driverDefinition
struct](https://github.com/kubernetes/kubernetes/blob/v1.16.0/test/e2e/storage/external/external.go#L142-L211)
and [the DriverInfo
struct](https://github.com/kubernetes/kubernetes/blob/v1.16.0/test/e2e/storage/testsuites/testdriver.go#L139-L185)
that is embedded inside it. For detailed usage instructions of
individual fields refer to these structs.

A word of warning: tests are often only run when setting some fields and the
file parser does not warn about unknown fields, so always check that
the file really matches those structs.

Here is an example that tests the
[`csi-driver-host-path`](https://github.com/kubernetes-csi/csi-driver-host-path):

```
$ cat >test-driver.yaml <<EOF
StorageClass:
  FromName: true
SnapshotClass:
  FromName: true
DriverInfo:
  Name: hostpath.csi.k8s.io
  Capabilities:
    block: true
    controllerExpansion: true
    exec: true
    multipods: true
    persistence: true
    pvcDataSource: true
    snapshotDataSource: true
InlineVolumes:
- Attributes: {}
EOF
```

At a minimum, you need to define the storage class you want to use in
the test, the name of your driver, and what capabilities you want to
test.
As with `csi-sanity`, the driver has to be running in the cluster
before testing it.
The actual `e2e.test` invocation then enables tests for this driver
with `-storage.testdriver` and selects the storage tests for it with
`-ginkgo.focus`:

```
$ ./e2e.test -ginkgo.v \
             -ginkgo.focus='External.Storage' \
             -storage.testdriver=test-driver.yaml
Oct  8 17:17:42.230: INFO: The --provider flag is not set. Continuing as if --provider=skeleton had been used.
I1008 17:17:42.230210  648569 e2e.go:92] Starting e2e run "90b9adb0-a3a2-435f-80e0-640742d56104" on Ginkgo node 1
Running Suite: Kubernetes e2e suite
===================================
Random Seed: 1570547861 - Will randomize all specs
Will run 163 of 5060 specs

Oct  8 17:17:42.237: INFO: >>> kubeConfig: /var/run/kubernetes/admin.kubeconfig
Oct  8 17:17:42.241: INFO: Waiting up to 30m0s for all (but 0) nodes to be schedulable
...
------------------------------
SSSSSSSSSSSSSSSSSSSS
------------------------------
External Storage [Driver: hostpath.csi.k8s.io] [Testpattern: Dynamic PV (filesystem volmode)] multiVolume [Slow] 
  should access to two volumes with different volume mode and retain data across pod recreation on the same node
  /workspace/anago-v1.16.0-rc.2.1+2bd9643cee5b3b/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/storage/testsuites/multivolume.go:191
[BeforeEach] [Testpattern: Dynamic PV (filesystem volmode)] multiVolume [Slow]
...
```

You can use `ginkgo` to run some kinds of test in parallel.
Alpha feature tests or those that by design have to be run
sequentially then need to be run separately:

```
$ ./ginkgo -p -v \
         -focus='External.Storage' \
         -skip='\[Feature:|\[Disruptive\]|\[Serial\]' \
         ./e2e.test \
         -- \
         -storage.testdriver=test-driver.yaml
$ ./ginkgo -v \
         -focus='External.Storage.*(\[Feature:|\[Disruptive\]|\[Serial\])' \
         ./e2e.test \
         -- \
         -storage.testdriver=test-driver.yaml
```

## Getting involved

Both the Kubernetes storage tests and the sanity tests are meant to be
applicable to arbitrary CSI drivers. But perhaps tests are based on
additional assumptions and your driver does not pass the testing
although it complies with the CSI specification. If that happens then
please file issues (links below).

These are open source projects which depend on the help of those
using them, so once a problem has been acknowledged, a pull request
addressing it will be highly welcome.

The same applies to writing new tests. The following searches in the
issue trackers select issues that have been marked specifically as
something that needs someone's help:
- [csi-test](https://github.com/kubernetes-csi/csi-test/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
- [Kubernetes](https://github.com/kubernetes/kubernetes/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+label%3Asig%2Fstorage+)

Happy testing! May the issues it finds be few and easy to fix.
