add csi-test to vendor

This commit is contained in:
wackxu
2018-08-21 23:03:36 +08:00
parent 62551068b1
commit a2987675cf
110 changed files with 11973 additions and 24 deletions

13
vendor/github.com/kubernetes-csi/csi-test/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
bin/mock
cmd/csi-sanity/csi-sanity

11
vendor/github.com/kubernetes-csi/csi-test/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,11 @@
language: go
matrix:
include:
- go: 1.10.3
script:
- make test
after_success:
- if [ "${TRAVIS_BRANCH}" == "master" ] && [ "${TRAVIS_PULL_REQUEST}" == "false" ]; then
docker login -u "${DOCKER_USERNAME}" -p "${DOCKER_PASSWORD}" quay.io;
make push;
fi

View File

@@ -0,0 +1,6 @@
FROM alpine
LABEL maintainers="Kubernetes Authors"
LABEL description="CSI Mock Driver"
COPY ./bin/mock mock
ENTRYPOINT ["/mock"]

195
vendor/github.com/kubernetes-csi/csi-test/Gopkg.lock generated vendored Normal file
View File

@@ -0,0 +1,195 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/container-storage-interface/spec"
packages = ["lib/go/csi/v0"]
revision = "2178fdeea87f1150a17a63252eee28d4d8141f72"
version = "v0.3.0"
[[projects]]
name = "github.com/golang/mock"
packages = ["gomock"]
revision = "c34cdb4725f4c3844d095133c6e40e448b86589b"
version = "v1.1.1"
[[projects]]
name = "github.com/golang/protobuf"
packages = [
"proto",
"protoc-gen-go/descriptor",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp",
"ptypes/wrappers"
]
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
name = "github.com/onsi/ginkgo"
packages = [
".",
"config",
"internal/codelocation",
"internal/containernode",
"internal/failer",
"internal/leafnodes",
"internal/remote",
"internal/spec",
"internal/spec_iterator",
"internal/specrunner",
"internal/suite",
"internal/testingtproxy",
"internal/writer",
"reporters",
"reporters/stenographer",
"reporters/stenographer/support/go-colorable",
"reporters/stenographer/support/go-isatty",
"types"
]
revision = "fa5fabab2a1bfbd924faf4c067d07ae414e2aedf"
version = "v1.5.0"
[[projects]]
name = "github.com/onsi/gomega"
packages = [
".",
"format",
"internal/assertion",
"internal/asyncassertion",
"internal/oraclematcher",
"internal/testingtsupport",
"matchers",
"matchers/support/goraph/bipartitegraph",
"matchers/support/goraph/edge",
"matchers/support/goraph/node",
"matchers/support/goraph/util",
"types"
]
revision = "62bff4df71bdbc266561a0caee19f0594b17c240"
version = "v1.4.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"html",
"html/atom",
"html/charset",
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace"
]
revision = "1e491301e022f8f977054da4c2d852decd59571f"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"encoding",
"encoding/charmap",
"encoding/htmlindex",
"encoding/internal",
"encoding/internal/identifier",
"encoding/japanese",
"encoding/korean",
"encoding/simplifiedchinese",
"encoding/traditionalchinese",
"encoding/unicode",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"internal/utf8internal",
"language",
"runes",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
revision = "32ee49c4dd805befd833990acba36cb75042378c"
[[projects]]
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"channelz",
"codes",
"connectivity",
"credentials",
"encoding",
"encoding/proto",
"grpclb/grpc_lb_v1/messages",
"grpclog",
"internal",
"keepalive",
"metadata",
"naming",
"peer",
"reflection",
"reflection/grpc_reflection_v1alpha",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
"transport"
]
revision = "7a6a684ca69eb4cae85ad0a484f2e531598c047b"
version = "v1.12.2"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "5dd480018adbb94025564b74bad8dd269cc516183b7b428317f6dd04b07726f4"
solver-name = "gps-cdcl"
solver-version = 1

62
vendor/github.com/kubernetes-csi/csi-test/Gopkg.toml generated vendored Normal file
View File

@@ -0,0 +1,62 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/container-storage-interface/spec"
version = "~0.3.0"
[[constraint]]
name = "github.com/golang/mock"
version = "1.0.0"
[[constraint]]
name = "github.com/golang/protobuf"
version = "v1.1.0"
[[constraint]]
name = "github.com/onsi/ginkgo"
version = "1.4.0"
[[constraint]]
name = "github.com/onsi/gomega"
version = "1.3.0"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
name = "google.golang.org/grpc"
version = "1.9.2"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "v2.1.1"
[prune]
go-tests = true
unused-packages = true

201
vendor/github.com/kubernetes-csi/csi-test/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

52
vendor/github.com/kubernetes-csi/csi-test/Makefile generated vendored Normal file
View File

@@ -0,0 +1,52 @@
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
IMAGE_NAME = quay.io/k8scsi/mock-driver
IMAGE_VERSION = canary
APP := ./bin/mock
ifdef V
TESTARGS = -v -args -alsologtostderr -v 5
else
TESTARGS =
endif
all: $(APP)
$(APP):
mkdir -p bin
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o $(APP) ./mock/main.go
clean:
rm -rf bin
container: $(APP)
docker build -f Dockerfile.mock -t $(IMAGE_NAME):$(IMAGE_VERSION) .
push: container
docker push $(IMAGE_NAME):$(IMAGE_VERSION)
test:
files=$$(find ./ -name '*.go' | grep -v '^./vendor' ); \
if [ $$(gofmt -d $$files | wc -l) -ne 0 ]; then \
echo "formatting errors:"; \
gofmt -d $$files; \
false; \
fi
go vet $$(go list ./... | grep -v vendor)
go test $$(go list ./... | grep -v vendor | grep -v "cmd/csi-sanity")
./hack/e2e.sh
.PHONY: all clean container push test

4
vendor/github.com/kubernetes-csi/csi-test/OWNERS generated vendored Normal file
View File

@@ -0,0 +1,4 @@
approvers:
- saad-ali
- lpabon
- pohly

29
vendor/github.com/kubernetes-csi/csi-test/README.md generated vendored Normal file
View File

