diff --git a/Makefile b/Makefile index 2773bd59..424dff92 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +IMAGE=chengpan/aws-fsx-csi-driver VERSION=0.1.0 .PHONY: aws-fsx-csi-driver @@ -22,3 +23,16 @@ aws-fsx-csi-driver: .PHONY: test test: go test -v -race ./pkg/... + +.PHONY: test-sanity +test-sanity: + go test -v ./tests/sanity/... + +.PHONY: image +image: + docker build -t $(IMAGE):testing . + +.PHONY: push +push: + docker push $(IMAGE):testing + diff --git a/deploy/kubernetes/controller.yaml b/deploy/kubernetes/controller.yaml index 81ba3307..72907c91 100644 --- a/deploy/kubernetes/controller.yaml +++ b/deploy/kubernetes/controller.yaml @@ -6,11 +6,44 @@ metadata: --- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: external-provisioner-role +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-provisioner-binding +subjects: + - kind: ServiceAccount + name: csi-controller-sa + namespace: kube-system +roleRef: + kind: ClusterRole + name: external-provisioner-role + apiGroup: rbac.authorization.k8s.io + +--- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: external-attacher-role - namespace: default rules: - apiGroups: [""] resources: ["persistentvolumes"] @@ -27,8 +60,7 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: csi-attacher-role - namespace: default + name: csi-attacher-binding subjects: - kind: ServiceAccount name: csi-controller-sa @@ -69,6 +101,30 @@ spec: env: - name: CSI_ENDPOINT value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: aws-secret + key: key_id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-secret + key: access_key + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: csi-provisioner + image: quay.io/k8scsi/csi-provisioner:v0.4.1 + imagePullPolicy: Always + args: + - --provisioner=fsx.csi.aws.com + - --csi-address=$(ADDRESS) + - --connection-timeout=5m + - --v=5 + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock volumeMounts: - name: socket-dir mountPath: /var/lib/csi/sockets/pluginproxy/ diff --git a/examples/kubernetes/dynamic_provisioning/claim.yaml b/examples/kubernetes/dynamic_provisioning/claim.yaml new file mode 100644 index 00000000..8965e36d --- /dev/null +++ b/examples/kubernetes/dynamic_provisioning/claim.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fsx-claim +spec: + accessModes: + - ReadWriteOnce + storageClassName: fsx-sc + resources: + requests: + storage: 5Gi diff --git a/examples/kubernetes/dynamic_provisioning/pod.yaml b/examples/kubernetes/dynamic_provisioning/pod.yaml new file mode 100644 index 00000000..b3ae5826 --- /dev/null +++ b/examples/kubernetes/dynamic_provisioning/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: fsx-app +spec: + containers: + - name: app + image: centos + command: ["/bin/sh"] + args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"] + volumeMounts: + - name: persistent-storage + mountPath: /data + volumes: + - name: persistent-storage + persistentVolumeClaim: + claimName: fsx-claim diff --git a/examples/kubernetes/dynamic_provisioning/storageclass.yaml b/examples/kubernetes/dynamic_provisioning/storageclass.yaml new file mode 100644 index 00000000..c67072ce --- /dev/null +++ b/examples/kubernetes/dynamic_provisioning/storageclass.yaml @@ -0,0 +1,8 @@ +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: fsx-sc +provisioner: fsx.csi.aws.com +parameters: + subnetId: subnet-056da83524edbe641 + securityGroupIds: sg-086f61ea73388fb6b diff --git a/go.mod b/go.mod index a8d4d2a5..07639a4d 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,19 @@ module github.com/kubernetes-sigs/aws-fsx-csi-driver require ( github.com/aws/aws-sdk-go v1.16.5 github.com/container-storage-interface/spec v0.3.0 + github.com/davecgh/go-spew v1.1.0 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/mock v1.2.0 - github.com/golangci/golangci-lint v1.12.5 // indirect + github.com/golang/protobuf v1.2.0 + github.com/kubernetes-csi/csi-test v0.3.0-2 + github.com/onsi/ginkgo v1.7.0 + github.com/onsi/gomega v1.4.3 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.1.0 // indirect + github.com/stretchr/testify v1.2.1 // indirect + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd google.golang.org/grpc v1.16.0 + k8s.io/apimachinery v0.0.0-20190205091131-4b4ea28f2790 k8s.io/klog v0.1.0 // indirect k8s.io/kubernetes v1.13.1 k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 // indirect diff --git a/go.sum b/go.sum index 7aefda97..84fc082d 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA= -github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aws/aws-sdk-go v1.16.5 h1:NVxzZXIuwX828VcJrpNxxWjur1tlOBISdMdDdHIKHcc= github.com/aws/aws-sdk-go v1.16.5/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/container-storage-interface/spec v0.3.0 h1:ALxSqFjptj8R5rL+cdyAbwbaLHHXDL5pmp1qIh1b+38= github.com/container-storage-interface/spec v0.3.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= -github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/go-critic/checkers v0.0.0-20181204210945-97246d3b3c67 h1:AhL5n4pH/qzefJ64+0RbymXZSBsvgbBaVJQCcjFaJPw= -github.com/go-critic/checkers v0.0.0-20181204210945-97246d3b3c67/go.mod h1:Cg5JCP9M6m93z6fecpRcVgD2lZf2RvPtb85ldjiShZc= -github.com/go-lintpack/lintpack v0.5.1 h1:v5D/csM90cu5PANqkj1JcNZGX/mrr3Z2Wu7Q8KuFd9M= -github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= -github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1 h1:h+1eMw+tZAlgTVclcVN0/rdPaBI/RUzG0peblT6df+Q= -github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o= -github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4 h1:wVs9OMjICHbAryp9hcIuWqUOi+NqEbUSZy9zMe3W//I= -github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8= -github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6 h1:aTBUNRTatDDU24gbOEKEoLiDwxtc98ga6K/iMTm6fvs= -github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086 h1:EIMuvbE9fbtQtimdLe5yeXjuC5CeKbQt8zH6GwtIrhM= -github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= -github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30 h1:zRJPftZJNLPDiOtvYbFRwjSbaJAcVOf80TeEmWGe2kQ= -github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= -github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= -github.com/go-toolsmith/pkgload v0.0.0-20181120203407-5122569a890b/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= -github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241 h1:ZRDeQioMGTBLeJxcPxXfFifEUgYxzR7fXw7w2WR+1bo= -github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676 h1:6Qrsp0+25KEkaS2bB26UE0giFgRrIc8mYXboDL5OVMA= -github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= -github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -46,139 +16,56 @@ github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 h1:i2jIkQFb8RG45DuQs+ElyROY848cSJIoIkBM+7XXypA= -github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:WadunOE/TeHR8U7f0TXiJACHeU3cuFOXuKafw4rozqU= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/go-tools v0.0.0-20180902103155-93eecd106a0b h1:FSrt9JBK7JINu5UobyIF6epfpjL66H+67KZoTbE0zwk= -github.com/golangci/go-tools v0.0.0-20180902103155-93eecd106a0b/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= -github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 h1:ir6/L2ZOJfFrJlOTsuf/hlzdPuUwXV/VzkSlgS6f1vs= -github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.12.5 h1:YY6rL4jOuF4P4CKfxaAVMlTMgs8c7e1RlXiHwc28KSs= -github.com/golangci/golangci-lint v1.12.5/go.mod h1:iMfuFWFYJ1CZxlMQfNWvPj3c22PuyUkw9RQ1UfhDFDk= -github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc= -github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo= -github.com/golangci/govet v0.0.0-20180818181408-44ddbe260190 h1:SLIgprnxQNjBpkz55PK1vfb64/gKU/TgVi0obFw8Lec= -github.com/golangci/govet v0.0.0-20180818181408-44ddbe260190/go.mod h1:pPwb+AK755h3/r73avHz5bEN6sa51/2HEZlLaV53hCo= -github.com/golangci/ineffassign v0.0.0-20180808204949-2ee8f2867dde h1:qEGp3ZF1Qw6TkbWKn6GdJ12Ssu/CpJBaBcJ4hrUjrSo= -github.com/golangci/ineffassign v0.0.0-20180808204949-2ee8f2867dde/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= -github.com/golangci/interfacer v0.0.0-20180902080945-01958817a6ec h1:rvg392QhtAd/yOevPRX4wzYMIDYyq99j9MV+Mb1uVHs= -github.com/golangci/interfacer v0.0.0-20180902080945-01958817a6ec/go.mod h1:yBorupihJ5OYDFE7/EZwrslyNyZaaidqqVptYTcNxnk= -github.com/golangci/lint v0.0.0-20170908181259-c2187e7932b5/go.mod h1:zs8jPuoOp76KrjiydDqO3CGeS4v9gq77HNNiYcxxTGw= -github.com/golangci/lint v0.0.0-20180902080404-c2187e7932b5 h1:9NYm50bkzER4RayDaggNjxF5kesUJREASyFgk4AcIis= -github.com/golangci/lint v0.0.0-20180902080404-c2187e7932b5/go.mod h1:zs8jPuoOp76KrjiydDqO3CGeS4v9gq77HNNiYcxxTGw= -github.com/golangci/lint-1 v0.0.0-20180610141402-4bf9709227d1 h1:PHK2kIh21Zt4IcG0bBRzQwEDVKF64LnkoSXnm8lfJUk= -github.com/golangci/lint-1 v0.0.0-20180610141402-4bf9709227d1/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= -github.com/golangci/tools v0.0.0-20180902102414-2cefd77fef9b h1:3hI7NZ9D3edEBVbN6V1urHWbFKJfcIlOFvX5m10jB88= -github.com/golangci/tools v0.0.0-20180902102414-2cefd77fef9b/go.mod h1:zgj6NOYXOC1cexsdtDceI4/mj3aXK4JOVg9AV3C5LWI= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/golangci/unparam v0.0.0-20180902112548-7ad9dbcccc16 h1:QURX/XMP2uJUzzEvfJ291v1snmbJuyznAJLSQVnPyko= -github.com/golangci/unparam v0.0.0-20180902112548-7ad9dbcccc16/go.mod h1:KW2L33j82vo0S0U6RP6uUQSuat+0Q457Yf+1mXC98/M= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= -github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= -github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= -github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/kubernetes-csi/csi-test v0.3.0-2 h1:RsVmFc3AlpjQdyfMKiphvd8igXwOI7lQgWo+kJAh49c= +github.com/kubernetes-csi/csi-test v0.3.0-2/go.mod h1:YxJ4UiuPWIhMBkxUKY5c267DyA0uDZ/MtAimhx/2TA0= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= -github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= -github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE= -github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= -github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= -github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab h1:w4c/LoOA2vE8SYwh8wEEQVRUwpph7TtcjH7AtZvOjy0= -golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180826000951-f6ba57429505/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52 h1:oOIe9Zzq27JsS/3ACpGF1HwWnWNflZWT/3EvM7mtcEk= -golang.org/x/tools v0.0.0-20181220024903-92cdcd90bf52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/apimachinery v0.0.0-20190205091131-4b4ea28f2790 h1:PB8RHaA3hBMXY8+dZcsbJqur3XzQBV4BhbrMqfoOaAw= +k8s.io/apimachinery v0.0.0-20190205091131-4b4ea28f2790/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kubernetes v1.13.1 h1:UGJpxWwhE/oxhHhaNgmoW/nQcdFdKETx5bV8K5FHgyk= k8s.io/kubernetes v1.13.1/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 h1:S3/Kq185JnolOEemhmDXXd23l2t4bX5hPQPQPADlF1E= k8s.io/utils v0.0.0-20181115163542-0d26856f57b3/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= -sourcegraph.com/sourcegraph/go-diff v0.0.0-20171119081133-3f415a150aec h1:wAAdENPXC7bE1oxY4VqSDdhaA+XQ8TgQHsZMMnrXjEk= -sourcegraph.com/sourcegraph/go-diff v0.0.0-20171119081133-3f415a150aec/go.mod h1:R09mWeb9JcPbO+A3cYDc11xjz0wp6r9+KnqdqROAoRU= -sourcegraph.com/sqs/pbtypes v0.0.0-20160107090929-4d1b9dc7ffc3 h1:hXy8YsgVLDz5mlngKhNHQhAsAGrSp3dlXZN4b0/4UUI= -sourcegraph.com/sqs/pbtypes v0.0.0-20160107090929-4d1b9dc7ffc3/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/hack/update-gomock b/hack/update-gomock index 18f80da9..55e2e963 100755 --- a/hack/update-gomock +++ b/hack/update-gomock @@ -19,3 +19,5 @@ set -euo pipefail IMPORT_PATH=github.com/kubernetes-sigs/aws-fsx-csi-driver mockgen -package=mocks -destination=./pkg/driver/mocks/mock_mount.go k8s.io/kubernetes/pkg/util/mount Interface mockgen -package=mocks -destination=./pkg/cloud/mocks/mock_ec2metadata.go ${IMPORT_PATH}/pkg/cloud EC2Metadata +mockgen -package=mocks -destination=./pkg/cloud/mocks/mock_fsx.go ${IMPORT_PATH}/pkg/cloud FSx +mockgen -package=mocks -destination=./pkg/driver/mocks/mock_cloud.go ${IMPORT_PATH}/pkg/cloud Cloud diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 9763df88..0653871a 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -17,19 +17,81 @@ limitations under the License. package cloud import ( + "context" + "errors" "fmt" + "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/util/wait" ) +const ( + // DefaultVolumeSize represents the default size used + // this is the minimum FSx for Lustre FS size + DefaultVolumeSize = 3600 +) + +// Tags +const ( + // VolumeNameTagKey is the key value that refers to the volume's name. + VolumeNameTagKey = "CSIVolumeName" +) + +var ( + // ErrMultiDisks is an error that is returned when multiple + // disks are found with the same volume name. + ErrMultiFileSystems = errors.New("Multiple filesystems with same ID") + + // ErrFsExistsDiffSize is an error that is returned if a filesystem + // exists with a given ID, but a different capacity is requested. + ErrFsExistsDiffSize = errors.New("There is already a disk with same ID and different size") + + // ErrNotFound is returned when a resource is not found. + ErrNotFound = errors.New("Resource was not found") +) + +// FileSystem represents a FSx for Lustre filesystem +type FileSystem struct { + FileSystemId string + CapacityGiB int64 + DnsName string +} + +// FileSystemOptions represents the options to create FSx for Lustre filesystem +type FileSystemOptions struct { + CapacityGiB int64 + SubnetId string + SecurityGroupIds []string +} + +// FSx abstracts FSx client to facilitate its mocking. +// See https://docs.aws.amazon.com/sdk-for-go/api/service/fsx/ for details +type FSx interface { + CreateFileSystemWithContext(aws.Context, *fsx.CreateFileSystemInput, ...request.Option) (*fsx.CreateFileSystemOutput, error) + DeleteFileSystemWithContext(aws.Context, *fsx.DeleteFileSystemInput, ...request.Option) (*fsx.DeleteFileSystemOutput, error) + DescribeFileSystemsWithContext(aws.Context, *fsx.DescribeFileSystemsInput, ...request.Option) (*fsx.DescribeFileSystemsOutput, error) +} + type Cloud interface { GetMetadata() MetadataService + CreateFileSystem(ctx context.Context, volumeName string, fileSystemOptions *FileSystemOptions) (fs *FileSystem, err error) + DeleteFileSystem(ctx context.Context, fileSystemId string) (err error) + DescribeFileSystem(ctx context.Context, fileSystemId string) (fs *FileSystem, err error) + WaitForFileSystemAvailable(ctx context.Context, fileSystemId string) error } type cloud struct { metadata MetadataService + fsx FSx } // NewCloud returns a new instance of AWS cloud @@ -43,11 +105,150 @@ func NewCloud() (Cloud, error) { return nil, fmt.Errorf("could not get metadata from AWS: %v", err) } + provider := []credentials.Provider{ + &credentials.EnvProvider{}, + &ec2rolecreds.EC2RoleProvider{Client: svc}, + &credentials.SharedCredentialsProvider{}, + } + + awsConfig := &aws.Config{ + Region: aws.String(metadata.GetRegion()), + Credentials: credentials.NewChainCredentials(provider), + CredentialsChainVerboseErrors: aws.Bool(true), + } + return &cloud{ metadata: metadata, + fsx: fsx.New(session.Must(session.NewSession(awsConfig))), }, nil } func (c *cloud) GetMetadata() MetadataService { return c.metadata } + +func (c *cloud) CreateFileSystem(ctx context.Context, volumeName string, fileSystemOptions *FileSystemOptions) (fs *FileSystem, err error) { + if len(fileSystemOptions.SubnetId) == 0 { + return nil, fmt.Errorf("SubnetId is required") + } + + input := &fsx.CreateFileSystemInput{ + ClientRequestToken: aws.String(volumeName), + FileSystemType: aws.String("LUSTRE"), + StorageCapacity: aws.Int64(fileSystemOptions.CapacityGiB), + SubnetIds: []*string{aws.String(fileSystemOptions.SubnetId)}, + SecurityGroupIds: aws.StringSlice(fileSystemOptions.SecurityGroupIds), + Tags: []*fsx.Tag{ + { + Key: aws.String(VolumeNameTagKey), + Value: aws.String(volumeName), + }, + }, + } + + output, err := c.fsx.CreateFileSystemWithContext(ctx, input) + if err != nil { + if isIncompatibleParameter(err) { + return nil, ErrFsExistsDiffSize + } + return nil, fmt.Errorf("CreateFileSystem failed: %v", err) + } + + return &FileSystem{ + FileSystemId: *output.FileSystem.FileSystemId, + CapacityGiB: *output.FileSystem.StorageCapacity, + DnsName: *output.FileSystem.DNSName, + }, nil +} + +func (c *cloud) DeleteFileSystem(ctx context.Context, fileSystemId string) (err error) { + input := &fsx.DeleteFileSystemInput{ + FileSystemId: aws.String(fileSystemId), + } + if _, err = c.fsx.DeleteFileSystemWithContext(ctx, input); err != nil { + if isFileSystemNotFound(err) { + return ErrNotFound + } + return fmt.Errorf("DeleteFileSystem failed: %v", err) + } + return nil +} + +func (c *cloud) DescribeFileSystem(ctx context.Context, fileSystemId string) (*FileSystem, error) { + fs, err := c.getFileSystem(ctx, fileSystemId) + if err != nil { + return nil, err + } + return &FileSystem{ + FileSystemId: *fs.FileSystemId, + CapacityGiB: *fs.StorageCapacity, + DnsName: *fs.DNSName, + }, nil +} + +func (c *cloud) WaitForFileSystemAvailable(ctx context.Context, fileSystemId string) error { + var ( + // interval to check if filesystem is ready + // needs to be shorter than the provisioner timeout + checkInterval = 15 * time.Second + // FSx for lustre filesystem creation time is around 5 mins + checkTimeout = 7 * time.Minute + ) + err := wait.Poll(checkInterval, checkTimeout, func() (done bool, err error) { + fs, err := c.getFileSystem(ctx, fileSystemId) + if err != nil { + return true, err + } + glog.V(4).Infof("WaitForFileSystemAvailable filesystem status is: %v", *fs.Lifecycle) + switch *fs.Lifecycle { + case "AVAILABLE": + return true, nil + case "CREATING": + return false, nil + default: + return true, fmt.Errorf("unexpected state for filesystem %s: %q", fileSystemId, *fs.Lifecycle) + } + }) + + return err + +} + +func (c *cloud) getFileSystem(ctx context.Context, fileSystemId string) (*fsx.FileSystem, error) { + input := &fsx.DescribeFileSystemsInput{ + FileSystemIds: []*string{aws.String(fileSystemId)}, + } + + output, err := c.fsx.DescribeFileSystemsWithContext(ctx, input) + if err != nil { + return nil, err + } + + if len(output.FileSystems) > 1 { + return nil, ErrMultiFileSystems + } + + if len(output.FileSystems) == 0 { + return nil, ErrNotFound + } + + return output.FileSystems[0], nil +} + +func isFileSystemNotFound(err error) bool { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == fsx.ErrCodeFileSystemNotFound { + return true + } + } + return false +} + +func isIncompatibleParameter(err error) bool { + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == fsx.ErrCodeIncompatibleParameterError { + return true + } + } + return false +} diff --git a/pkg/cloud/cloud_test.go b/pkg/cloud/cloud_test.go new file mode 100644 index 00000000..f6307a4b --- /dev/null +++ b/pkg/cloud/cloud_test.go @@ -0,0 +1,269 @@ +/* +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. +*/ + +package cloud + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/fsx" + "github.com/golang/mock/gomock" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud/mocks" +) + +func TestCreateFileSystem(t *testing.T) { + var ( + volumeName = "volumeName" + fileSystemId = "fs-1234" + volumeSizeGiB int64 = 3600 + subnetId = "subnet-056da83524edbe641" + securityGroupIds = []string{"sg-086f61ea73388fb6b", "sg-0145e55e976000c9e"} + dnsname = "test.fsx.us-west-2.amazoawd.com" + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + req := &FileSystemOptions{ + CapacityGiB: volumeSizeGiB, + SubnetId: subnetId, + SecurityGroupIds: securityGroupIds, + } + + output := &fsx.CreateFileSystemOutput{ + FileSystem: &fsx.FileSystem{ + FileSystemId: aws.String(fileSystemId), + StorageCapacity: aws.Int64(volumeSizeGiB), + DNSName: aws.String(dnsname), + }, + } + ctx := context.Background() + mockFSx.EXPECT().CreateFileSystemWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) + resp, err := c.CreateFileSystem(ctx, volumeName, req) + if err != nil { + t.Fatalf("CreateFileSystem is failed: %v", err) + } + + if resp == nil { + t.Fatal("resp is nil") + } + + if resp.FileSystemId != fileSystemId { + t.Fatalf("FileSystemId mismatches. actual: %v expected: %v", resp.FileSystemId, fileSystemId) + } + + if resp.CapacityGiB != volumeSizeGiB { + t.Fatalf("CapacityGiB mismatches. actual: %v expected: %v", resp.CapacityGiB, volumeSizeGiB) + } + + if resp.DnsName != dnsname { + t.Fatalf("DnsName mismatches. actual: %v expected: %v", resp.DnsName, dnsname) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: missing subnet ID", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + req := &FileSystemOptions{ + CapacityGiB: volumeSizeGiB, + SecurityGroupIds: securityGroupIds, + } + + ctx := context.Background() + _, err := c.CreateFileSystem(ctx, volumeName, req) + if err == nil { + t.Fatal("CreateFileSystem is not failed") + } + + mockCtl.Finish() + }, + }, + { + name: "fail: CreateFileSystemWithContext return error", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + req := &FileSystemOptions{ + CapacityGiB: volumeSizeGiB, + SubnetId: subnetId, + SecurityGroupIds: securityGroupIds, + } + + ctx := context.Background() + mockFSx.EXPECT().CreateFileSystemWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("CreateFileSystemWithContext failed")) + _, err := c.CreateFileSystem(ctx, volumeName, req) + if err == nil { + t.Fatal("CreateFileSystem is not failed") + } + + mockCtl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + +func TestDeleteFileSystem(t *testing.T) { + var ( + fileSystemId = "fs-1234" + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + output := &fsx.DeleteFileSystemOutput{} + ctx := context.Background() + mockFSx.EXPECT().DeleteFileSystemWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) + err := c.DeleteFileSystem(ctx, fileSystemId) + if err != nil { + t.Fatalf("DeleteFileSystem is failed: %v", err) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: DeleteFileSystemWithContext return error", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + ctx := context.Background() + mockFSx.EXPECT().DeleteFileSystemWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("DeleteFileSystemWithContext failed")) + err := c.DeleteFileSystem(ctx, fileSystemId) + if err == nil { + t.Fatal("DeleteFileSystem is not failed") + } + + mockCtl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + +func TestDescribeFileSystem(t *testing.T) { + var ( + fileSystemId = "fs-1234" + volumeSizeGiB int64 = 3600 + dnsname = "test.fsx.us-west-2.amazoawd.com" + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + output := &fsx.DescribeFileSystemsOutput{ + FileSystems: []*fsx.FileSystem{ + { + FileSystemId: aws.String(fileSystemId), + StorageCapacity: aws.Int64(volumeSizeGiB), + DNSName: aws.String(dnsname), + }, + }, + } + ctx := context.Background() + mockFSx.EXPECT().DescribeFileSystemsWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) + _, err := c.DescribeFileSystem(ctx, fileSystemId) + if err != nil { + t.Fatalf("DeleteFileSystem is failed: %v", err) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: DescribeFileSystemWithContext return error", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockFSx := mocks.NewMockFSx(mockCtl) + c := &cloud{ + metadata: &metadata{"instanceID", "region", "az"}, + fsx: mockFSx, + } + + ctx := context.Background() + mockFSx.EXPECT().DescribeFileSystemsWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("DescribeFileSystemsWithContext failed")) + _, err := c.DescribeFileSystem(ctx, fileSystemId) + if err == nil { + t.Fatal("DescribeFileSystem is not failed") + } + + mockCtl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} diff --git a/pkg/cloud/fakes.go b/pkg/cloud/fakes.go new file mode 100644 index 00000000..cd656525 --- /dev/null +++ b/pkg/cloud/fakes.go @@ -0,0 +1,88 @@ +/* +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. +*/ + +package cloud + +import ( + "context" + "fmt" + "math/rand" + "time" +) + +var random *rand.Rand + +func init() { + random = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +type FakeCloudProvider struct { + m *metadata + fileSystems map[string]*FileSystem +} + +func NewFakeCloudProvider() *FakeCloudProvider { + return &FakeCloudProvider{ + m: &metadata{"instanceID", "region", "az"}, + fileSystems: make(map[string]*FileSystem), + } +} + +func (c *FakeCloudProvider) GetMetadata() MetadataService { + return c.m +} + +func (c *FakeCloudProvider) CreateFileSystem(ctx context.Context, volumeName string, fileSystemOptions *FileSystemOptions) (fs *FileSystem, err error) { + fs, exists := c.fileSystems[volumeName] + if exists { + if fs.CapacityGiB == fileSystemOptions.CapacityGiB { + return fs, nil + } else { + return nil, ErrFsExistsDiffSize + } + } + + fs = &FileSystem{ + FileSystemId: fmt.Sprintf("fs-%d", random.Uint64()), + CapacityGiB: fileSystemOptions.CapacityGiB, + DnsName: "test.us-east-1.fsx.amazonaws.com", + } + c.fileSystems[volumeName] = fs + return fs, nil +} + +func (c *FakeCloudProvider) DeleteFileSystem(ctx context.Context, volumeID string) (err error) { + delete(c.fileSystems, volumeID) + for name, fs := range c.fileSystems { + if fs.FileSystemId == volumeID { + delete(c.fileSystems, name) + } + } + return nil +} + +func (c *FakeCloudProvider) DescribeFileSystem(ctx context.Context, volumeID string) (fs *FileSystem, err error) { + for _, fs := range c.fileSystems { + if fs.FileSystemId == volumeID { + return fs, nil + } + } + return nil, ErrNotFound +} + +func (c *FakeCloudProvider) WaitForFileSystemAvailable(ctx context.Context, fileSystemId string) error { + return nil +} diff --git a/pkg/cloud/mocks/mock_fsx.go b/pkg/cloud/mocks/mock_fsx.go new file mode 100644 index 00000000..b5d4123a --- /dev/null +++ b/pkg/cloud/mocks/mock_fsx.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud (interfaces: FSx) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + aws "github.com/aws/aws-sdk-go/aws" + request "github.com/aws/aws-sdk-go/aws/request" + fsx "github.com/aws/aws-sdk-go/service/fsx" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockFSx is a mock of FSx interface +type MockFSx struct { + ctrl *gomock.Controller + recorder *MockFSxMockRecorder +} + +// MockFSxMockRecorder is the mock recorder for MockFSx +type MockFSxMockRecorder struct { + mock *MockFSx +} + +// NewMockFSx creates a new mock instance +func NewMockFSx(ctrl *gomock.Controller) *MockFSx { + mock := &MockFSx{ctrl: ctrl} + mock.recorder = &MockFSxMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockFSx) EXPECT() *MockFSxMockRecorder { + return m.recorder +} + +// CreateFileSystemWithContext mocks base method +func (m *MockFSx) CreateFileSystemWithContext(arg0 aws.Context, arg1 *fsx.CreateFileSystemInput, arg2 ...request.Option) (*fsx.CreateFileSystemOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateFileSystemWithContext", varargs...) + ret0, _ := ret[0].(*fsx.CreateFileSystemOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateFileSystemWithContext indicates an expected call of CreateFileSystemWithContext +func (mr *MockFSxMockRecorder) CreateFileSystemWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFileSystemWithContext", reflect.TypeOf((*MockFSx)(nil).CreateFileSystemWithContext), varargs...) +} + +// DeleteFileSystemWithContext mocks base method +func (m *MockFSx) DeleteFileSystemWithContext(arg0 aws.Context, arg1 *fsx.DeleteFileSystemInput, arg2 ...request.Option) (*fsx.DeleteFileSystemOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteFileSystemWithContext", varargs...) + ret0, _ := ret[0].(*fsx.DeleteFileSystemOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteFileSystemWithContext indicates an expected call of DeleteFileSystemWithContext +func (mr *MockFSxMockRecorder) DeleteFileSystemWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFileSystemWithContext", reflect.TypeOf((*MockFSx)(nil).DeleteFileSystemWithContext), varargs...) +} + +// DescribeFileSystemsWithContext mocks base method +func (m *MockFSx) DescribeFileSystemsWithContext(arg0 aws.Context, arg1 *fsx.DescribeFileSystemsInput, arg2 ...request.Option) (*fsx.DescribeFileSystemsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeFileSystemsWithContext", varargs...) + ret0, _ := ret[0].(*fsx.DescribeFileSystemsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeFileSystemsWithContext indicates an expected call of DescribeFileSystemsWithContext +func (mr *MockFSxMockRecorder) DescribeFileSystemsWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeFileSystemsWithContext", reflect.TypeOf((*MockFSx)(nil).DescribeFileSystemsWithContext), varargs...) +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go new file mode 100644 index 00000000..ed2cb06b --- /dev/null +++ b/pkg/driver/controller.go @@ -0,0 +1,207 @@ +/* +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. +*/ + +package driver + +import ( + "context" + "strings" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" + "github.com/golang/glog" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/util" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + // controllerCaps represents the capability of controller service + controllerCaps = []csi.ControllerServiceCapability_RPC_Type{ + csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, + } +) + +func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { + glog.V(4).Infof("CreateVolume: called with args %#v", req) + volName := req.GetName() + if len(volName) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume name not provided") + } + + volCaps := req.GetVolumeCapabilities() + if len(volCaps) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume capabilities not provided") + } + + if !d.isValidVolumeCapabilities(volCaps) { + return nil, status.Error(codes.InvalidArgument, "Volume capabilities not supported") + } + + // create a new volume with idempotency + // idempotency is handled by `CreateFileSystem` + capRange := req.GetCapacityRange() + var volumeSizeGiB int64 + if capRange == nil { + volumeSizeGiB = cloud.DefaultVolumeSize + } else { + volumeSizeGiB = util.RoundUp3600GiB(capRange.GetRequiredBytes()) + } + + volumeParams := req.GetParameters() + subnetId := volumeParams["subnetId"] + securityGroupIds := volumeParams["securityGroupIds"] + fsOptions := &cloud.FileSystemOptions{ + CapacityGiB: volumeSizeGiB, + SubnetId: subnetId, + SecurityGroupIds: strings.Split(securityGroupIds, ","), + } + fs, err := d.cloud.CreateFileSystem(ctx, volName, fsOptions) + if err != nil { + switch err { + case cloud.ErrFsExistsDiffSize: + return nil, status.Error(codes.AlreadyExists, err.Error()) + default: + return nil, status.Errorf(codes.Internal, "Could not create volume %q: %v", volName, err) + } + } + + err = d.cloud.WaitForFileSystemAvailable(ctx, fs.FileSystemId) + if err != nil { + return nil, status.Errorf(codes.Internal, "Filesystem is not ready: %v", err) + } + + return newCreatVolumeResponse(fs), nil +} + +func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { + glog.V(4).Infof("DeleteVolume: called with args: %#v", req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } + + if err := d.cloud.DeleteFileSystem(ctx, volumeID); err != nil { + if err == cloud.ErrNotFound { + glog.V(4).Infof("DeleteVolume: volume not found, returning with success") + return &csi.DeleteVolumeResponse{}, nil + } + return nil, status.Errorf(codes.Internal, "Could not delete volume ID %q: %v", volumeID, err) + } + return &csi.DeleteVolumeResponse{}, nil +} + +func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) { + glog.V(4).Infof("ControllerGetCapabilities: called with args %#v", req) + var caps []*csi.ControllerServiceCapability + for _, cap := range controllerCaps { + c := &csi.ControllerServiceCapability{ + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: cap, + }, + }, + } + caps = append(caps, c) + } + return &csi.ControllerGetCapabilitiesResponse{Capabilities: caps}, nil +} + +func (d *Driver) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) { + glog.V(4).Infof("GetCapacity: called with args %#v", req) + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) ListVolumes(ctx context.Context, req *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) { + glog.V(4).Infof("ListVolumes: called with args %#v", req) + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) ValidateVolumeCapabilities(ctx context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) { + glog.V(4).Infof("ValidateVolumeCapabilities: called with args %#v", req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } + + volCaps := req.GetVolumeCapabilities() + if len(volCaps) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume capabilities not provided") + } + + if _, err := d.cloud.DescribeFileSystem(ctx, volumeID); err != nil { + if err == cloud.ErrNotFound { + return nil, status.Error(codes.NotFound, "Volume not found") + } + return nil, status.Errorf(codes.Internal, "Could not get volume with ID %q: %v", volumeID, err) + } + + supported := d.isValidVolumeCapabilities(volCaps) + return &csi.ValidateVolumeCapabilitiesResponse{ + Supported: supported, + }, nil +} + +func (d *Driver) isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) bool { + hasSupport := func(cap *csi.VolumeCapability) bool { + for _, c := range volumeCaps { + if c.GetMode() == cap.AccessMode.GetMode() { + return true + } + } + return false + } + + foundAll := true + for _, c := range volCaps { + if !hasSupport(c) { + foundAll = false + } + } + return foundAll +} + +func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) ListSnapshots(ctx context.Context, req *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func newCreatVolumeResponse(fs *cloud.FileSystem) *csi.CreateVolumeResponse { + return &csi.CreateVolumeResponse{ + Volume: &csi.Volume{ + Id: fs.FileSystemId, + CapacityBytes: util.GiBToBytes(fs.CapacityGiB), + Attributes: map[string]string{ + "dnsname": fs.DnsName, + }, + }, + } +} diff --git a/pkg/driver/controller_test.go b/pkg/driver/controller_test.go new file mode 100644 index 00000000..fdf409ce --- /dev/null +++ b/pkg/driver/controller_test.go @@ -0,0 +1,447 @@ +/* +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. +*/ + +package driver + +import ( + "context" + "errors" + "testing" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" + "github.com/golang/mock/gomock" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/driver/mocks" +) + +func TestCreateVolume(t *testing.T) { + + var ( + endpoint = "endpoint" + volumeName = "volumeName" + fileSystemId = "fs-1234" + volumeSizeGiB int64 = 3600 + subnetId = "subnet-056da83524edbe641" + securityGroupIds = "sg-086f61ea73388fb6b,sg-0145e55e976000c9e" + dnsname = "test.fsx.us-west-2.amazoawd.com" + stdVolCap = &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + }, + } + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + Parameters: map[string]string{ + "subnetId": subnetId, + "securityGroupIds": securityGroupIds, + }, + } + + ctx := context.Background() + fs := &cloud.FileSystem{ + FileSystemId: fileSystemId, + CapacityGiB: volumeSizeGiB, + DnsName: dnsname, + } + mockCloud.EXPECT().CreateFileSystem(gomock.Eq(ctx), gomock.Eq(volumeName), gomock.Any()).Return(fs, nil) + mockCloud.EXPECT().WaitForFileSystemAvailable(gomock.Eq(ctx), gomock.Eq(fileSystemId)).Return(nil) + + resp, err := driver.CreateVolume(ctx, req) + if err != nil { + t.Fatalf("CreateVolume is failed: %v", err) + } + + if resp.Volume == nil { + t.Fatal("resp.Volume is nil") + } + + if resp.Volume.Id != fileSystemId { + t.Fatalf("VolumeId mismatches. actual: %v expected: %v", resp.Volume.Id, fileSystemId) + } + + if resp.Volume.CapacityBytes == 0 { + t.Fatalf("resp.Volume.CapacityGiB is zero") + } + + name, exists := resp.Volume.Attributes["dnsname"] + if !exists { + t.Fatal("dnsname is missing") + } + + if name != dnsname { + t.Fatalf("dnaname mismatches. actual: %v expected: %v", name, dnsname) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: volume name missing", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.CreateVolumeRequest{ + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + Parameters: map[string]string{ + "subnetId": subnetId, + "securityGroupIds": securityGroupIds, + }, + } + + ctx := context.Background() + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume is not failed") + } + + mockCtl.Finish() + }, + }, + { + name: "fail: volume capacity missing", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + Parameters: map[string]string{ + "subnetId": subnetId, + "securityGroupIds": securityGroupIds, + }, + } + + ctx := context.Background() + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume is not failed") + } + + mockCtl.Finish() + }, + }, + { + name: "fail: CreateFileSystem return error", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + Parameters: map[string]string{ + "subnetId": subnetId, + "securityGroupIds": securityGroupIds, + }, + } + + ctx := context.Background() + mockCloud.EXPECT().CreateFileSystem(gomock.Eq(ctx), gomock.Eq(volumeName), gomock.Any()).Return(nil, cloud.ErrFsExistsDiffSize) + + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume is not failed") + } + + mockCtl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + +func TestDeleteVolume(t *testing.T) { + var ( + endpoint = "endpoint" + fileSystemId = "fs-1234" + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "sucess: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.DeleteVolumeRequest{ + VolumeId: fileSystemId, + } + + ctx := context.Background() + + mockCloud.EXPECT().DeleteFileSystem(gomock.Eq(ctx), gomock.Eq(fileSystemId)).Return(nil) + _, err := driver.DeleteVolume(ctx, req) + if err != nil { + t.Fatalf("CreateVolume is failed: %v", err) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: volume ID is missing", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.DeleteVolumeRequest{} + + ctx := context.Background() + _, err := driver.DeleteVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume is not failed") + } + + mockCtl.Finish() + }, + }, + { + name: "success: DeleteFileSystem returns ErrNotFound", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.DeleteVolumeRequest{ + VolumeId: fileSystemId, + } + + ctx := context.Background() + mockCloud.EXPECT().DeleteFileSystem(gomock.Eq(ctx), gomock.Eq(fileSystemId)).Return(cloud.ErrNotFound) + _, err := driver.DeleteVolume(ctx, req) + if err != nil { + t.Fatalf("CreateVolume is failed: %v", err) + } + + mockCtl.Finish() + }, + }, + { + name: "fail: DeleteFileSystem returns other error", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + req := &csi.DeleteVolumeRequest{ + VolumeId: fileSystemId, + } + + ctx := context.Background() + mockCloud.EXPECT().DeleteFileSystem(gomock.Eq(ctx), gomock.Eq(fileSystemId)).Return(errors.New("DeleteFileSystem failed")) + _, err := driver.DeleteVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume is not failed") + } + + mockCtl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + +func TestControllerGetCapabilities(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + endpoint := "endpoint" + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + ctx := context.Background() + _, err := driver.ControllerGetCapabilities(ctx, &csi.ControllerGetCapabilitiesRequest{}) + if err != nil { + t.Fatalf("ControllerGetCapabilities is failed: %v", err) + } +} + +func TestValidateVolumeCapabilities(t *testing.T) { + + var ( + endpoint = "endpoint" + fileSystemId = "fs-12345" + stdVolCap = &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + }, + } + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + ctx := context.Background() + + fs := &cloud.FileSystem{} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Eq(fileSystemId)).Return(fs, nil) + + req := &csi.ValidateVolumeCapabilitiesRequest{ + VolumeId: fileSystemId, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + } + + resp, err := driver.ValidateVolumeCapabilities(ctx, req) + if err != nil { + t.Fatalf("ControllerGetCapabilities is failed: %v", err) + } + if !resp.Supported { + t.Fatal("capability is not supported") + } + mockCtl.Finish() + }, + }, + { + name: "fail: volume ID is missing", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + ctx := context.Background() + req := &csi.ValidateVolumeCapabilitiesRequest{ + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + } + + _, err := driver.ValidateVolumeCapabilities(ctx, req) + if err == nil { + t.Fatal("ControllerGetCapabilities is not failed") + } + + mockCtl.Finish() + }, + }, + { + name: "fail: volueme capability is missing", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + } + + ctx := context.Background() + req := &csi.ValidateVolumeCapabilitiesRequest{ + VolumeId: fileSystemId, + } + + _, err := driver.ValidateVolumeCapabilities(ctx, req) + if err == nil { + t.Fatal("ControllerGetCapabilities is not failed") + } + + mockCtl.Finish() + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 203e447a..671ba575 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -49,10 +49,11 @@ var ( type Driver struct { endpoint string - nodeID string + srv *grpc.Server - srv *grpc.Server + cloud cloud.Cloud + nodeID string mounter mount.Interface } @@ -65,6 +66,7 @@ func NewDriver(endpoint string) *Driver { return &Driver{ endpoint: endpoint, nodeID: cloud.GetMetadata().GetInstanceID(), + cloud: cloud, mounter: newSafeMounter(), } } @@ -93,12 +95,18 @@ func (d *Driver) Run() error { d.srv = grpc.NewServer(opts...) csi.RegisterIdentityServer(d.srv, d) + csi.RegisterControllerServer(d.srv, d) csi.RegisterNodeServer(d.srv, d) glog.Infof("Listening for connections on address: %#v", listener.Addr()) return d.srv.Serve(listener) } +func (d *Driver) Stop() { + glog.Infof("Stopping server") + d.srv.Stop() +} + func newSafeMounter() *mount.SafeFormatAndMount { return &mount.SafeFormatAndMount{ Interface: mount.New(""), diff --git a/pkg/driver/fakes.go b/pkg/driver/fakes.go new file mode 100644 index 00000000..693973a0 --- /dev/null +++ b/pkg/driver/fakes.go @@ -0,0 +1,43 @@ +/* +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. +*/ + +package driver + +import ( + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud" + "k8s.io/kubernetes/pkg/util/mount" +) + +func NewFakeMounter() *mount.SafeFormatAndMount { + return &mount.SafeFormatAndMount{ + Interface: &mount.FakeMounter{ + MountPoints: []mount.MountPoint{}, + Log: []mount.FakeAction{}, + }, + Exec: mount.NewFakeExec(nil), + } +} + +// NewFakeDriver creates a new mock driver used for testing +func NewFakeDriver(endpoint string) *Driver { + cloud := cloud.NewFakeCloudProvider() + return &Driver{ + endpoint: endpoint, + nodeID: cloud.GetMetadata().GetInstanceID(), + cloud: cloud, + mounter: NewFakeMounter(), + } +} diff --git a/pkg/driver/identity.go b/pkg/driver/identity.go index 69018311..365e16f8 100644 --- a/pkg/driver/identity.go +++ b/pkg/driver/identity.go @@ -32,7 +32,17 @@ func (d *Driver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoReques } func (d *Driver) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { - resp := &csi.GetPluginCapabilitiesResponse{} + resp := &csi.GetPluginCapabilitiesResponse{ + Capabilities: []*csi.PluginCapability{ + { + Type: &csi.PluginCapability_Service_{ + Service: &csi.PluginCapability_Service{ + Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, + }, + }, + }, + }, + } return resp, nil } diff --git a/pkg/driver/mocks/mock_cloud.go b/pkg/driver/mocks/mock_cloud.go new file mode 100644 index 00000000..07a205fe --- /dev/null +++ b/pkg/driver/mocks/mock_cloud.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud (interfaces: Cloud) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + cloud "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/cloud" + reflect "reflect" +) + +// MockCloud is a mock of Cloud interface +type MockCloud struct { + ctrl *gomock.Controller + recorder *MockCloudMockRecorder +} + +// MockCloudMockRecorder is the mock recorder for MockCloud +type MockCloudMockRecorder struct { + mock *MockCloud +} + +// NewMockCloud creates a new mock instance +func NewMockCloud(ctrl *gomock.Controller) *MockCloud { + mock := &MockCloud{ctrl: ctrl} + mock.recorder = &MockCloudMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockCloud) EXPECT() *MockCloudMockRecorder { + return m.recorder +} + +// CreateFileSystem mocks base method +func (m *MockCloud) CreateFileSystem(arg0 context.Context, arg1 string, arg2 *cloud.FileSystemOptions) (*cloud.FileSystem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateFileSystem", arg0, arg1, arg2) + ret0, _ := ret[0].(*cloud.FileSystem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateFileSystem indicates an expected call of CreateFileSystem +func (mr *MockCloudMockRecorder) CreateFileSystem(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFileSystem", reflect.TypeOf((*MockCloud)(nil).CreateFileSystem), arg0, arg1, arg2) +} + +// DeleteFileSystem mocks base method +func (m *MockCloud) DeleteFileSystem(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFileSystem", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFileSystem indicates an expected call of DeleteFileSystem +func (mr *MockCloudMockRecorder) DeleteFileSystem(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFileSystem", reflect.TypeOf((*MockCloud)(nil).DeleteFileSystem), arg0, arg1) +} + +// DescribeFileSystem mocks base method +func (m *MockCloud) DescribeFileSystem(arg0 context.Context, arg1 string) (*cloud.FileSystem, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeFileSystem", arg0, arg1) + ret0, _ := ret[0].(*cloud.FileSystem) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeFileSystem indicates an expected call of DescribeFileSystem +func (mr *MockCloudMockRecorder) DescribeFileSystem(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeFileSystem", reflect.TypeOf((*MockCloud)(nil).DescribeFileSystem), arg0, arg1) +} + +// GetMetadata mocks base method +func (m *MockCloud) GetMetadata() cloud.MetadataService { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMetadata") + ret0, _ := ret[0].(cloud.MetadataService) + return ret0 +} + +// GetMetadata indicates an expected call of GetMetadata +func (mr *MockCloudMockRecorder) GetMetadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*MockCloud)(nil).GetMetadata)) +} + +// WaitForFileSystemAvailable mocks base method +func (m *MockCloud) WaitForFileSystemAvailable(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitForFileSystemAvailable", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitForFileSystemAvailable indicates an expected call of WaitForFileSystemAvailable +func (mr *MockCloudMockRecorder) WaitForFileSystemAvailable(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForFileSystemAvailable", reflect.TypeOf((*MockCloud)(nil).WaitForFileSystemAvailable), arg0, arg1) +} diff --git a/pkg/driver/mocks/mock_mount.go b/pkg/driver/mocks/mock_mount.go index 87ece8ca..4d16510c 100644 --- a/pkg/driver/mocks/mock_mount.go +++ b/pkg/driver/mocks/mock_mount.go @@ -5,11 +5,10 @@ package mocks import ( - os "os" - reflect "reflect" - gomock "github.com/golang/mock/gomock" mount "k8s.io/kubernetes/pkg/util/mount" + os "os" + reflect "reflect" ) // MockInterface is a mock of Interface interface diff --git a/pkg/driver/node.go b/pkg/driver/node.go index 6e1e908d..1b569162 100644 --- a/pkg/driver/node.go +++ b/pkg/driver/node.go @@ -42,6 +42,11 @@ func (d *Driver) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolu func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { glog.V(4).Infof("NodePublishVolume: called with args %+v", req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } + attributes := req.GetVolumeAttributes() dnsname := attributes["dnsname"] if len(dnsname) == 0 { @@ -86,6 +91,10 @@ func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolu func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { glog.V(4).Infof("NodeUnpublishVolume: called with args %+v", req) + volumeID := req.GetVolumeId() + if len(volumeID) == 0 { + return nil, status.Error(codes.InvalidArgument, "Volume ID not provided") + } target := req.GetTargetPath() if len(target) == 0 { return nil, status.Error(codes.InvalidArgument, "Target path not provided") @@ -130,22 +139,3 @@ func (d *Driver) NodeGetId(ctx context.Context, req *csi.NodeGetIdRequest) (*csi NodeId: d.nodeID, }, nil } - -func (d *Driver) isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) bool { - hasSupport := func(cap *csi.VolumeCapability) bool { - for _, c := range volumeCaps { - if c.GetMode() == cap.AccessMode.GetMode() { - return true - } - } - return false - } - - foundAll := true - for _, c := range volCaps { - if !hasSupport(c) { - foundAll = false - } - } - return foundAll -} diff --git a/pkg/util/util.go b/pkg/util/util.go index 3bb2f827..56c0f221 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -25,6 +25,21 @@ import ( "strings" ) +const ( + GiB = 1024 * 1024 * 1024 +) + +// RoundUp3600GiB rounds up the volume size in bytes upto +// multiplications of 3600 GiB in the unit of GiB +func RoundUp3600GiB(volumeSizeBytes int64) int64 { + return roundUpSize(volumeSizeBytes, 3600*GiB) * 3600 +} + +// GiBToBytes converts GiB to Bytes +func GiBToBytes(volumeSizeGiB int64) int64 { + return volumeSizeGiB * GiB +} + func ParseEndpoint(endpoint string) (string, string, error) { u, err := url.Parse(endpoint) if err != nil { @@ -47,3 +62,7 @@ func ParseEndpoint(endpoint string) (string, string, error) { return scheme, addr, nil } + +func roundUpSize(volumeSizeBytes int64, allocationUnitBytes int64) int64 { + return (volumeSizeBytes + allocationUnitBytes - 1) / allocationUnitBytes +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 00000000..a36c4dd0 --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,71 @@ +/* +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. +*/ + +package util + +import "testing" + +func TestGiBToBytes(t *testing.T) { + var sizeInGiB int64 = 3 + + actual := GiBToBytes(sizeInGiB) + if actual != 3*GiB { + t.Fatalf("Wrong result for GiBToBytes. Got: %d", actual) + } +} + +func TestRoundUp3600GiB(t *testing.T) { + testCases := []struct { + name string + sizeInBytes int64 + expected int64 + }{ + { + name: "Roundup 1 byte", + sizeInBytes: 1, + expected: 3600, + }, + { + name: "Roundup 1 Gib", + sizeInBytes: 1 * GiB, + expected: 3600, + }, + { + name: "Roundup 2000 Gib", + sizeInBytes: 2000 * GiB, + expected: 3600, + }, + { + name: "Roundup 3600 Gib", + sizeInBytes: 3600 * GiB, + expected: 3600, + }, + { + name: "Roundup 3600 Gib + 1 Byte", + sizeInBytes: 3600*GiB + 1, + expected: 7200, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := RoundUp3600GiB(tc.sizeInBytes) + if actual != tc.expected { + t.Fatalf("RoundUp3600GiB got wrong result. actual: %d, expected: %d", actual, tc.expected) + } + }) + } +} diff --git a/tests/sanity/sanity_test.go b/tests/sanity/sanity_test.go new file mode 100644 index 00000000..91fbe959 --- /dev/null +++ b/tests/sanity/sanity_test.go @@ -0,0 +1,63 @@ +/* +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. +*/ + +package sanity + +import ( + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + sanity "github.com/kubernetes-csi/csi-test/pkg/sanity" + + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/driver" + "github.com/kubernetes-sigs/aws-fsx-csi-driver/pkg/util" +) + +const ( + mountPath = "/tmp/csi/mount" + stagePath = "/tmp/csi/stage" + socket = "/tmp/csi.sock" + endpoint = "unix://" + socket +) + +var fsxDriver *driver.Driver + +func TestSanity(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sanity Tests Suite") +} + +var _ = BeforeSuite(func() { + fsxDriver = driver.NewFakeDriver(endpoint) + go func() { + Expect(fsxDriver.Run()).NotTo(HaveOccurred()) + }() +}) + +var _ = AfterSuite(func() { + fsxDriver.Stop() + Expect(os.RemoveAll(socket)).NotTo(HaveOccurred()) +}) + +var _ = Describe("AWS FSx for Lustre CSI Driver", func() { + config := &sanity.Config{ + Address: endpoint, + TargetPath: mountPath, + StagingPath: stagePath, + TestVolumeSize: 2000 * util.GiB, + } + sanity.GinkgoTest(config) +})