@@ -0,0 +1,29 @@
[![Build Status](https://travis-ci.org/kubernetes-csi/csi-test.svg?branch=master)](https://travis-ci.org/kubernetes-csi/csi-test)
[![Docker Repository on Quay](https://quay.io/repository/k8scsi/mock-driver/status "Docker Repository on
Quay")](https://quay.io/repository/k8scsi/mock-driver)
# csi-test
csi-test houses packages and libraries to help test CSI client and plugins.
## For Container Orchestration Tests
CO developers can use this framework to create drivers based on the
[Golang mock](https://github.com/golang/mock) framework. Please see
[co_test.go](test/co_test.go) for an example.
### Mock driver for testing
We also provide a container called `quay.io/k8scsi/mock-driver:canary` which can be used as an in-memory mock driver.
It follows the same release cycle as other containers, so the latest release is `quay.io/k8scsi/mock-driver:v0.3.0`.
You will need to setup the environment variable `CSI_ENDPOINT` for the mock driver to know where to create the unix
domain socket.
## For CSI Driver Tests
To test drivers please take a look at [pkg/sanity](https://github.com/kubernetes-csi/csi-test/tree/master/pkg/sanity).
This package and [csi-sanity](https://github.com/kubernetes-csi/csi-test/tree/master/cmd/csi-sanity) are meant to test
the CSI API capability of a driver. They are meant to be an additional test to the unit, functional, and e2e tests of a
CSI driver.
### Note
* Master is for CSI v0.4.0. Please see the branches for other CSI releases.
* Only Golang 1.9+ supported. See [gRPC issue](https://github.com/grpc/grpc-go/issues/711#issuecomment-326626790)

View File

@@ -0,0 +1,14 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Team to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
saad-ali
lpabon

View File

@@ -0,0 +1,61 @@
APP_NAME := csi-sanity
VER :=$(shell git describe)
RELEASEVER := $(shell git describe --abbrev=0)
BRANCH := $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
SHA := $(shell git rev-parse --short HEAD)
ARCH := $(shell go env GOARCH)
GOOS := $(shell go env GOOS)
DIR=.
ifdef APP_SUFFIX
VERSION = $(VER)-$(subst /,-,$(APP_SUFFIX))
else
ifeq (master,$(BRANCH))
VERSION = $(VER)
else
VERSION = $(VER)-$(BRANCH)
endif
endif
LDFLAGS :=-ldflags "-w -X github.com/kubernetes-csi/csi-test/cmd/csi-sanity.VERSION=$(VERSION) -extldflags '-z relro -z now'"
PACKAGE :=$(DIR)/dist/$(APP_NAME)-$(RELEASEVER).$(GOOS).$(ARCH).tar.gz
all: $(APP_NAME)
$(APP_NAME): Makefile sanity_test.go
go test $(LDFLAGS) -c -o $(APP_NAME)
install: $(APP_NAME)
cp $(APP_NAME) $(GOPATH)/bin
clean:
rm -f csi-sanity
dist-clean:
rm -rf $(DIR)/dist
dist: clean $(PACKAGE)
$(PACKAGE): $(APP_NAME)
@echo Packaging Binaries...
@mkdir -p tmp/$(APP_NAME)
@cp $(APP_NAME) tmp/$(APP_NAME)/
@mkdir -p $(DIR)/dist/
tar -czf $@ -C tmp $(APP_NAME);
@rm -rf tmp
@echo
@echo Package $@ saved in dist directory
linux_amd64_dist:
GOOS=linux GOARCH=amd64 $(MAKE) dist
linux_arm64_dist:
GOOS=linux GOARCH=arm64 $(MAKE) dist
darwin_amd64_dist:
GOOS=darwin GOARCH=amd64 $(MAKE) dist
release: dist-clean darwin_amd64_dist linux_amd64_dist linux_arm64_dist
.PHONY: release darwin_amd64_dist linux_arm64_dist linux_amd64_dist \
linux_arm_dist linux_amd64_dist clean dist-clean

View File

@@ -0,0 +1,58 @@
# Sanity Test Command Line Program
This is the command line program that tests a CSI driver using the [`sanity`](https://github.com/kubernetes-csi/csi-test/tree/master/pkg/sanity) package test suite.
Example:
```
$ csi-sanity --csi.endpoint=<your csi driver endpoint>
```
If you want to specify a mount point:
```
$ csi-sanity --csi.endpoint=<your csi driver endpoint> --csi.mountpoint=/mnt
```
For verbose type:
```
$ csi-sanity --ginkgo.v --csi.endpoint=<your csi driver endpoint>
```
For csi-credentials, create a secrets file with all the secrets in it:
```yaml
CreateVolumeSecret:
secretKey: secretval1
DeleteVolumeSecret:
secretKey: secretval2
ControllerPublishVolumeSecret:
secretKey: secretval3
ControllerUnpublishVolumeSecret:
secretKey: secretval4
NodeStageVolumeSecret:
secretKey: secretval5
NodePublishVolumeSecret:
secretKey: secretval6
```
Pass the file path to csi-sanity as:
```
$ csi-sanity --csi.endpoint=<your csi driver endpoint> --csi.secrets=<path to secrets file>
```
Replace the keys and values of the credentials appropriately. Since the whole
secret is passed in the request, multiple key-val pairs can be used.
### Help
The full Ginkgo and golang unit test parameters are available. Type
```
$ csi-sanity -h
```
to get more information
### Download
Please see the [Releases](https://github.com/kubernetes-csi/csi-test/releases) page
to download the latest version of `csi-sanity`

View File

@@ -0,0 +1,56 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
"flag"
"fmt"
"os"
"testing"
"github.com/kubernetes-csi/csi-test/pkg/sanity"
)
const (
prefix string = "csi."
)
var (
VERSION = "(dev)"
version bool
config sanity.Config
)
func init() {
flag.StringVar(&config.Address, prefix+"endpoint", "", "CSI endpoint")
flag.BoolVar(&version, prefix+"version", false, "Version of this program")
flag.StringVar(&config.TargetPath, prefix+"mountdir", os.TempDir()+"/csi", "Mount point for NodePublish")
flag.StringVar(&config.StagingPath, prefix+"stagingdir", os.TempDir()+"/csi", "Mount point for NodeStage if staging is supported")
flag.StringVar(&config.SecretsFile, prefix+"secrets", "", "CSI secrets file")
flag.Int64Var(&config.TestVolumeSize, prefix+"testvolumesize", sanity.DefTestVolumeSize, "Base volume size used for provisioned volumes")
flag.Parse()
}
func TestSanity(t *testing.T) {
if version {
fmt.Printf("Version = %s\n", VERSION)
return
}
if len(config.Address) == 0 {
t.Fatalf("--%sendpoint must be provided with an CSI endpoint", prefix)
}
sanity.Test(t, &config)
}

View File

@@ -0,0 +1,3 @@
# Kubernetes Community Code of Conduct
Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)

View File

@@ -0,0 +1,247 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
//go:generate mockgen -package=driver -destination=driver.mock.go github.com/container-storage-interface/spec/lib/go/csi/v0 IdentityServer,ControllerServer,NodeServer
package driver
import (
"context"
"errors"
"net"
"sync"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var (
// ErrNoCredentials is the error when a secret is enabled but not passed in the request.
ErrNoCredentials = errors.New("secret must be provided")
// ErrAuthFailed is the error when the secret is incorrect.
ErrAuthFailed = errors.New("authentication failed")
)
type CSIDriverServers struct {
Controller csi.ControllerServer
Identity csi.IdentityServer
Node csi.NodeServer
}
// This is the key name in all the CSI secret objects.
const secretField = "secretKey"
// CSICreds is a driver specific secret type. Drivers can have a key-val pair of
// secrets. This mock driver has a single string secret with secretField as the
// key.
type CSICreds struct {
CreateVolumeSecret string
DeleteVolumeSecret string
ControllerPublishVolumeSecret string
ControllerUnpublishVolumeSecret string
NodeStageVolumeSecret string
NodePublishVolumeSecret string
CreateSnapshotSecret string
DeleteSnapshotSecret string
}
type CSIDriver struct {
listener net.Listener
server *grpc.Server
servers *CSIDriverServers
wg sync.WaitGroup
running bool
lock sync.Mutex
creds *CSICreds
}
func NewCSIDriver(servers *CSIDriverServers) *CSIDriver {
return &CSIDriver{
servers: servers,
}
}
func (c *CSIDriver) goServe(started chan<- bool) {
c.wg.Add(1)
go func() {
defer c.wg.Done()
started <- true
err := c.server.Serve(c.listener)
if err != nil {
panic(err.Error())
}
}()
}
func (c *CSIDriver) Address() string {
return c.listener.Addr().String()
}
func (c *CSIDriver) Start(l net.Listener) error {
c.lock.Lock()
defer c.lock.Unlock()
// Set listener
c.listener = l
// Create a new grpc server
c.server = grpc.NewServer(
grpc.UnaryInterceptor(c.authInterceptor),
)
// Register Mock servers
if c.servers.Controller != nil {
csi.RegisterControllerServer(c.server, c.servers.Controller)
}
if c.servers.Identity != nil {
csi.RegisterIdentityServer(c.server, c.servers.Identity)
}
if c.servers.Node != nil {
csi.RegisterNodeServer(c.server, c.servers.Node)
}
reflection.Register(c.server)
// Start listening for requests
waitForServer := make(chan bool)
c.goServe(waitForServer)
<-waitForServer
c.running = true
return nil
}
func (c *CSIDriver) Stop() {
c.lock.Lock()
defer c.lock.Unlock()
if !c.running {
return
}
c.server.Stop()
c.wg.Wait()
}
func (c *CSIDriver) Close() {
c.server.Stop()
}
func (c *CSIDriver) IsRunning() bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.running
}
// SetDefaultCreds sets the default secrets for CSI creds.
func (c *CSIDriver) SetDefaultCreds() {
c.creds = &CSICreds{
CreateVolumeSecret: "secretval1",
DeleteVolumeSecret: "secretval2",
ControllerPublishVolumeSecret: "secretval3",
ControllerUnpublishVolumeSecret: "secretval4",
NodeStageVolumeSecret: "secretval5",
NodePublishVolumeSecret: "secretval6",
CreateSnapshotSecret: "secretval7",
DeleteSnapshotSecret: "secretval8",
}
}
func (c *CSIDriver) authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if c.creds != nil {
authenticated, authErr := isAuthenticated(req, c.creds)
if !authenticated {
if authErr == ErrNoCredentials {
return nil, status.Error(codes.InvalidArgument, authErr.Error())
}
if authErr == ErrAuthFailed {
return nil, status.Error(codes.Unauthenticated, authErr.Error())
}
}
}
h, err := handler(ctx, req)
return h, err
}
func isAuthenticated(req interface{}, creds *CSICreds) (bool, error) {
switch r := req.(type) {
case *csi.CreateVolumeRequest:
return authenticateCreateVolume(r, creds)
case *csi.DeleteVolumeRequest:
return authenticateDeleteVolume(r, creds)
case *csi.ControllerPublishVolumeRequest:
return authenticateControllerPublishVolume(r, creds)
case *csi.ControllerUnpublishVolumeRequest:
return authenticateControllerUnpublishVolume(r, creds)
case *csi.NodeStageVolumeRequest:
return authenticateNodeStageVolume(r, creds)
case *csi.NodePublishVolumeRequest:
return authenticateNodePublishVolume(r, creds)
case *csi.CreateSnapshotRequest:
return authenticateCreateSnapshot(r, creds)
case *csi.DeleteSnapshotRequest:
return authenticateDeleteSnapshot(r, creds)
default:
return true, nil
}
}
func authenticateCreateVolume(req *csi.CreateVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetControllerCreateSecrets(), creds.CreateVolumeSecret)
}
func authenticateDeleteVolume(req *csi.DeleteVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetControllerDeleteSecrets(), creds.DeleteVolumeSecret)
}
func authenticateControllerPublishVolume(req *csi.ControllerPublishVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetControllerPublishSecrets(), creds.ControllerPublishVolumeSecret)
}
func authenticateControllerUnpublishVolume(req *csi.ControllerUnpublishVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetControllerUnpublishSecrets(), creds.ControllerUnpublishVolumeSecret)
}
func authenticateNodeStageVolume(req *csi.NodeStageVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetNodeStageSecrets(), creds.NodeStageVolumeSecret)
}
func authenticateNodePublishVolume(req *csi.NodePublishVolumeRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetNodePublishSecrets(), creds.NodePublishVolumeSecret)
}
func authenticateCreateSnapshot(req *csi.CreateSnapshotRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetCreateSnapshotSecrets(), creds.CreateSnapshotSecret)
}
func authenticateDeleteSnapshot(req *csi.DeleteSnapshotRequest, creds *CSICreds) (bool, error) {
return credsCheck(req.GetDeleteSnapshotSecrets(), creds.DeleteSnapshotSecret)
}
func credsCheck(secrets map[string]string, secretVal string) (bool, error) {
if len(secrets) == 0 {
return false, ErrNoCredentials
}
if secrets[secretField] != secretVal {
return false, ErrAuthFailed
}
return true, nil
}

View File

@@ -0,0 +1,354 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/container-storage-interface/spec/lib/go/csi/v0 (interfaces: IdentityServer,ControllerServer,NodeServer)
// Package driver is a generated GoMock package.
package driver
import (
context "context"
v0 "github.com/container-storage-interface/spec/lib/go/csi/v0"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockIdentityServer is a mock of IdentityServer interface
type MockIdentityServer struct {
ctrl *gomock.Controller
recorder *MockIdentityServerMockRecorder
}
// MockIdentityServerMockRecorder is the mock recorder for MockIdentityServer
type MockIdentityServerMockRecorder struct {
mock *MockIdentityServer
}
// NewMockIdentityServer creates a new mock instance
func NewMockIdentityServer(ctrl *gomock.Controller) *MockIdentityServer {
mock := &MockIdentityServer{ctrl: ctrl}
mock.recorder = &MockIdentityServerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockIdentityServer) EXPECT() *MockIdentityServerMockRecorder {
return m.recorder
}
// GetPluginCapabilities mocks base method
func (m *MockIdentityServer) GetPluginCapabilities(arg0 context.Context, arg1 *v0.GetPluginCapabilitiesRequest) (*v0.GetPluginCapabilitiesResponse, error) {
ret := m.ctrl.Call(m, "GetPluginCapabilities", arg0, arg1)
ret0, _ := ret[0].(*v0.GetPluginCapabilitiesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPluginCapabilities indicates an expected call of GetPluginCapabilities
func (mr *MockIdentityServerMockRecorder) GetPluginCapabilities(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginCapabilities", reflect.TypeOf((*MockIdentityServer)(nil).GetPluginCapabilities), arg0, arg1)
}
// GetPluginInfo mocks base method
func (m *MockIdentityServer) GetPluginInfo(arg0 context.Context, arg1 *v0.GetPluginInfoRequest) (*v0.GetPluginInfoResponse, error) {
ret := m.ctrl.Call(m, "GetPluginInfo", arg0, arg1)
ret0, _ := ret[0].(*v0.GetPluginInfoResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPluginInfo indicates an expected call of GetPluginInfo
func (mr *MockIdentityServerMockRecorder) GetPluginInfo(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPluginInfo", reflect.TypeOf((*MockIdentityServer)(nil).GetPluginInfo), arg0, arg1)
}
// Probe mocks base method
func (m *MockIdentityServer) Probe(arg0 context.Context, arg1 *v0.ProbeRequest) (*v0.ProbeResponse, error) {
ret := m.ctrl.Call(m, "Probe", arg0, arg1)
ret0, _ := ret[0].(*v0.ProbeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Probe indicates an expected call of Probe
func (mr *MockIdentityServerMockRecorder) Probe(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Probe", reflect.TypeOf((*MockIdentityServer)(nil).Probe), arg0, arg1)
}
// MockControllerServer is a mock of ControllerServer interface
type MockControllerServer struct {
ctrl *gomock.Controller
recorder *MockControllerServerMockRecorder
}
// MockControllerServerMockRecorder is the mock recorder for MockControllerServer
type MockControllerServerMockRecorder struct {
mock *MockControllerServer
}
// NewMockControllerServer creates a new mock instance
func NewMockControllerServer(ctrl *gomock.Controller) *MockControllerServer {
mock := &MockControllerServer{ctrl: ctrl}
mock.recorder = &MockControllerServerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockControllerServer) EXPECT() *MockControllerServerMockRecorder {
return m.recorder
}
// ControllerGetCapabilities mocks base method
func (m *MockControllerServer) ControllerGetCapabilities(arg0 context.Context, arg1 *v0.ControllerGetCapabilitiesRequest) (*v0.ControllerGetCapabilitiesResponse, error) {
ret := m.ctrl.Call(m, "ControllerGetCapabilities", arg0, arg1)
ret0, _ := ret[0].(*v0.ControllerGetCapabilitiesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ControllerGetCapabilities indicates an expected call of ControllerGetCapabilities
func (mr *MockControllerServerMockRecorder) ControllerGetCapabilities(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ControllerGetCapabilities", reflect.TypeOf((*MockControllerServer)(nil).ControllerGetCapabilities), arg0, arg1)
}
// ControllerPublishVolume mocks base method
func (m *MockControllerServer) ControllerPublishVolume(arg0 context.Context, arg1 *v0.ControllerPublishVolumeRequest) (*v0.ControllerPublishVolumeResponse, error) {
ret := m.ctrl.Call(m, "ControllerPublishVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.ControllerPublishVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ControllerPublishVolume indicates an expected call of ControllerPublishVolume
func (mr *MockControllerServerMockRecorder) ControllerPublishVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ControllerPublishVolume", reflect.TypeOf((*MockControllerServer)(nil).ControllerPublishVolume), arg0, arg1)
}
// ControllerUnpublishVolume mocks base method
func (m *MockControllerServer) ControllerUnpublishVolume(arg0 context.Context, arg1 *v0.ControllerUnpublishVolumeRequest) (*v0.ControllerUnpublishVolumeResponse, error) {
ret := m.ctrl.Call(m, "ControllerUnpublishVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.ControllerUnpublishVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ControllerUnpublishVolume indicates an expected call of ControllerUnpublishVolume
func (mr *MockControllerServerMockRecorder) ControllerUnpublishVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ControllerUnpublishVolume", reflect.TypeOf((*MockControllerServer)(nil).ControllerUnpublishVolume), arg0, arg1)
}
// CreateSnapshot mocks base method
func (m *MockControllerServer) CreateSnapshot(arg0 context.Context, arg1 *v0.CreateSnapshotRequest) (*v0.CreateSnapshotResponse, error) {
ret := m.ctrl.Call(m, "CreateSnapshot", arg0, arg1)
ret0, _ := ret[0].(*v0.CreateSnapshotResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSnapshot indicates an expected call of CreateSnapshot
func (mr *MockControllerServerMockRecorder) CreateSnapshot(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSnapshot", reflect.TypeOf((*MockControllerServer)(nil).CreateSnapshot), arg0, arg1)
}
// CreateVolume mocks base method
func (m *MockControllerServer) CreateVolume(arg0 context.Context, arg1 *v0.CreateVolumeRequest) (*v0.CreateVolumeResponse, error) {
ret := m.ctrl.Call(m, "CreateVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.CreateVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateVolume indicates an expected call of CreateVolume
func (mr *MockControllerServerMockRecorder) CreateVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolume", reflect.TypeOf((*MockControllerServer)(nil).CreateVolume), arg0, arg1)
}
// DeleteSnapshot mocks base method
func (m *MockControllerServer) DeleteSnapshot(arg0 context.Context, arg1 *v0.DeleteSnapshotRequest) (*v0.DeleteSnapshotResponse, error) {
ret := m.ctrl.Call(m, "DeleteSnapshot", arg0, arg1)
ret0, _ := ret[0].(*v0.DeleteSnapshotResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteSnapshot indicates an expected call of DeleteSnapshot
func (mr *MockControllerServerMockRecorder) DeleteSnapshot(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSnapshot", reflect.TypeOf((*MockControllerServer)(nil).DeleteSnapshot), arg0, arg1)
}
// DeleteVolume mocks base method
func (m *MockControllerServer) DeleteVolume(arg0 context.Context, arg1 *v0.DeleteVolumeRequest) (*v0.DeleteVolumeResponse, error) {
ret := m.ctrl.Call(m, "DeleteVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.DeleteVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteVolume indicates an expected call of DeleteVolume
func (mr *MockControllerServerMockRecorder) DeleteVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVolume", reflect.TypeOf((*MockControllerServer)(nil).DeleteVolume), arg0, arg1)
}
// GetCapacity mocks base method
func (m *MockControllerServer) GetCapacity(arg0 context.Context, arg1 *v0.GetCapacityRequest) (*v0.GetCapacityResponse, error) {
ret := m.ctrl.Call(m, "GetCapacity", arg0, arg1)
ret0, _ := ret[0].(*v0.GetCapacityResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCapacity indicates an expected call of GetCapacity
func (mr *MockControllerServerMockRecorder) GetCapacity(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCapacity", reflect.TypeOf((*MockControllerServer)(nil).GetCapacity), arg0, arg1)
}
// ListSnapshots mocks base method
func (m *MockControllerServer) ListSnapshots(arg0 context.Context, arg1 *v0.ListSnapshotsRequest) (*v0.ListSnapshotsResponse, error) {
ret := m.ctrl.Call(m, "ListSnapshots", arg0, arg1)
ret0, _ := ret[0].(*v0.ListSnapshotsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListSnapshots indicates an expected call of ListSnapshots
func (mr *MockControllerServerMockRecorder) ListSnapshots(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSnapshots", reflect.TypeOf((*MockControllerServer)(nil).ListSnapshots), arg0, arg1)
}
// ListVolumes mocks base method
func (m *MockControllerServer) ListVolumes(arg0 context.Context, arg1 *v0.ListVolumesRequest) (*v0.ListVolumesResponse, error) {
ret := m.ctrl.Call(m, "ListVolumes", arg0, arg1)
ret0, _ := ret[0].(*v0.ListVolumesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListVolumes indicates an expected call of ListVolumes
func (mr *MockControllerServerMockRecorder) ListVolumes(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVolumes", reflect.TypeOf((*MockControllerServer)(nil).ListVolumes), arg0, arg1)
}
// ValidateVolumeCapabilities mocks base method
func (m *MockControllerServer) ValidateVolumeCapabilities(arg0 context.Context, arg1 *v0.ValidateVolumeCapabilitiesRequest) (*v0.ValidateVolumeCapabilitiesResponse, error) {
ret := m.ctrl.Call(m, "ValidateVolumeCapabilities", arg0, arg1)
ret0, _ := ret[0].(*v0.ValidateVolumeCapabilitiesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ValidateVolumeCapabilities indicates an expected call of ValidateVolumeCapabilities
func (mr *MockControllerServerMockRecorder) ValidateVolumeCapabilities(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateVolumeCapabilities", reflect.TypeOf((*MockControllerServer)(nil).ValidateVolumeCapabilities), arg0, arg1)
}
// MockNodeServer is a mock of NodeServer interface
type MockNodeServer struct {
ctrl *gomock.Controller
recorder *MockNodeServerMockRecorder
}
// MockNodeServerMockRecorder is the mock recorder for MockNodeServer
type MockNodeServerMockRecorder struct {
mock *MockNodeServer
}
// NewMockNodeServer creates a new mock instance
func NewMockNodeServer(ctrl *gomock.Controller) *MockNodeServer {
mock := &MockNodeServer{ctrl: ctrl}
mock.recorder = &MockNodeServerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockNodeServer) EXPECT() *MockNodeServerMockRecorder {
return m.recorder
}
// NodeGetCapabilities mocks base method
func (m *MockNodeServer) NodeGetCapabilities(arg0 context.Context, arg1 *v0.NodeGetCapabilitiesRequest) (*v0.NodeGetCapabilitiesResponse, error) {
ret := m.ctrl.Call(m, "NodeGetCapabilities", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeGetCapabilitiesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeGetCapabilities indicates an expected call of NodeGetCapabilities
func (mr *MockNodeServerMockRecorder) NodeGetCapabilities(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeGetCapabilities", reflect.TypeOf((*MockNodeServer)(nil).NodeGetCapabilities), arg0, arg1)
}
// NodeGetId mocks base method
func (m *MockNodeServer) NodeGetId(arg0 context.Context, arg1 *v0.NodeGetIdRequest) (*v0.NodeGetIdResponse, error) {
ret := m.ctrl.Call(m, "NodeGetId", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeGetIdResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeGetId indicates an expected call of NodeGetId
func (mr *MockNodeServerMockRecorder) NodeGetId(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeGetId", reflect.TypeOf((*MockNodeServer)(nil).NodeGetId), arg0, arg1)
}
// NodeGetInfo mocks base method
func (m *MockNodeServer) NodeGetInfo(arg0 context.Context, arg1 *v0.NodeGetInfoRequest) (*v0.NodeGetInfoResponse, error) {
ret := m.ctrl.Call(m, "NodeGetInfo", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeGetInfoResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeGetInfo indicates an expected call of NodeGetInfo
func (mr *MockNodeServerMockRecorder) NodeGetInfo(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeGetInfo", reflect.TypeOf((*MockNodeServer)(nil).NodeGetInfo), arg0, arg1)
}
// NodePublishVolume mocks base method
func (m *MockNodeServer) NodePublishVolume(arg0 context.Context, arg1 *v0.NodePublishVolumeRequest) (*v0.NodePublishVolumeResponse, error) {
ret := m.ctrl.Call(m, "NodePublishVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.NodePublishVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodePublishVolume indicates an expected call of NodePublishVolume
func (mr *MockNodeServerMockRecorder) NodePublishVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodePublishVolume", reflect.TypeOf((*MockNodeServer)(nil).NodePublishVolume), arg0, arg1)
}
// NodeStageVolume mocks base method
func (m *MockNodeServer) NodeStageVolume(arg0 context.Context, arg1 *v0.NodeStageVolumeRequest) (*v0.NodeStageVolumeResponse, error) {
ret := m.ctrl.Call(m, "NodeStageVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeStageVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeStageVolume indicates an expected call of NodeStageVolume
func (mr *MockNodeServerMockRecorder) NodeStageVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeStageVolume", reflect.TypeOf((*MockNodeServer)(nil).NodeStageVolume), arg0, arg1)
}
// NodeUnpublishVolume mocks base method
func (m *MockNodeServer) NodeUnpublishVolume(arg0 context.Context, arg1 *v0.NodeUnpublishVolumeRequest) (*v0.NodeUnpublishVolumeResponse, error) {
ret := m.ctrl.Call(m, "NodeUnpublishVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeUnpublishVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeUnpublishVolume indicates an expected call of NodeUnpublishVolume
func (mr *MockNodeServerMockRecorder) NodeUnpublishVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeUnpublishVolume", reflect.TypeOf((*MockNodeServer)(nil).NodeUnpublishVolume), arg0, arg1)
}
// NodeUnstageVolume mocks base method
func (m *MockNodeServer) NodeUnstageVolume(arg0 context.Context, arg1 *v0.NodeUnstageVolumeRequest) (*v0.NodeUnstageVolumeResponse, error) {
ret := m.ctrl.Call(m, "NodeUnstageVolume", arg0, arg1)
ret0, _ := ret[0].(*v0.NodeUnstageVolumeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NodeUnstageVolume indicates an expected call of NodeUnstageVolume
func (mr *MockNodeServerMockRecorder) NodeUnstageVolume(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeUnstageVolume", reflect.TypeOf((*MockNodeServer)(nil).NodeUnstageVolume), arg0, arg1)
}

View File

@@ -0,0 +1,83 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"net"
"github.com/kubernetes-csi/csi-test/utils"
"google.golang.org/grpc"
)
type MockCSIDriverServers struct {
Controller *MockControllerServer
Identity *MockIdentityServer
Node *MockNodeServer
}
type MockCSIDriver struct {
CSIDriver
conn *grpc.ClientConn
}
func NewMockCSIDriver(servers *MockCSIDriverServers) *MockCSIDriver {
return &MockCSIDriver{
CSIDriver: CSIDriver{
servers: &CSIDriverServers{
Controller: servers.Controller,
Node: servers.Node,
Identity: servers.Identity,
},
},
}
}
func (m *MockCSIDriver) Start() error {
// Listen on a port assigned by the net package
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
if err := m.CSIDriver.Start(l); err != nil {
l.Close()
return err
}
return nil
}
func (m *MockCSIDriver) Nexus() (*grpc.ClientConn, error) {
// Start server
err := m.Start()
if err != nil {
return nil, err
}
// Create a client connection
m.conn, err = utils.Connect(m.Address())
if err != nil {
return nil, err
}
return m.conn, nil
}
func (m *MockCSIDriver) Close() {
m.conn.Close()
m.server.Stop()
}

50
vendor/github.com/kubernetes-csi/csi-test/hack/e2e.sh generated vendored Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
TESTARGS=$@
UDS="/tmp/e2e-csi-sanity.sock"
CSI_ENDPOINTS="$CSI_ENDPOINTS ${UDS}"
CSI_MOCK_VERSION="master"
#
# $1 - endpoint for mock.
# $2 - endpoint for csi-sanity in Grpc format.
# See https://github.com/grpc/grpc/blob/master/doc/naming.md
runTest()
{
CSI_ENDPOINT=$1 ./bin/mock &
local pid=$!
./cmd/csi-sanity/csi-sanity $TESTARGS --csi.endpoint=$2; ret=$?
kill -9 $pid
if [ $ret -ne 0 ] ; then
exit $ret
fi
}
runTestWithCreds()
{
CSI_ENDPOINT=$1 CSI_ENABLE_CREDS=true ./bin/mock &
local pid=$!
./cmd/csi-sanity/csi-sanity $TESTARGS --csi.endpoint=$2 --csi.secrets=mock/mocksecret.yaml; ret=$?
kill -9 $pid
if [ $ret -ne 0 ] ; then
exit $ret
fi
}
go build -o bin/mock ./mock || exit 1
cd cmd/csi-sanity
make clean install || exit 1
cd ../..
runTest "${UDS}" "${UDS}"
rm -f $UDS
runTestWithCreds "${UDS}" "${UDS}"
rm -f $UDS
exit 0

View File

@@ -0,0 +1,2 @@
TheCodeTeam
Kubernetes Authors

View File

@@ -0,0 +1,2 @@
# Mock CSI Driver
Extremely simple mock driver used to test `csi-sanity` based on `rexray/gocsi/mock`

View File

@@ -0,0 +1,89 @@
package cache
import (
"strings"
"sync"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
)
type SnapshotCache interface {
Add(snapshot Snapshot)
Delete(i int)
List(status csi.SnapshotStatus_Type) []csi.Snapshot
FindSnapshot(k, v string) (int, Snapshot)
}
type Snapshot struct {
Name string
Parameters map[string]string
SnapshotCSI csi.Snapshot
}
type snapshotCache struct {
snapshotsRWL sync.RWMutex
snapshots []Snapshot
}
func NewSnapshotCache() SnapshotCache {
return &snapshotCache{
snapshots: make([]Snapshot, 0),
}
}
func (snap *snapshotCache) Add(snapshot Snapshot) {
snap.snapshotsRWL.Lock()
defer snap.snapshotsRWL.Unlock()
snap.snapshots = append(snap.snapshots, snapshot)
}
func (snap *snapshotCache) Delete(i int) {
snap.snapshotsRWL.Lock()
defer snap.snapshotsRWL.Unlock()
copy(snap.snapshots[i:], snap.snapshots[i+1:])
snap.snapshots = snap.snapshots[:len(snap.snapshots)-1]
}
func (snap *snapshotCache) List(status csi.SnapshotStatus_Type) []csi.Snapshot {
snap.snapshotsRWL.RLock()
defer snap.snapshotsRWL.RUnlock()
snapshots := make([]csi.Snapshot, 0)
for _, v := range snap.snapshots {
if v.SnapshotCSI.GetStatus() != nil && v.SnapshotCSI.GetStatus().Type == status {
snapshots = append(snapshots, v.SnapshotCSI)
}
}
return snapshots
}
func (snap *snapshotCache) FindSnapshot(k, v string) (int, Snapshot) {
snap.snapshotsRWL.RLock()
defer snap.snapshotsRWL.RUnlock()
snapshotIdx := -1
for i, vi := range snap.snapshots {
switch k {
case "id":
if strings.EqualFold(v, vi.SnapshotCSI.Id) {
return i, vi
}
case "sourceVolumeId":
if strings.EqualFold(v, vi.SnapshotCSI.SourceVolumeId) {
return i, vi
}
case "name":
if vi.Name == v {
return i, vi
}
}
}
return snapshotIdx, Snapshot{}
}

88
vendor/github.com/kubernetes-csi/csi-test/mock/main.go generated vendored Normal file
View File

@@ -0,0 +1,88 @@
/*
Copyright 2018 Kubernetes Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"net"
"os"
"os/signal"
"strings"
"syscall"
"github.com/kubernetes-csi/csi-test/driver"
"github.com/kubernetes-csi/csi-test/mock/service"
)
func main() {
endpoint := os.Getenv("CSI_ENDPOINT")
if len(endpoint) == 0 {
fmt.Println("CSI_ENDPOINT must be defined and must be a path")
os.Exit(1)
}
if strings.Contains(endpoint, ":") {
fmt.Println("CSI_ENDPOINT must be a unix path")
os.Exit(1)
}
// Create mock driver
s := service.New()
servers := &driver.CSIDriverServers{
Controller: s,
Identity: s,
Node: s,
}
d := driver.NewCSIDriver(servers)
// If creds is enabled, set the default creds.
setCreds := os.Getenv("CSI_ENABLE_CREDS")
if len(setCreds) > 0 && setCreds == "true" {
d.SetDefaultCreds()
}
// Listen
os.Remove(endpoint)
l, err := net.Listen("unix", endpoint)
if err != nil {
fmt.Printf("Error: Unable to listen on %s socket: %v\n",
endpoint,
err)
os.Exit(1)
}
defer os.Remove(endpoint)
// Start server
if err := d.Start(l); err != nil {
fmt.Printf("Error: Unable to start mock CSI server: %v\n",
err)
os.Exit(1)
}
fmt.Println("mock driver started")
// Wait for signal
sigc := make(chan os.Signal, 1)
sigs := []os.Signal{
syscall.SIGTERM,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGQUIT,
}
signal.Notify(sigc, sigs...)
<-sigc
d.Stop()
fmt.Println("mock driver stopped")
}

View File

@@ -0,0 +1,16 @@
CreateVolumeSecret:
secretKey: secretval1
DeleteVolumeSecret:
secretKey: secretval2
ControllerPublishVolumeSecret:
secretKey: secretval3
ControllerUnpublishVolumeSecret:
secretKey: secretval4
NodeStageVolumeSecret:
secretKey: secretval5
NodePublishVolumeSecret:
secretKey: secretval6
CreateSnapshotSecret:
secretKey: secretval7
DeleteSnapshotSecret:
secretKey: secretval8

View File

@@ -0,0 +1,559 @@
package service
import (
"fmt"
"math"
"path"
"reflect"
"strconv"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
)
const (
MaxStorageCapacity = tib
ReadOnlyKey = "readonly"
)
func (s *service) CreateVolume(
ctx context.Context,
req *csi.CreateVolumeRequest) (
*csi.CreateVolumeResponse, error) {
if len(req.Name) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume Name cannot be empty")
}
if req.VolumeCapabilities == nil {
return nil, status.Error(codes.InvalidArgument, "Volume Capabilities cannot be empty")
}
// Check to see if the volume already exists.
if i, v := s.findVolByName(ctx, req.Name); i >= 0 {
// Requested volume name already exists, need to check if the existing volume's
// capacity is more or equal to new request's capacity.
if v.GetCapacityBytes() < req.GetCapacityRange().GetRequiredBytes() {
return nil, status.Error(codes.AlreadyExists,
fmt.Sprintf("Volume with name %s already exists", req.GetName()))
}
return &csi.CreateVolumeResponse{Volume: &v}, nil
}
// If no capacity is specified then use 100GiB
capacity := gib100
if cr := req.CapacityRange; cr != nil {
if rb := cr.RequiredBytes; rb > 0 {
capacity = rb
}
if lb := cr.LimitBytes; lb > 0 {
capacity = lb
}
}
// Check for maximum available capacity
if capacity >= MaxStorageCapacity {
return nil, status.Errorf(codes.OutOfRange, "Requested capacity %d exceeds maximum allowed %d", capacity, MaxStorageCapacity)
}
// Create the volume and add it to the service's in-mem volume slice.
v := s.newVolume(req.Name, capacity)
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
s.vols = append(s.vols, v)
MockVolumes[v.Id] = Volume{
VolumeCSI: v,
NodeID: "",
ISStaged: false,
ISPublished: false,
StageTargetPath: "",
TargetPath: "",
}
return &csi.CreateVolumeResponse{Volume: &v}, nil
}
func (s *service) DeleteVolume(
ctx context.Context,
req *csi.DeleteVolumeRequest) (
*csi.DeleteVolumeResponse, error) {
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
// If the volume is not specified, return error
if len(req.VolumeId) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
// If the volume does not exist then return an idempotent response.
i, _ := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return &csi.DeleteVolumeResponse{}, nil
}
// This delete logic preserves order and prevents potential memory
// leaks. The slice's elements may not be pointers, but the structs
// themselves have fields that are.
copy(s.vols[i:], s.vols[i+1:])
s.vols[len(s.vols)-1] = csi.Volume{}
s.vols = s.vols[:len(s.vols)-1]
log.WithField("volumeID", req.VolumeId).Debug("mock delete volume")
return &csi.DeleteVolumeResponse{}, nil
}
func (s *service) ControllerPublishVolume(
ctx context.Context,
req *csi.ControllerPublishVolumeRequest) (
*csi.ControllerPublishVolumeResponse, error) {
if len(req.VolumeId) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.NodeId) == 0 {
return nil, status.Error(codes.InvalidArgument, "Node ID cannot be empty")
}
if req.VolumeCapability == nil {
return nil, status.Error(codes.InvalidArgument, "Volume Capabilities cannot be empty")
}
if req.NodeId != s.nodeID {
return nil, status.Errorf(codes.NotFound, "Not matching Node ID %s to Mock Node ID %s", req.NodeId, s.nodeID)
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// devPathKey is the key in the volume's attributes that is set to a
// mock device path if the volume has been published by the controller
// to the specified node.
devPathKey := path.Join(req.NodeId, "dev")
// Check to see if the volume is already published.
if device := v.Attributes[devPathKey]; device != "" {
var volRo bool
var roVal string
if ro, ok := v.Attributes[ReadOnlyKey]; ok {
roVal = ro
}
if roVal == "true" {
volRo = true
} else {
volRo = false
}
// Check if readonly flag is compatible with the publish request.
if req.GetReadonly() != volRo {
return nil, status.Error(codes.AlreadyExists, "Volume published but has incompatible readonly flag")
}
return &csi.ControllerPublishVolumeResponse{
PublishInfo: map[string]string{
"device": device,
"readonly": roVal,
},
}, nil
}
var roVal string
if req.GetReadonly() {
roVal = "true"
} else {
roVal = "false"
}
// Publish the volume.
device := "/dev/mock"
v.Attributes[devPathKey] = device
v.Attributes[ReadOnlyKey] = roVal
s.vols[i] = v
return &csi.ControllerPublishVolumeResponse{
PublishInfo: map[string]string{
"device": device,
"readonly": roVal,
},
}, nil
}
func (s *service) ControllerUnpublishVolume(
ctx context.Context,
req *csi.ControllerUnpublishVolumeRequest) (
*csi.ControllerUnpublishVolumeResponse, error) {
if len(req.VolumeId) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
nodeID := req.NodeId
if len(nodeID) == 0 {
// If node id is empty, no failure as per Spec
nodeID = s.nodeID
}
if req.NodeId != s.nodeID {
return nil, status.Errorf(codes.NotFound, "Node ID %s does not match to expected Node ID %s", req.NodeId, s.nodeID)
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// devPathKey is the key in the volume's attributes that is set to a
// mock device path if the volume has been published by the controller
// to the specified node.
devPathKey := path.Join(nodeID, "dev")
// Check to see if the volume is already unpublished.
if v.Attributes[devPathKey] == "" {
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
// Unpublish the volume.
delete(v.Attributes, devPathKey)
delete(v.Attributes, ReadOnlyKey)
s.vols[i] = v
return &csi.ControllerUnpublishVolumeResponse{}, nil
}
func (s *service) ValidateVolumeCapabilities(
ctx context.Context,
req *csi.ValidateVolumeCapabilitiesRequest) (
*csi.ValidateVolumeCapabilitiesResponse, error) {
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.VolumeCapabilities) == 0 {
return nil, status.Error(codes.InvalidArgument, req.VolumeId)
}
i, _ := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
return &csi.ValidateVolumeCapabilitiesResponse{
Supported: true,
}, nil
}
func (s *service) ListVolumes(
ctx context.Context,
req *csi.ListVolumesRequest) (
*csi.ListVolumesResponse, error) {
// Copy the mock volumes into a new slice in order to avoid
// locking the service's volume slice for the duration of the
// ListVolumes RPC.
var vols []csi.Volume
func() {
s.volsRWL.RLock()
defer s.volsRWL.RUnlock()
vols = make([]csi.Volume, len(s.vols))
copy(vols, s.vols)
}()
var (
ulenVols = int32(len(vols))
maxEntries = req.MaxEntries
startingToken int32
)
if v := req.StartingToken; v != "" {
i, err := strconv.ParseUint(v, 10, 32)
if err != nil {
return nil, status.Errorf(
codes.InvalidArgument,
"startingToken=%d !< int32=%d",
startingToken, math.MaxUint32)
}
startingToken = int32(i)
}
if startingToken > ulenVols {
return nil, status.Errorf(
codes.InvalidArgument,
"startingToken=%d > len(vols)=%d",
startingToken, ulenVols)
}
// Discern the number of remaining entries.
rem := ulenVols - startingToken
// If maxEntries is 0 or greater than the number of remaining entries then
// set maxEntries to the number of remaining entries.
if maxEntries == 0 || maxEntries > rem {
maxEntries = rem
}
var (
i int
j = startingToken
entries = make(
[]*csi.ListVolumesResponse_Entry,
maxEntries)
)
for i = 0; i < len(entries); i++ {
entries[i] = &csi.ListVolumesResponse_Entry{
Volume: &vols[j],
}
j++
}
var nextToken string
if n := startingToken + int32(i); n < ulenVols {
nextToken = fmt.Sprintf("%d", n)
}
return &csi.ListVolumesResponse{
Entries: entries,
NextToken: nextToken,
}, nil
}
func (s *service) GetCapacity(
ctx context.Context,
req *csi.GetCapacityRequest) (
*csi.GetCapacityResponse, error) {
return &csi.GetCapacityResponse{
AvailableCapacity: MaxStorageCapacity,
}, nil
}
func (s *service) ControllerGetCapabilities(
ctx context.Context,
req *csi.ControllerGetCapabilitiesRequest) (
*csi.ControllerGetCapabilitiesResponse, error) {
return &csi.ControllerGetCapabilitiesResponse{
Capabilities: []*csi.ControllerServiceCapability{
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
},
},
},
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
},
},
},
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
},
},
},
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_GET_CAPACITY,
},
},
},
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_LIST_SNAPSHOTS,
},
},
},
{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,
},
},
},
},
}, nil
}
func (s *service) CreateSnapshot(ctx context.Context,
req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
// Check arguments
if len(req.GetName()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Snapshot Name cannot be empty")
}
if len(req.GetSourceVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Snapshot SourceVolumeId cannot be empty")
}
// Check to see if the snapshot already exists.
if i, v := s.snapshots.FindSnapshot("name", req.GetName()); i >= 0 {
// Requested snapshot name already exists
if v.SnapshotCSI.GetSourceVolumeId() != req.GetSourceVolumeId() || !reflect.DeepEqual(v.Parameters, req.GetParameters()) {
return nil, status.Error(codes.AlreadyExists,
fmt.Sprintf("Snapshot with name %s already exists", req.GetName()))
}
return &csi.CreateSnapshotResponse{Snapshot: &v.SnapshotCSI}, nil
}
// Create the snapshot and add it to the service's in-mem snapshot slice.
snapshot := s.newSnapshot(req.GetName(), req.GetSourceVolumeId(), req.GetParameters())
s.snapshots.Add(snapshot)
return &csi.CreateSnapshotResponse{Snapshot: &snapshot.SnapshotCSI}, nil
}
func (s *service) DeleteSnapshot(ctx context.Context,
req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
// If the snapshot is not specified, return error
if len(req.SnapshotId) == 0 {
return nil, status.Error(codes.InvalidArgument, "Snapshot ID cannot be empty")
}
// If the snapshot does not exist then return an idempotent response.
i, _ := s.snapshots.FindSnapshot("id", req.SnapshotId)
if i < 0 {
return &csi.DeleteSnapshotResponse{}, nil
}
// This delete logic preserves order and prevents potential memory
// leaks. The slice's elements may not be pointers, but the structs
// themselves have fields that are.
s.snapshots.Delete(i)
log.WithField("SnapshotId", req.SnapshotId).Debug("mock delete snapshot")
return &csi.DeleteSnapshotResponse{}, nil
}
func (s *service) ListSnapshots(ctx context.Context,
req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
// case 1: SnapshotId is not empty, return snapshots that match the snapshot id.
if len(req.GetSnapshotId()) != 0 {
return getSnapshotById(s, req)
}
// case 2: SourceVolumeId is not empty, return snapshots that match the source volume id.
if len(req.GetSourceVolumeId()) != 0 {
return getSnapshotByVolumeId(s, req)
}
// case 3: no parameter is set, so we return all the snapshots.
return getAllSnapshots(s, req)
}
func getSnapshotById(s *service, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
if len(req.GetSnapshotId()) != 0 {
i, snapshot := s.snapshots.FindSnapshot("id", req.GetSnapshotId())
if i < 0 {
return &csi.ListSnapshotsResponse{}, nil
}
if len(req.GetSourceVolumeId()) != 0 {
if snapshot.SnapshotCSI.GetSourceVolumeId() != req.GetSourceVolumeId() {
return &csi.ListSnapshotsResponse{}, nil
}
}
return &csi.ListSnapshotsResponse{
Entries: []*csi.ListSnapshotsResponse_Entry{
{
Snapshot: &snapshot.SnapshotCSI,
},
},
}, nil
}
return nil, nil
}
func getSnapshotByVolumeId(s *service, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
if len(req.GetSourceVolumeId()) != 0 {
i, snapshot := s.snapshots.FindSnapshot("sourceVolumeId", req.SourceVolumeId)
if i < 0 {
return &csi.ListSnapshotsResponse{}, nil
}
return &csi.ListSnapshotsResponse{
Entries: []*csi.ListSnapshotsResponse_Entry{
{
Snapshot: &snapshot.SnapshotCSI,
},
},
}, nil
}
return nil, nil
}
func getAllSnapshots(s *service, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
// Copy the mock snapshots into a new slice in order to avoid
// locking the service's snapshot slice for the duration of the
// ListSnapshots RPC.
snapshots := s.snapshots.List(csi.SnapshotStatus_READY)
var (
ulenSnapshots = int32(len(snapshots))
maxEntries = req.MaxEntries
startingToken int32
)
if v := req.StartingToken; v != "" {
i, err := strconv.ParseUint(v, 10, 32)
if err != nil {
return nil, status.Errorf(
codes.Aborted,
"startingToken=%d !< int32=%d",
startingToken, math.MaxUint32)
}
startingToken = int32(i)
}
if startingToken > ulenSnapshots {
return nil, status.Errorf(
codes.Aborted,
"startingToken=%d > len(snapshots)=%d",
startingToken, ulenSnapshots)
}
// Discern the number of remaining entries.
rem := ulenSnapshots - startingToken
// If maxEntries is 0 or greater than the number of remaining entries then
// set maxEntries to the number of remaining entries.
if maxEntries == 0 || maxEntries > rem {
maxEntries = rem
}
var (
i int
j = startingToken
entries = make(
[]*csi.ListSnapshotsResponse_Entry,
maxEntries)
)
for i = 0; i < len(entries); i++ {
entries[i] = &csi.ListSnapshotsResponse_Entry{
Snapshot: &snapshots[j],
}
j++
}
var nextToken string
if n := startingToken + int32(i); n < ulenSnapshots {
nextToken = fmt.Sprintf("%d", n)
}
return &csi.ListSnapshotsResponse{
Entries: entries,
NextToken: nextToken,
}, nil
}

View File

@@ -0,0 +1,48 @@
package service
import (
"golang.org/x/net/context"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
"github.com/golang/protobuf/ptypes/wrappers"
)
func (s *service) GetPluginInfo(
ctx context.Context,
req *csi.GetPluginInfoRequest) (
*csi.GetPluginInfoResponse, error) {
return &csi.GetPluginInfoResponse{
Name: Name,
VendorVersion: VendorVersion,
Manifest: Manifest,
}, nil
}
func (s *service) Probe(
ctx context.Context,
req *csi.ProbeRequest) (
*csi.ProbeResponse, error) {
return &csi.ProbeResponse{
Ready: &wrappers.BoolValue{Value: true},
}, nil
}
func (s *service) GetPluginCapabilities(
ctx context.Context,
req *csi.GetPluginCapabilitiesRequest) (
*csi.GetPluginCapabilitiesResponse, error) {
return &csi.GetPluginCapabilitiesResponse{
Capabilities: []*csi.PluginCapability{
{
Type: &csi.PluginCapability_Service_{
Service: &csi.PluginCapability_Service{
Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
},
},
},
},
}, nil
}

View File

@@ -0,0 +1,236 @@
package service
import (
"path"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"golang.org/x/net/context"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
)
func (s *service) NodeStageVolume(
ctx context.Context,
req *csi.NodeStageVolumeRequest) (
*csi.NodeStageVolumeResponse, error) {
device, ok := req.PublishInfo["device"]
if !ok {
return nil, status.Error(
codes.InvalidArgument,
"stage volume info 'device' key required")
}
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.GetStagingTargetPath()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Staging Target Path cannot be empty")
}
if req.GetVolumeCapability() == nil {
return nil, status.Error(codes.InvalidArgument, "Volume Capability cannot be empty")
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// nodeStgPathKey is the key in the volume's attributes that is set to a
// mock stage path if the volume has been published by the node
nodeStgPathKey := path.Join(s.nodeID, req.StagingTargetPath)
// Check to see if the volume has already been staged.
if v.Attributes[nodeStgPathKey] != "" {
// TODO: Check for the capabilities to be equal. Return "ALREADY_EXISTS"
// if the capabilities don't match.
return &csi.NodeStageVolumeResponse{}, nil
}
// Stage the volume.
v.Attributes[nodeStgPathKey] = device
s.vols[i] = v
return &csi.NodeStageVolumeResponse{}, nil
}
func (s *service) NodeUnstageVolume(
ctx context.Context,
req *csi.NodeUnstageVolumeRequest) (
*csi.NodeUnstageVolumeResponse, error) {
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.GetStagingTargetPath()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Staging Target Path cannot be empty")
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// nodeStgPathKey is the key in the volume's attributes that is set to a
// mock stage path if the volume has been published by the node
nodeStgPathKey := path.Join(s.nodeID, req.StagingTargetPath)
// Check to see if the volume has already been unstaged.
if v.Attributes[nodeStgPathKey] == "" {
return &csi.NodeUnstageVolumeResponse{}, nil
}
// Unpublish the volume.
delete(v.Attributes, nodeStgPathKey)
s.vols[i] = v
return &csi.NodeUnstageVolumeResponse{}, nil
}
func (s *service) NodePublishVolume(
ctx context.Context,
req *csi.NodePublishVolumeRequest) (
*csi.NodePublishVolumeResponse, error) {
device, ok := req.PublishInfo["device"]
if !ok {
return nil, status.Error(
codes.InvalidArgument,
"publish volume info 'device' key required")
}
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.GetTargetPath()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Target Path cannot be empty")
}
if req.GetVolumeCapability() == nil {
return nil, status.Error(codes.InvalidArgument, "Volume Capability cannot be empty")
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// nodeMntPathKey is the key in the volume's attributes that is set to a
// mock mount path if the volume has been published by the node
nodeMntPathKey := path.Join(s.nodeID, req.TargetPath)
// Check to see if the volume has already been published.
if v.Attributes[nodeMntPathKey] != "" {
// Requests marked Readonly fail due to volumes published by
// the Mock driver supporting only RW mode.
if req.Readonly {
return nil, status.Error(codes.AlreadyExists, req.VolumeId)
}
return &csi.NodePublishVolumeResponse{}, nil
}
// Publish the volume.
if req.GetStagingTargetPath() != "" {
v.Attributes[nodeMntPathKey] = req.GetStagingTargetPath()
} else {
v.Attributes[nodeMntPathKey] = device
}
s.vols[i] = v
return &csi.NodePublishVolumeResponse{}, nil
}
func (s *service) NodeUnpublishVolume(
ctx context.Context,
req *csi.NodeUnpublishVolumeRequest) (
*csi.NodeUnpublishVolumeResponse, error) {
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID cannot be empty")
}
if len(req.GetTargetPath()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Target Path cannot be empty")
}
s.volsRWL.Lock()
defer s.volsRWL.Unlock()
i, v := s.findVolNoLock("id", req.VolumeId)
if i < 0 {
return nil, status.Error(codes.NotFound, req.VolumeId)
}
// nodeMntPathKey is the key in the volume's attributes that is set to a
// mock mount path if the volume has been published by the node
nodeMntPathKey := path.Join(s.nodeID, req.TargetPath)
// Check to see if the volume has already been unpublished.
if v.Attributes[nodeMntPathKey] == "" {
return &csi.NodeUnpublishVolumeResponse{}, nil
}
// Unpublish the volume.
delete(v.Attributes, nodeMntPathKey)
s.vols[i] = v
return &csi.NodeUnpublishVolumeResponse{}, nil
}
func (s *service) NodeGetId(
ctx context.Context,
req *csi.NodeGetIdRequest) (
*csi.NodeGetIdResponse, error) {
return &csi.NodeGetIdResponse{
NodeId: s.nodeID,
}, nil
}
func (s *service) NodeGetCapabilities(
ctx context.Context,
req *csi.NodeGetCapabilitiesRequest) (
*csi.NodeGetCapabilitiesResponse, error) {
return &csi.NodeGetCapabilitiesResponse{
Capabilities: []*csi.NodeServiceCapability{
{
Type: &csi.NodeServiceCapability_Rpc{
Rpc: &csi.NodeServiceCapability_RPC{
Type: csi.NodeServiceCapability_RPC_UNKNOWN,
},
},
},
{
Type: &csi.NodeServiceCapability_Rpc{
Rpc: &csi.NodeServiceCapability_RPC{
Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
},
},
},
},
}, nil
}
func (s *service) NodeGetInfo(ctx context.Context,
req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
return &csi.NodeGetInfoResponse{
NodeId: s.nodeID,
}, nil
}

View File

@@ -0,0 +1,137 @@
package service
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
"github.com/kubernetes-csi/csi-test/mock/cache"
"golang.org/x/net/context"
)
const (
// Name is the name of the CSI plug-in.
Name = "io.kubernetes.storage.mock"
// VendorVersion is the version returned by GetPluginInfo.
VendorVersion = "0.3.0"
)
// Manifest is the SP's manifest.
var Manifest = map[string]string{
"url": "https://github.com/kubernetes-csi/csi-test/mock",
}
// Service is the CSI Mock service provider.
type Service interface {
csi.ControllerServer
csi.IdentityServer
csi.NodeServer
}
type service struct {
sync.Mutex
nodeID string
vols []csi.Volume
volsRWL sync.RWMutex
volsNID uint64
snapshots cache.SnapshotCache
snapshotsNID uint64
}
type Volume struct {
sync.Mutex
VolumeCSI csi.Volume
NodeID string
ISStaged bool
ISPublished bool
StageTargetPath string
TargetPath string
}
var MockVolumes map[string]Volume
// New returns a new Service.
func New() Service {
s := &service{nodeID: Name}
s.snapshots = cache.NewSnapshotCache()
s.vols = []csi.Volume{
s.newVolume("Mock Volume 1", gib100),
s.newVolume("Mock Volume 2", gib100),
s.newVolume("Mock Volume 3", gib100),
}
MockVolumes = map[string]Volume{}
s.snapshots.Add(s.newSnapshot("Mock Snapshot 1", "1", map[string]string{"Description": "snapshot 1"}))
s.snapshots.Add(s.newSnapshot("Mock Snapshot 2", "2", map[string]string{"Description": "snapshot 2"}))
s.snapshots.Add(s.newSnapshot("Mock Snapshot 3", "3", map[string]string{"Description": "snapshot 3"}))
return s
}
const (
kib int64 = 1024
mib int64 = kib * 1024
gib int64 = mib * 1024
gib100 int64 = gib * 100
tib int64 = gib * 1024
tib100 int64 = tib * 100
)
func (s *service) newVolume(name string, capcity int64) csi.Volume {
return csi.Volume{
Id: fmt.Sprintf("%d", atomic.AddUint64(&s.volsNID, 1)),
Attributes: map[string]string{"name": name},
CapacityBytes: capcity,
}
}
func (s *service) findVol(k, v string) (volIdx int, volInfo csi.Volume) {
s.volsRWL.RLock()
defer s.volsRWL.RUnlock()
return s.findVolNoLock(k, v)
}
func (s *service) findVolNoLock(k, v string) (volIdx int, volInfo csi.Volume) {
volIdx = -1
for i, vi := range s.vols {
switch k {
case "id":
if strings.EqualFold(v, vi.Id) {
return i, vi
}
case "name":
if n, ok := vi.Attributes["name"]; ok && strings.EqualFold(v, n) {
return i, vi
}
}
}
return
}
func (s *service) findVolByName(
ctx context.Context, name string) (int, csi.Volume) {
return s.findVol("name", name)
}
func (s *service) newSnapshot(name, sourceVolumeId string, parameters map[string]string) cache.Snapshot {
return cache.Snapshot{
Name: name,
Parameters: parameters,
SnapshotCSI: csi.Snapshot{
Id: fmt.Sprintf("%d", atomic.AddUint64(&s.snapshotsNID, 1)),
CreatedAt: time.Now().UnixNano(),
SourceVolumeId: sourceVolumeId,
Status: &csi.SnapshotStatus{
Type: csi.SnapshotStatus_READY,
Details: "snapshot ready",
},
},
}
}

View File

@@ -0,0 +1,62 @@
# CSI Driver Sanity Tester
This library provides a simple way to ensure that a CSI driver conforms to
the CSI specification. There are two ways to leverage this testing framework.
For CSI drivers written in Golang, the framework provides a simple API function
to call to test the driver. Another way to run the test suite is to use the
command line program [csi-sanity](https://github.com/kubernetes-csi/csi-test/tree/master/cmd/csi-sanity).
## For Golang CSI Drivers
This framework leverages the Ginkgo BDD testing framework to deliver a descriptive
test suite for your driver. To test your driver, simply call the API in one of your
Golang `TestXXX` functions. For example:
```go
func TestMyDriver(t *testing.T) {
// Setup the full driver and its environment
... setup driver ...
config := &sanity.Config{
TargetPath: ...
StagingPath: ...
Address: endpoint,
}
// Now call the test suite
sanity.Test(t, config)
}
```
Only one such test function is supported because under the hood a
Ginkgo test suite gets constructed and executed by the call.
Alternatively, the tests can also be embedded inside a Ginkgo test
suite. In that case it is possible to define multiple tests with
different configurations:
```go
var _ = Describe("MyCSIDriver", func () {
Context("Config A", func () {
var config &sanity.Config
BeforeEach() {
... setup driver and config...
}
AfterEach() {
...tear down driver...
}
Describe("CSI sanity", func() {
sanity.GinkgoTest(config)
})
})
Context("Config B", func () {
...
})
})
```
## Command line program
Please see [csi-sanity](https://github.com/kubernetes-csi/csi-test/tree/master/cmd/csi-sanity)

View File

@@ -0,0 +1,134 @@
/*
Copyright 2018 Intel Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
"context"
"log"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
. "github.com/onsi/ginkgo"
)
// VolumeInfo keeps track of the information needed to delete a volume.
type VolumeInfo struct {
// Node on which the volume was published, empty if none
// or publishing is not supported.
NodeID string
// Volume ID assigned by CreateVolume.
VolumeID string
}
// Cleanup keeps track of resources, in particular volumes, which need
// to be freed when testing is done.
type Cleanup struct {
Context *SanityContext
ControllerClient csi.ControllerClient
NodeClient csi.NodeClient
ControllerPublishSupported bool
NodeStageSupported bool
// Maps from volume name to the node ID for which the volume
// is published and the volume ID.
volumes map[string]VolumeInfo
}
// RegisterVolume adds or updates an entry for the volume with the
// given name.
func (cl *Cleanup) RegisterVolume(name string, info VolumeInfo) {
if cl.volumes == nil {
cl.volumes = make(map[string]VolumeInfo)
}
cl.volumes[name] = info
}
// MaybeRegisterVolume adds or updates an entry for the volume with
// the given name if CreateVolume was successful.
func (cl *Cleanup) MaybeRegisterVolume(name string, vol *csi.CreateVolumeResponse, err error) {
if err == nil && vol.GetVolume().GetId() != "" {
cl.RegisterVolume(name, VolumeInfo{VolumeID: vol.GetVolume().GetId()})
}
}
// UnregisterVolume removes the entry for the volume with the
// given name, thus preventing all cleanup operations for it.
func (cl *Cleanup) UnregisterVolume(name string) {
if cl.volumes != nil {
delete(cl.volumes, name)
}
}
// DeleteVolumes stops using the registered volumes and tries to delete all of them.
func (cl *Cleanup) DeleteVolumes() {
if cl.volumes == nil {
return
}
logger := log.New(GinkgoWriter, "cleanup: ", 0)
ctx := context.Background()
for name, info := range cl.volumes {
logger.Printf("deleting %s = %s", name, info.VolumeID)
if _, err := cl.NodeClient.NodeUnpublishVolume(
ctx,
&csi.NodeUnpublishVolumeRequest{
VolumeId: info.VolumeID,
TargetPath: cl.Context.Config.TargetPath,
},
); err != nil {
logger.Printf("warning: NodeUnpublishVolume: %s", err)
}
if cl.NodeStageSupported {
if _, err := cl.NodeClient.NodeUnstageVolume(
ctx,
&csi.NodeUnstageVolumeRequest{
VolumeId: info.VolumeID,
StagingTargetPath: cl.Context.Config.StagingPath,
},
); err != nil {
logger.Printf("warning: NodeUnstageVolume: %s", err)
}
}
if cl.ControllerPublishSupported && info.NodeID != "" {
if _, err := cl.ControllerClient.ControllerUnpublishVolume(
ctx,
&csi.ControllerUnpublishVolumeRequest{
VolumeId: info.VolumeID,
NodeId: info.NodeID,
ControllerUnpublishSecrets: cl.Context.Secrets.ControllerUnpublishVolumeSecret,
},
); err != nil {
logger.Printf("warning: ControllerUnpublishVolume: %s", err)
}
}
if _, err := cl.ControllerClient.DeleteVolume(
ctx,
&csi.DeleteVolumeRequest{
VolumeId: info.VolumeID,
ControllerDeleteSecrets: cl.Context.Secrets.DeleteVolumeSecret,
},
); err != nil {
logger.Printf("error: DeleteVolume: %s", err)
}
cl.UnregisterVolume(name)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
"context"
"fmt"
"regexp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = DescribeSanity("Identity Service", func(sc *SanityContext) {
var (
c csi.IdentityClient
)
BeforeEach(func() {
c = csi.NewIdentityClient(sc.Conn)
})
Describe("GetPluginCapabilities", func() {
It("should return appropriate capabilities", func() {
req := &csi.GetPluginCapabilitiesRequest{}
res, err := c.GetPluginCapabilities(context.Background(), req)
Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())
By("checking successful response")
Expect(res.GetCapabilities()).NotTo(BeNil())
for _, cap := range res.GetCapabilities() {
switch cap.GetService().GetType() {
case csi.PluginCapability_Service_CONTROLLER_SERVICE:
case csi.PluginCapability_Service_ACCESSIBILITY_CONSTRAINTS:
default:
Fail(fmt.Sprintf("Unknown capability: %v\n", cap.GetService().GetType()))
}
}
})
})
Describe("Probe", func() {
It("should return appropriate information", func() {
req := &csi.ProbeRequest{}
res, err := c.Probe(context.Background(), req)
Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())
By("verifying return status")
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code() == codes.FailedPrecondition ||
serverError.Code() == codes.OK).To(BeTrue())
if res.GetReady() != nil {
Expect(res.GetReady().GetValue() == true ||
res.GetReady().GetValue() == false).To(BeTrue())
}
})
})
Describe("GetPluginInfo", func() {
It("should return appropriate information", func() {
req := &csi.GetPluginInfoRequest{}
res, err := c.GetPluginInfo(context.Background(), req)
Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())
By("verifying name size and characters")
Expect(res.GetName()).ToNot(HaveLen(0))
Expect(len(res.GetName())).To(BeNumerically("<=", 63))
Expect(regexp.
MustCompile("^[a-zA-Z][A-Za-z0-9-\\.\\_]{0,61}[a-zA-Z]$").
MatchString(res.GetName())).To(BeTrue())
})
})
})

View File

@@ -0,0 +1,528 @@
/*
Copyright 2017 Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func isNodeCapabilitySupported(c csi.NodeClient,
capType csi.NodeServiceCapability_RPC_Type,
) bool {
caps, err := c.NodeGetCapabilities(
context.Background(),
&csi.NodeGetCapabilitiesRequest{})
Expect(err).NotTo(HaveOccurred())
Expect(caps).NotTo(BeNil())
for _, cap := range caps.GetCapabilities() {
Expect(cap.GetRpc()).NotTo(BeNil())
if cap.GetRpc().GetType() == capType {
return true
}
}
return false
}
func isPluginCapabilitySupported(c csi.IdentityClient,
capType csi.PluginCapability_Service_Type,
) bool {
caps, err := c.GetPluginCapabilities(
context.Background(),
&csi.GetPluginCapabilitiesRequest{})
Expect(err).NotTo(HaveOccurred())
Expect(caps).NotTo(BeNil())
Expect(caps.GetCapabilities()).NotTo(BeNil())
for _, cap := range caps.GetCapabilities() {
Expect(cap.GetService()).NotTo(BeNil())
if cap.GetService().GetType() == capType {
return true
}
}
return false
}
var _ = DescribeSanity("Node Service", func(sc *SanityContext) {
var (
cl *Cleanup
c csi.NodeClient
s csi.ControllerClient
controllerPublishSupported bool
nodeStageSupported bool
)
BeforeEach(func() {
c = csi.NewNodeClient(sc.Conn)
s = csi.NewControllerClient(sc.Conn)
controllerPublishSupported = isControllerCapabilitySupported(
s,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME)
nodeStageSupported = isNodeCapabilitySupported(c, csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME)
if nodeStageSupported {
err := createMountTargetLocation(sc.Config.StagingPath)
Expect(err).NotTo(HaveOccurred())
}
cl = &Cleanup{
Context: sc,
NodeClient: c,
ControllerClient: s,
ControllerPublishSupported: controllerPublishSupported,
NodeStageSupported: nodeStageSupported,
}
})
AfterEach(func() {
cl.DeleteVolumes()
})
Describe("NodeGetCapabilities", func() {
It("should return appropriate capabilities", func() {
caps, err := c.NodeGetCapabilities(
context.Background(),
&csi.NodeGetCapabilitiesRequest{})
By("checking successful response")
Expect(err).NotTo(HaveOccurred())
Expect(caps).NotTo(BeNil())
for _, cap := range caps.GetCapabilities() {
Expect(cap.GetRpc()).NotTo(BeNil())
switch cap.GetRpc().GetType() {
case csi.NodeServiceCapability_RPC_UNKNOWN:
case csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME:
default:
Fail(fmt.Sprintf("Unknown capability: %v\n", cap.GetRpc().GetType()))
}
}
})
})
Describe("NodeGetId", func() {
It("should return appropriate values", func() {
nid, err := c.NodeGetId(
context.Background(),
&csi.NodeGetIdRequest{})
Expect(err).NotTo(HaveOccurred())
Expect(nid).NotTo(BeNil())
Expect(nid.GetNodeId()).NotTo(BeEmpty())
})
})
Describe("NodeGetInfo", func() {
var (
i csi.IdentityClient
accessibilityConstraintSupported bool
)
BeforeEach(func() {
i = csi.NewIdentityClient(sc.Conn)
accessibilityConstraintSupported = isPluginCapabilitySupported(i, csi.PluginCapability_Service_ACCESSIBILITY_CONSTRAINTS)
})
It("should return approproate values", func() {
ninfo, err := c.NodeGetInfo(
context.Background(),
&csi.NodeGetInfoRequest{})
Expect(err).NotTo(HaveOccurred())
Expect(ninfo).NotTo(BeNil())
Expect(ninfo.GetNodeId()).NotTo(BeEmpty())
Expect(ninfo.GetMaxVolumesPerNode()).NotTo(BeNumerically("<", 0))
if accessibilityConstraintSupported {
Expect(ninfo.GetAccessibleTopology()).NotTo(BeNil())
}
})
})
Describe("NodePublishVolume", func() {
It("should fail when no volume id is provided", func() {
_, err := c.NodePublishVolume(
context.Background(),
&csi.NodePublishVolumeRequest{
NodePublishSecrets: sc.Secrets.NodePublishVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no target path is provided", func() {
_, err := c.NodePublishVolume(
context.Background(),
&csi.NodePublishVolumeRequest{
VolumeId: "id",
NodePublishSecrets: sc.Secrets.NodePublishVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no volume capability is provided", func() {
_, err := c.NodePublishVolume(
context.Background(),
&csi.NodePublishVolumeRequest{
VolumeId: "id",
TargetPath: sc.Config.TargetPath,
NodePublishSecrets: sc.Secrets.NodePublishVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
})
Describe("NodeUnpublishVolume", func() {
It("should fail when no volume id is provided", func() {
_, err := c.NodeUnpublishVolume(
context.Background(),
&csi.NodeUnpublishVolumeRequest{})
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no target path is provided", func() {
_, err := c.NodeUnpublishVolume(
context.Background(),
&csi.NodeUnpublishVolumeRequest{
VolumeId: "id",
})
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
})
Describe("NodeStageVolume", func() {
var (
device string
)
BeforeEach(func() {
if !nodeStageSupported {
Skip("NodeStageVolume not supported")
}
device = "/dev/mock"
})
It("should fail when no volume id is provided", func() {
_, err := c.NodeStageVolume(
context.Background(),
&csi.NodeStageVolumeRequest{
StagingTargetPath: sc.Config.StagingPath,
VolumeCapability: &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
PublishInfo: map[string]string{
"device": device,
},
NodeStageSecrets: sc.Secrets.NodeStageVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no staging target path is provided", func() {
_, err := c.NodeStageVolume(
context.Background(),
&csi.NodeStageVolumeRequest{
VolumeId: "id",
VolumeCapability: &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
PublishInfo: map[string]string{
"device": device,
},
NodeStageSecrets: sc.Secrets.NodeStageVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no volume capability is provided", func() {
_, err := c.NodeStageVolume(
context.Background(),
&csi.NodeStageVolumeRequest{
VolumeId: "id",
StagingTargetPath: sc.Config.StagingPath,
PublishInfo: map[string]string{
"device": device,
},
NodeStageSecrets: sc.Secrets.NodeStageVolumeSecret,
},
)
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
})
Describe("NodeUnstageVolume", func() {
BeforeEach(func() {
if !nodeStageSupported {
Skip("NodeUnstageVolume not supported")
}
})
It("should fail when no volume id is provided", func() {
_, err := c.NodeUnstageVolume(
context.Background(),
&csi.NodeUnstageVolumeRequest{
StagingTargetPath: sc.Config.StagingPath,
})
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
It("should fail when no staging target path is provided", func() {
_, err := c.NodeUnstageVolume(
context.Background(),
&csi.NodeUnstageVolumeRequest{
VolumeId: "id",
})
Expect(err).To(HaveOccurred())
serverError, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(serverError.Code()).To(Equal(codes.InvalidArgument))
})
})
It("should work", func() {
name := uniqueString("sanity-node-full")
// Create Volume First
By("creating a single node writer volume")
vol, err := s.CreateVolume(
context.Background(),
&csi.CreateVolumeRequest{
Name: name,
VolumeCapabilities: []*csi.VolumeCapability{
{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
},
ControllerCreateSecrets: sc.Secrets.CreateVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
Expect(vol).NotTo(BeNil())
Expect(vol.GetVolume()).NotTo(BeNil())
Expect(vol.GetVolume().GetId()).NotTo(BeEmpty())
cl.RegisterVolume(name, VolumeInfo{VolumeID: vol.GetVolume().GetId()})
By("getting a node id")
nid, err := c.NodeGetId(
context.Background(),
&csi.NodeGetIdRequest{})
Expect(err).NotTo(HaveOccurred())
Expect(nid).NotTo(BeNil())
Expect(nid.GetNodeId()).NotTo(BeEmpty())
var conpubvol *csi.ControllerPublishVolumeResponse
if controllerPublishSupported {
By("controller publishing volume")
conpubvol, err = s.ControllerPublishVolume(
context.Background(),
&csi.ControllerPublishVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
NodeId: nid.GetNodeId(),
VolumeCapability: &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
VolumeAttributes: vol.GetVolume().GetAttributes(),
Readonly: false,
ControllerPublishSecrets: sc.Secrets.ControllerPublishVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
cl.RegisterVolume(name, VolumeInfo{VolumeID: vol.GetVolume().GetId(), NodeID: nid.GetNodeId()})
Expect(conpubvol).NotTo(BeNil())
}
// NodeStageVolume
if nodeStageSupported {
By("node staging volume")
nodestagevol, err := c.NodeStageVolume(
context.Background(),
&csi.NodeStageVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
VolumeCapability: &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
StagingTargetPath: sc.Config.StagingPath,
VolumeAttributes: vol.GetVolume().GetAttributes(),
PublishInfo: conpubvol.GetPublishInfo(),
NodeStageSecrets: sc.Secrets.NodeStageVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
Expect(nodestagevol).NotTo(BeNil())
}
// NodePublishVolume
By("publishing the volume on a node")
var stagingPath string
if nodeStageSupported {
stagingPath = sc.Config.StagingPath
}
nodepubvol, err := c.NodePublishVolume(
context.Background(),
&csi.NodePublishVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
TargetPath: sc.Config.TargetPath,
StagingTargetPath: stagingPath,
VolumeCapability: &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER,
},
},
VolumeAttributes: vol.GetVolume().GetAttributes(),
PublishInfo: conpubvol.GetPublishInfo(),
NodePublishSecrets: sc.Secrets.NodePublishVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
Expect(nodepubvol).NotTo(BeNil())
// NodeUnpublishVolume
By("cleaning up calling nodeunpublish")
nodeunpubvol, err := c.NodeUnpublishVolume(
context.Background(),
&csi.NodeUnpublishVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
TargetPath: sc.Config.TargetPath,
})
Expect(err).NotTo(HaveOccurred())
Expect(nodeunpubvol).NotTo(BeNil())
if nodeStageSupported {
By("cleaning up calling nodeunstage")
nodeunstagevol, err := c.NodeUnstageVolume(
context.Background(),
&csi.NodeUnstageVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
StagingTargetPath: sc.Config.StagingPath,
},
)
Expect(err).NotTo(HaveOccurred())
Expect(nodeunstagevol).NotTo(BeNil())
}
if controllerPublishSupported {
By("cleaning up calling controllerunpublishing")
controllerunpubvol, err := s.ControllerUnpublishVolume(
context.Background(),
&csi.ControllerUnpublishVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
NodeId: nid.GetNodeId(),
ControllerUnpublishSecrets: sc.Secrets.ControllerUnpublishVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
Expect(controllerunpubvol).NotTo(BeNil())
}
By("cleaning up deleting the volume")
_, err = s.DeleteVolume(
context.Background(),
&csi.DeleteVolumeRequest{
VolumeId: vol.GetVolume().GetId(),
ControllerDeleteSecrets: sc.Secrets.DeleteVolumeSecret,
},
)
Expect(err).NotTo(HaveOccurred())
})
})

View File

@@ -0,0 +1,163 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
"crypto/rand"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/kubernetes-csi/csi-test/utils"
yaml "gopkg.in/yaml.v2"
"google.golang.org/grpc"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
// CSISecrets consists of secrets used in CSI credentials.
type CSISecrets struct {
CreateVolumeSecret map[string]string `yaml:"CreateVolumeSecret"`
DeleteVolumeSecret map[string]string `yaml:"DeleteVolumeSecret"`
ControllerPublishVolumeSecret map[string]string `yaml:"ControllerPublishVolumeSecret"`
ControllerUnpublishVolumeSecret map[string]string `yaml:"ControllerUnpublishVolumeSecret"`
NodeStageVolumeSecret map[string]string `yaml:"NodeStageVolumeSecret"`
NodePublishVolumeSecret map[string]string `yaml:"NodePublishVolumeSecret"`
CreateSnapshotSecret map[string]string `yaml:"CreateSnapshotSecret"`
DeleteSnapshotSecret map[string]string `yaml:"DeleteSnapshotSecret"`
}
// Config provides the configuration for the sanity tests. It
// needs to be initialized by the user of the sanity package.
type Config struct {
TargetPath string
StagingPath string
Address string
SecretsFile string
TestVolumeSize int64
}
// SanityContext holds the variables that each test can depend on. It
// gets initialized before each test block runs.
type SanityContext struct {
Config *Config
Conn *grpc.ClientConn
Secrets *CSISecrets
}
// Test will test the CSI driver at the specified address by
// setting up a Ginkgo suite and running it.
func Test(t *testing.T, reqConfig *Config) {
sc := &SanityContext{
Config: reqConfig,
}
registerTestsInGinkgo(sc)
RegisterFailHandler(Fail)
RunSpecs(t, "CSI Driver Test Suite")
}
func GinkgoTest(reqConfig *Config) {
sc := &SanityContext{
Config: reqConfig,
}
registerTestsInGinkgo(sc)
}
func (sc *SanityContext) setup() {
var err error
if len(sc.Config.SecretsFile) > 0 {
sc.Secrets, err = loadSecrets(sc.Config.SecretsFile)
Expect(err).NotTo(HaveOccurred())
} else {
sc.Secrets = &CSISecrets{}
}
By("connecting to CSI driver")
sc.Conn, err = utils.Connect(sc.Config.Address)
Expect(err).NotTo(HaveOccurred())
By("creating mount and staging directories")
err = createMountTargetLocation(sc.Config.TargetPath)
Expect(err).NotTo(HaveOccurred())
if len(sc.Config.StagingPath) > 0 {
err = createMountTargetLocation(sc.Config.StagingPath)
Expect(err).NotTo(HaveOccurred())
}
}
func (sc *SanityContext) teardown() {
if sc.Conn != nil {
sc.Conn.Close()
sc.Conn = nil
}
}
func createMountTargetLocation(targetPath string) error {
fileInfo, err := os.Stat(targetPath)
if err != nil && os.IsNotExist(err) {
return os.MkdirAll(targetPath, 0755)
} else if err != nil {
return err
}
if !fileInfo.IsDir() {
return fmt.Errorf("Target location %s is not a directory", targetPath)
}
return nil
}
func loadSecrets(path string) (*CSISecrets, error) {
var creds CSISecrets
yamlFile, err := ioutil.ReadFile(path)
if err != nil {
return &creds, fmt.Errorf("failed to read file %q: #%v", path, err)
}
err = yaml.Unmarshal(yamlFile, &creds)
if err != nil {
return &creds, fmt.Errorf("error unmarshaling yaml: #%v", err)
}
return &creds, nil
}
var uniqueSuffix = "-" + pseudoUUID()
// pseudoUUID returns a unique string generated from random
// bytes, empty string in case of error.
func pseudoUUID() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
// Shouldn't happen?!
return ""
}
return fmt.Sprintf("%08X-%08X", b[0:4], b[4:8])
}
// uniqueString returns a unique string by appending a random
// number. In case of an error, just the prefix is returned, so it
// alone should already be fairly unique.
func uniqueString(prefix string) string {
return prefix + uniqueSuffix
}

View File

@@ -0,0 +1,56 @@
/*
Copyright 2018 Intel Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sanity
import (
. "github.com/onsi/ginkgo"
)
type test struct {
text string
body func(*SanityContext)
}
var tests []test
// DescribeSanity must be used instead of the usual Ginkgo Describe to
// register a test block. The difference is that the body function
// will be called multiple times with the right context (when
// setting up a Ginkgo suite or a testing.T test, with the right
// configuration).
func DescribeSanity(text string, body func(*SanityContext)) bool {
tests = append(tests, test{text, body})
return true
}
// registerTestsInGinkgo invokes the actual Gingko Describe
// for the tests registered earlier with DescribeSanity.
func registerTestsInGinkgo(sc *SanityContext) {
for _, test := range tests {
Describe(test.text, func() {
BeforeEach(func() {
sc.setup()
})
test.body(sc)
AfterEach(func() {
sc.teardown()
})
})
}
}

View File

@@ -0,0 +1,188 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package test
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/proto"
mock_driver "github.com/kubernetes-csi/csi-test/driver"
mock_utils "github.com/kubernetes-csi/csi-test/utils"
)
func TestPluginInfoResponse(t *testing.T) {
// Setup mock
m := gomock.NewController(t)
defer m.Finish()
driver := mock_driver.NewMockIdentityServer(m)
// Setup input
in := &csi.GetPluginInfoRequest{}
// Setup mock outout
out := &csi.GetPluginInfoResponse{
Name: "mock",
VendorVersion: "0.1.1",
Manifest: map[string]string{
"hello": "world",
},
}
// Setup expectation
driver.EXPECT().GetPluginInfo(nil, in).Return(out, nil).Times(1)
// Actual call
r, err := driver.GetPluginInfo(nil, in)
name := r.GetName()
if err != nil {
t.Errorf("Error: %s", err.Error())
}
if name != "mock" {
t.Errorf("Unknown name: %s\n", name)
}
}
type pbMatcher struct {
x proto.Message
}
func (p pbMatcher) Matches(x interface{}) bool {
y := x.(proto.Message)
return proto.Equal(p.x, y)
}
func (p pbMatcher) String() string {
return fmt.Sprintf("pb equal to %v", p.x)
}
func pbMatch(x interface{}) gomock.Matcher {
v := x.(proto.Message)
return &pbMatcher{v}
}
func TestGRPCGetPluginInfoReponse(t *testing.T) {
// Setup mock
m := gomock.NewController(&mock_utils.SafeGoroutineTester{})
defer m.Finish()
driver := mock_driver.NewMockIdentityServer(m)
// Setup input
in := &csi.GetPluginInfoRequest{}
// Setup mock outout
out := &csi.GetPluginInfoResponse{
Name: "mock",
VendorVersion: "0.1.1",
Manifest: map[string]string{
"hello": "world",
},
}
// Setup expectation
// !IMPORTANT!: Must set context expected value to gomock.Any() to match any value
driver.EXPECT().GetPluginInfo(gomock.Any(), pbMatch(in)).Return(out, nil).Times(1)
// Create a new RPC
server := mock_driver.NewMockCSIDriver(&mock_driver.MockCSIDriverServers{
Identity: driver,
})
conn, err := server.Nexus()
if err != nil {
t.Errorf("Error: %s", err.Error())
}
defer server.Close()
// Make call
c := csi.NewIdentityClient(conn)
r, err := c.GetPluginInfo(context.Background(), in)
if err != nil {
t.Errorf("Error: %s", err.Error())
}
name := r.GetName()
if name != "mock" {
t.Errorf("Unknown name: %s\n", name)
}
}
func TestGRPCAttach(t *testing.T) {
// Setup mock
m := gomock.NewController(&mock_utils.SafeGoroutineTester{})
defer m.Finish()
driver := mock_driver.NewMockControllerServer(m)
// Setup input
defaultVolumeID := "myname"
defaultNodeID := "MyNodeID"
defaultCaps := &csi.VolumeCapability{
AccessType: &csi.VolumeCapability_Mount{
Mount: &csi.VolumeCapability_MountVolume{},
},
AccessMode: &csi.VolumeCapability_AccessMode{
Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
},
}
publishVolumeInfo := map[string]string{
"first": "foo",
"second": "bar",
"third": "baz",
}
defaultRequest := &csi.ControllerPublishVolumeRequest{
VolumeId: defaultVolumeID,
NodeId: defaultNodeID,
VolumeCapability: defaultCaps,
Readonly: false,
}
// Setup mock outout
out := &csi.ControllerPublishVolumeResponse{
PublishInfo: publishVolumeInfo,
}
// Setup expectation
// !IMPORTANT!: Must set context expected value to gomock.Any() to match any value
driver.EXPECT().ControllerPublishVolume(gomock.Any(), pbMatch(defaultRequest)).Return(out, nil).Times(1)
// Create a new RPC
server := mock_driver.NewMockCSIDriver(&mock_driver.MockCSIDriverServers{
Controller: driver,
})
conn, err := server.Nexus()
if err != nil {
t.Errorf("Error: %s", err.Error())
}
defer server.Close()
// Make call
c := csi.NewControllerClient(conn)
r, err := c.ControllerPublishVolume(context.Background(), defaultRequest)
if err != nil {
t.Errorf("Error: %s", err.Error())
}
info := r.GetPublishInfo()
if !reflect.DeepEqual(info, publishVolumeInfo) {
t.Errorf("Invalid publish info: %v", info)
}
}

View File

@@ -0,0 +1,127 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package test
import (
"context"
"net"
"sync"
"testing"
"github.com/container-storage-interface/spec/lib/go/csi/v0"
"github.com/kubernetes-csi/csi-test/utils"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// Example simple driver
// This example assumes that your driver will create the server and listen on
// some unix domain socket or port for tests.
type simpleDriver struct {
listener net.Listener
server *grpc.Server
wg sync.WaitGroup
}
func (s *simpleDriver) GetPluginCapabilities(context.Context, *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
// TODO: Return some simple Plugin Capabilities
return &csi.GetPluginCapabilitiesResponse{}, nil
}
func (s *simpleDriver) Probe(context.Context, *csi.ProbeRequest) (*csi.ProbeResponse, error) {
return &csi.ProbeResponse{}, nil
}
func (s *simpleDriver) GetPluginInfo(
context.Context, *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
return &csi.GetPluginInfoResponse{
Name: "simpleDriver",
VendorVersion: "0.1.1",
Manifest: map[string]string{
"hello": "world",
},
}, nil
}
func (s *simpleDriver) goServe() {
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.server.Serve(s.listener)
}()
}
func (s *simpleDriver) Address() string {
return s.listener.Addr().String()
}
func (s *simpleDriver) Start() error {
// Listen on a port assigned by the net package
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
s.listener = l
// Create a new grpc server
s.server = grpc.NewServer()
csi.RegisterIdentityServer(s.server, s)
reflection.Register(s.server)
// Start listening for requests
s.goServe()
return nil
}
func (s *simpleDriver) Stop() {
s.server.Stop()
s.wg.Wait()
}
//
// Tests
//
func TestSimpleDriver(t *testing.T) {
// Setup simple driver
s := &simpleDriver{}
err := s.Start()
if err != nil {
t.Errorf("Error: %s", err.Error())
}
defer s.Stop()
// Setup a connection to the driver
conn, err := utils.Connect(s.Address())
if err != nil {
t.Errorf("Error: %s", err.Error())
}
defer conn.Close()
// Make a call
c := csi.NewIdentityClient(conn)
r, err := c.GetPluginInfo(context.Background(), &csi.GetPluginInfoRequest{})
if err != nil {
t.Errorf("Error: %s", err.Error())
}
// Verify
name := r.GetName()
if name != "simpleDriver" {
t.Errorf("Unknown name: %s\n", name)
}
}

View File

@@ -0,0 +1,59 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"context"
"fmt"
"net"
"net/url"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
// Connect address by grpc
func Connect(address string) (*grpc.ClientConn, error) {
dialOptions := []grpc.DialOption{
grpc.WithInsecure(),
}
u, err := url.Parse(address)
if err == nil && (!u.IsAbs() || u.Scheme == "unix") {
dialOptions = append(dialOptions,
grpc.WithDialer(
func(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("unix", u.Path, timeout)
}))
}
conn, err := grpc.Dial(address, dialOptions...)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
for {
if !conn.WaitForStateChange(ctx, conn.GetState()) {
return conn, fmt.Errorf("Connection timed out")
}
if conn.GetState() == connectivity.Ready {
return conn, nil
}
}
}

View File

@@ -0,0 +1,40 @@
/*
Copyright 2017 Luis Pabón luis@portworx.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import "fmt"
// SafeGoroutineTester is an implementation of the mock ... interface
// which can be used to use the mock functions in another go routine.
//
// The major issue is that the golang mock framework uses t.Fatalf()
// which causes a deadlock when called in another goroutine. To avoid
// this issue, this simple implementation prints the error then panics,
// which avoids the deadlock.
type SafeGoroutineTester struct{}
// Errorf prints the error to the screen then panics
func (s *SafeGoroutineTester) Errorf(format string, args ...interface{}) {
fmt.Printf(format, args)
panic("MOCK TEST ERROR")
}
// Fatalf prints the error to the screen then panics
func (s *SafeGoroutineTester) Fatalf(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...)
panic("MOCK TEST FATAL FAILURE")
}