diff --git a/cmd/main.go b/cmd/main.go index e56879f8e..1d54a4131 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,8 +16,19 @@ limitations under the License. package main -import "fmt" +import ( + "flag" + + "github.com/golang/glog" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/driver" +) func main() { - fmt.Println("efs driver") + var endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI Endpoint") + flag.Parse() + + drv := driver.NewDriver(*endpoint) + if err := drv.Run(); err != nil { + glog.Fatalln(err) + } } diff --git a/deploy/kubernetes/attacher.yaml b/deploy/kubernetes/attacher.yaml new file mode 100644 index 000000000..a105d6bb6 --- /dev/null +++ b/deploy/kubernetes/attacher.yaml @@ -0,0 +1,122 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: csi-attacher-sa + namespace: default + +--- + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: external-attacher-runner + namespace: default +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["events"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments"] + verbs: ["get", "list", "watch", "update"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-attacher-role + namespace: default +subjects: + - kind: ServiceAccount + name: csi-attacher-sa + namespace: default +roleRef: + kind: ClusterRole + name: external-attacher-runner + apiGroup: rbac.authorization.k8s.io + +--- + +kind: Service +apiVersion: v1 +metadata: + name: csi-attacher + labels: + app: csi-attacher +spec: + selector: + app: csi-attacher + clusterIP: None +--- + +kind: StatefulSet +apiVersion: apps/v1beta1 +metadata: + name: csi-attacher +spec: + serviceName: "csi-attacher" + replicas: 1 + template: + metadata: + labels: + app: csi-attacher + spec: + serviceAccount: csi-attacher-sa + containers: + - name: csi-attacher + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN"] + allowPrivilegeEscalation: true + image: quay.io/k8scsi/csi-attacher:v0.4.1 + args: + - --v=5 + - --csi-address=$(ADDRESS) + env: + - name: ADDRESS + value: /var/lib/csi/sockets/pluginproxy/csi.sock + imagePullPolicy: Always + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + - name: efs-plugin + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN"] + allowPrivilegeEscalation: true + image: chengpan/aws-efs-csi-driver:testing + args : + - --endpoint=$(CSI_ENDPOINT) + - --logtostderr + - --v=5 + 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 + imagePullPolicy: Always + volumeMounts: + - name: socket-dir + mountPath: /var/lib/csi/sockets/pluginproxy/ + volumes: + - name: socket-dir + emptyDir: {} diff --git a/deploy/kubernetes/node.yaml b/deploy/kubernetes/node.yaml new file mode 100644 index 000000000..1a8ae10c0 --- /dev/null +++ b/deploy/kubernetes/node.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: csi-node-sa + namespace: default + +--- + +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-node + namespace: default +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list", "update"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["volumeattachments"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["csi.storage.k8s.io"] + resources: ["csinodeinfos"] + verbs: ["get", "list", "watch", "update"] + +--- + +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: csi-node + namespace: default +subjects: + - kind: ServiceAccount + name: csi-node-sa + namespace: default +roleRef: + kind: ClusterRole + name: csi-node + apiGroup: rbac.authorization.k8s.io + +--- + +kind: DaemonSet +apiVersion: apps/v1beta2 +metadata: + name: csi-node +spec: + selector: + matchLabels: + app: csi-node + template: + metadata: + labels: + app: csi-node + spec: + serviceAccount: csi-node-sa + hostNetwork: true + containers: + - name: csi-driver-registrar + securityContext: + privileged: true + imagePullPolicy: Always + image: quay.io/k8scsi/driver-registrar:v0.4.1 + args: + - --v=5 + - --csi-address=$(ADDRESS) + - --mode=node-register + - --driver-requires-attachment=true + - --pod-info-mount-version="v1" + - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH) + env: + - name: ADDRESS + value: /csi/csi.sock + - name: DRIVER_REG_SOCK_PATH + value: /var/lib/kubelet/plugins/efs.csi.aws.com/csi.sock + - name: KUBE_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + volumeMounts: + - name: plugin-dir + mountPath: /csi + - name: registration-dir + mountPath: /registration + - name: efs-plugin + securityContext: + privileged: true + imagePullPolicy: Always + image: chengpan/aws-efs-csi-driver:testing + args: + - --endpoint=$(CSI_ENDPOINT) + - --logtostderr + - --v=5 + env: + - name: CSI_ENDPOINT + value: unix:/csi/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: kubelet-dir + mountPath: /var/lib/kubelet + mountPropagation: "Bidirectional" + - name: plugin-dir + mountPath: /csi + - name: device-dir + mountPath: /dev + volumes: + - name: kubelet-dir + hostPath: + path: /var/lib/kubelet + type: Directory + - name: plugin-dir + hostPath: + path: /var/lib/kubelet/plugins/efs.csi.aws.com/ + type: DirectoryOrCreate + - name: registration-dir + hostPath: + path: /var/lib/kubelet/plugins/ + type: Directory + - name: device-dir + hostPath: + path: /dev + type: Directory diff --git a/deploy/kubernetes/sample_app/claim.yaml b/deploy/kubernetes/sample_app/claim.yaml new file mode 100644 index 000000000..44333e075 --- /dev/null +++ b/deploy/kubernetes/sample_app/claim.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: efs-claim +spec: + accessModes: + - ReadWriteOnce + storageClassName: efs-sc + resources: + requests: + storage: 5Gi diff --git a/deploy/kubernetes/sample_app/pod.yaml b/deploy/kubernetes/sample_app/pod.yaml new file mode 100644 index 000000000..d0671dc14 --- /dev/null +++ b/deploy/kubernetes/sample_app/pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: 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: efs-claim diff --git a/deploy/kubernetes/sample_app/pv.yaml b/deploy/kubernetes/sample_app/pv.yaml new file mode 100644 index 000000000..23205c8fa --- /dev/null +++ b/deploy/kubernetes/sample_app/pv.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: efs-pv +spec: + capacity: + storage: 5Gi + volumeMode: Filesystem + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Recycle + storageClassName: efs-sc + csi: + driver: efs.csi.aws.com + volumeHandle: fs-ff2a9557 diff --git a/deploy/kubernetes/sample_app/storageclass.yaml b/deploy/kubernetes/sample_app/storageclass.yaml new file mode 100644 index 000000000..7c3d9680b --- /dev/null +++ b/deploy/kubernetes/sample_app/storageclass.yaml @@ -0,0 +1,5 @@ +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: efs-sc +provisioner: efs.csi.aws.com diff --git a/deploy/kubernetes/secret.yaml b/deploy/kubernetes/secret.yaml new file mode 100644 index 000000000..9d469cf54 --- /dev/null +++ b/deploy/kubernetes/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: aws-secret +stringData: + key_id: + access_key: diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d71bc3fe0 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/kubernetes-sigs/aws-efs-csi-driver + +require ( + github.com/aws/aws-sdk-go v1.16.5 + github.com/container-storage-interface/spec v0.3.0 + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/golang/mock v1.2.0 + github.com/golang/protobuf v1.2.0 + golang.org/x/net v0.0.0-20180826012351-8a410e7b638d + google.golang.org/grpc v1.17.0 + k8s.io/apimachinery v0.0.0-20181211025822-57dc7e687b54 // indirect + k8s.io/klog v0.1.0 + k8s.io/kubernetes v1.13.1 + k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..6b817827e --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/container-storage-interface/spec v1.0.0 h1:3DyXuJgf9MU6kyULESegQUmozsSxhpyrrv9u5bfwA3E= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +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/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/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20180828015842-6cd1fcedba52/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.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/apimachinery v0.0.0-20181211025822-57dc7e687b54 h1:2O23KCqjI/ISedkxmAkVTb/+Of8oKfy67lmRo3rLFbQ= +k8s.io/apimachinery v0.0.0-20181211025822-57dc7e687b54/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= diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go new file mode 100644 index 000000000..9763df886 --- /dev/null +++ b/pkg/cloud/cloud.go @@ -0,0 +1,53 @@ +/* +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 ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" +) + +type Cloud interface { + GetMetadata() MetadataService +} + +type cloud struct { + metadata MetadataService +} + +// NewCloud returns a new instance of AWS cloud +// It panics if session is invalid +func NewCloud() (Cloud, error) { + sess := session.Must(session.NewSession(&aws.Config{})) + svc := ec2metadata.New(sess) + + metadata, err := NewMetadataService(svc) + if err != nil { + return nil, fmt.Errorf("could not get metadata from AWS: %v", err) + } + + return &cloud{ + metadata: metadata, + }, nil +} + +func (c *cloud) GetMetadata() MetadataService { + return c.metadata +} diff --git a/pkg/cloud/metadata.go b/pkg/cloud/metadata.go new file mode 100644 index 000000000..0d6324024 --- /dev/null +++ b/pkg/cloud/metadata.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 ( + "fmt" + + "github.com/aws/aws-sdk-go/aws/ec2metadata" +) + +type EC2Metadata interface { + Available() bool + GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error) +} + +// MetadataService represents AWS metadata service. +type MetadataService interface { + GetInstanceID() string + GetRegion() string + GetAvailabilityZone() string +} + +type metadata struct { + instanceID string + region string + availabilityZone string +} + +var _ MetadataService = &metadata{} + +// GetInstanceID returns the instance identification. +func (m *metadata) GetInstanceID() string { + return m.instanceID +} + +// GetRegion returns the region Zone which the instance is in. +func (m *metadata) GetRegion() string { + return m.region +} + +// GetAvailabilityZone returns the Availability Zone which the instance is in. +func (m *metadata) GetAvailabilityZone() string { + return m.availabilityZone +} + +// NewMetadataService returns a new MetadataServiceImplementation. +func NewMetadataService(svc EC2Metadata) (MetadataService, error) { + if !svc.Available() { + return nil, fmt.Errorf("EC2 instance metadata is not available") + } + + doc, err := svc.GetInstanceIdentityDocument() + if err != nil { + return nil, fmt.Errorf("could not get EC2 instance identity metadata") + } + + if len(doc.InstanceID) == 0 { + return nil, fmt.Errorf("could not get valid EC2 instance ID") + } + + if len(doc.Region) == 0 { + return nil, fmt.Errorf("could not get valid EC2 region") + } + + if len(doc.AvailabilityZone) == 0 { + return nil, fmt.Errorf("could not get valid EC2 availavility zone") + } + + return &metadata{ + instanceID: doc.InstanceID, + region: doc.Region, + availabilityZone: doc.AvailabilityZone, + }, nil +} diff --git a/pkg/cloud/metadata_test.go b/pkg/cloud/metadata_test.go new file mode 100644 index 000000000..05d081ff9 --- /dev/null +++ b/pkg/cloud/metadata_test.go @@ -0,0 +1,143 @@ +/* +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 ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/golang/mock/gomock" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud/mocks" +) + +var ( + stdInstanceID = "instance-1" + stdRegion = "instance-1" + stdAvailabilityZone = "az-1" +) + +func TestNewMetadataService(t *testing.T) { + testCases := []struct { + name string + isAvailable bool + isPartial bool + identityDocument ec2metadata.EC2InstanceIdentityDocument + err error + }{ + { + name: "success: normal", + isAvailable: true, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: stdInstanceID, + Region: stdRegion, + AvailabilityZone: stdAvailabilityZone, + }, + err: nil, + }, + { + name: "fail: metadata not available", + isAvailable: false, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: stdInstanceID, + Region: stdRegion, + AvailabilityZone: stdAvailabilityZone, + }, + err: nil, + }, + { + name: "fail: GetInstanceIdentityDocument returned error", + isAvailable: true, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: stdInstanceID, + Region: stdRegion, + AvailabilityZone: stdAvailabilityZone, + }, + err: fmt.Errorf(""), + }, + { + name: "fail: GetInstanceIdentityDocument returned empty instance", + isAvailable: true, + isPartial: true, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: "", + Region: stdRegion, + AvailabilityZone: stdAvailabilityZone, + }, + err: nil, + }, + { + name: "fail: GetInstanceIdentityDocument returned empty region", + isAvailable: true, + isPartial: true, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: stdInstanceID, + Region: "", + AvailabilityZone: stdAvailabilityZone, + }, + err: nil, + }, + { + name: "fail: GetInstanceIdentityDocument returned empty az", + isAvailable: true, + isPartial: true, + identityDocument: ec2metadata.EC2InstanceIdentityDocument{ + InstanceID: stdInstanceID, + Region: stdRegion, + AvailabilityZone: "", + }, + err: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockEC2Metadata := mocks.NewMockEC2Metadata(mockCtrl) + + mockEC2Metadata.EXPECT().Available().Return(tc.isAvailable) + if tc.isAvailable { + mockEC2Metadata.EXPECT().GetInstanceIdentityDocument().Return(tc.identityDocument, tc.err) + } + + m, err := NewMetadataService(mockEC2Metadata) + if tc.isAvailable && tc.err == nil && !tc.isPartial { + if err != nil { + t.Fatalf("NewMetadataService() failed: expected no error, got %v", err) + } + + if m.GetInstanceID() != tc.identityDocument.InstanceID { + t.Fatalf("GetInstanceID() failed: expected %v, got %v", tc.identityDocument.InstanceID, m.GetInstanceID()) + } + + if m.GetRegion() != tc.identityDocument.Region { + t.Fatalf("GetRegion() failed: expected %v, got %v", tc.identityDocument.Region, m.GetRegion()) + } + + if m.GetAvailabilityZone() != tc.identityDocument.AvailabilityZone { + t.Fatalf("GetAvailabilityZone() failed: expected %v, got %v", tc.identityDocument.AvailabilityZone, m.GetAvailabilityZone()) + } + } else { + if err == nil { + t.Fatal("NewMetadataService() failed: expected error when GetInstanceIdentityDocument returns partial data, got nothing") + } + } + + mockCtrl.Finish() + }) + } +} diff --git a/pkg/cloud/mocks/mock_ec2metadata.go b/pkg/cloud/mocks/mock_ec2metadata.go new file mode 100644 index 000000000..e1e2102e7 --- /dev/null +++ b/pkg/cloud/mocks/mock_ec2metadata.go @@ -0,0 +1,63 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud (interfaces: EC2Metadata) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + ec2metadata "github.com/aws/aws-sdk-go/aws/ec2metadata" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockEC2Metadata is a mock of EC2Metadata interface +type MockEC2Metadata struct { + ctrl *gomock.Controller + recorder *MockEC2MetadataMockRecorder +} + +// MockEC2MetadataMockRecorder is the mock recorder for MockEC2Metadata +type MockEC2MetadataMockRecorder struct { + mock *MockEC2Metadata +} + +// NewMockEC2Metadata creates a new mock instance +func NewMockEC2Metadata(ctrl *gomock.Controller) *MockEC2Metadata { + mock := &MockEC2Metadata{ctrl: ctrl} + mock.recorder = &MockEC2MetadataMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEC2Metadata) EXPECT() *MockEC2MetadataMockRecorder { + return m.recorder +} + +// Available mocks base method +func (m *MockEC2Metadata) Available() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Available") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Available indicates an expected call of Available +func (mr *MockEC2MetadataMockRecorder) Available() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Available", reflect.TypeOf((*MockEC2Metadata)(nil).Available)) +} + +// GetInstanceIdentityDocument mocks base method +func (m *MockEC2Metadata) GetInstanceIdentityDocument() (ec2metadata.EC2InstanceIdentityDocument, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInstanceIdentityDocument") + ret0, _ := ret[0].(ec2metadata.EC2InstanceIdentityDocument) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInstanceIdentityDocument indicates an expected call of GetInstanceIdentityDocument +func (mr *MockEC2MetadataMockRecorder) GetInstanceIdentityDocument() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceIdentityDocument", reflect.TypeOf((*MockEC2Metadata)(nil).GetInstanceIdentityDocument)) +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go new file mode 100644 index 000000000..66964195c --- /dev/null +++ b/pkg/driver/driver.go @@ -0,0 +1,107 @@ +/* +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" + "net" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" + "github.com/golang/glog" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/util" + "google.golang.org/grpc" + "k8s.io/kubernetes/pkg/util/mount" +) + +const ( + driverName = "efs.csi.aws.com" +) + +var ( + vendorVersion = "0.1.0" +) + +var ( + volumeCaps = []csi.VolumeCapability_AccessMode{ + { + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + { + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + }, + } +) + +type Driver struct { + endpoint string + nodeID string + + srv *grpc.Server + + mounter mount.Interface +} + +func NewDriver(endpoint string) *Driver { + cloud, err := cloud.NewCloud() + if err != nil { + glog.Fatalln(err) + } + + return &Driver{ + endpoint: endpoint, + nodeID: cloud.GetMetadata().GetInstanceID(), + mounter: newSafeMounter(), + } +} + +func (d *Driver) Run() error { + scheme, addr, err := util.ParseEndpoint(d.endpoint) + if err != nil { + return err + } + + listener, err := net.Listen(scheme, addr) + if err != nil { + return err + } + + logErr := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + resp, err := handler(ctx, req) + if err != nil { + glog.Errorf("GRPC error: %v", err) + } + return resp, err + } + opts := []grpc.ServerOption{ + grpc.UnaryInterceptor(logErr), + } + d.srv = grpc.NewServer(opts...) + + csi.RegisterIdentityServer(d.srv, d) + csi.RegisterNodeServer(d.srv, d) + + glog.Infof("Listening for connections on address: %#v", listener.Addr()) + return d.srv.Serve(listener) +} + +func newSafeMounter() *mount.SafeFormatAndMount { + return &mount.SafeFormatAndMount{ + Interface: mount.New(""), + Exec: mount.NewOsExec(), + } +} diff --git a/pkg/driver/identity.go b/pkg/driver/identity.go new file mode 100644 index 000000000..69018311e --- /dev/null +++ b/pkg/driver/identity.go @@ -0,0 +1,42 @@ +/* +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" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" +) + +func (d *Driver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { + resp := &csi.GetPluginInfoResponse{ + Name: driverName, + VendorVersion: vendorVersion, + } + + return resp, nil +} + +func (d *Driver) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { + resp := &csi.GetPluginCapabilitiesResponse{} + + return resp, nil +} + +func (d *Driver) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) { + return &csi.ProbeResponse{}, nil +} diff --git a/pkg/driver/mocks/mock_mount.go b/pkg/driver/mocks/mock_mount.go new file mode 100644 index 000000000..87ece8ca4 --- /dev/null +++ b/pkg/driver/mocks/mock_mount.go @@ -0,0 +1,359 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: k8s.io/kubernetes/pkg/util/mount (interfaces: Interface) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + os "os" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + mount "k8s.io/kubernetes/pkg/util/mount" +) + +// MockInterface is a mock of Interface interface +type MockInterface struct { + ctrl *gomock.Controller + recorder *MockInterfaceMockRecorder +} + +// MockInterfaceMockRecorder is the mock recorder for MockInterface +type MockInterfaceMockRecorder struct { + mock *MockInterface +} + +// NewMockInterface creates a new mock instance +func NewMockInterface(ctrl *gomock.Controller) *MockInterface { + mock := &MockInterface{ctrl: ctrl} + mock.recorder = &MockInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { + return m.recorder +} + +// CleanSubPaths mocks base method +func (m *MockInterface) CleanSubPaths(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CleanSubPaths", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CleanSubPaths indicates an expected call of CleanSubPaths +func (mr *MockInterfaceMockRecorder) CleanSubPaths(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanSubPaths", reflect.TypeOf((*MockInterface)(nil).CleanSubPaths), arg0, arg1) +} + +// DeviceOpened mocks base method +func (m *MockInterface) DeviceOpened(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeviceOpened", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeviceOpened indicates an expected call of DeviceOpened +func (mr *MockInterfaceMockRecorder) DeviceOpened(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceOpened", reflect.TypeOf((*MockInterface)(nil).DeviceOpened), arg0) +} + +// EvalHostSymlinks mocks base method +func (m *MockInterface) EvalHostSymlinks(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EvalHostSymlinks", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EvalHostSymlinks indicates an expected call of EvalHostSymlinks +func (mr *MockInterfaceMockRecorder) EvalHostSymlinks(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalHostSymlinks", reflect.TypeOf((*MockInterface)(nil).EvalHostSymlinks), arg0) +} + +// ExistsPath mocks base method +func (m *MockInterface) ExistsPath(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExistsPath", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExistsPath indicates an expected call of ExistsPath +func (mr *MockInterfaceMockRecorder) ExistsPath(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsPath", reflect.TypeOf((*MockInterface)(nil).ExistsPath), arg0) +} + +// GetDeviceNameFromMount mocks base method +func (m *MockInterface) GetDeviceNameFromMount(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDeviceNameFromMount", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDeviceNameFromMount indicates an expected call of GetDeviceNameFromMount +func (mr *MockInterfaceMockRecorder) GetDeviceNameFromMount(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeviceNameFromMount", reflect.TypeOf((*MockInterface)(nil).GetDeviceNameFromMount), arg0, arg1) +} + +// GetFSGroup mocks base method +func (m *MockInterface) GetFSGroup(arg0 string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFSGroup", arg0) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFSGroup indicates an expected call of GetFSGroup +func (mr *MockInterfaceMockRecorder) GetFSGroup(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFSGroup", reflect.TypeOf((*MockInterface)(nil).GetFSGroup), arg0) +} + +// GetFileType mocks base method +func (m *MockInterface) GetFileType(arg0 string) (mount.FileType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFileType", arg0) + ret0, _ := ret[0].(mount.FileType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFileType indicates an expected call of GetFileType +func (mr *MockInterfaceMockRecorder) GetFileType(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileType", reflect.TypeOf((*MockInterface)(nil).GetFileType), arg0) +} + +// GetMode mocks base method +func (m *MockInterface) GetMode(arg0 string) (os.FileMode, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMode", arg0) + ret0, _ := ret[0].(os.FileMode) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMode indicates an expected call of GetMode +func (mr *MockInterfaceMockRecorder) GetMode(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMode", reflect.TypeOf((*MockInterface)(nil).GetMode), arg0) +} + +// GetMountRefs mocks base method +func (m *MockInterface) GetMountRefs(arg0 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMountRefs", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMountRefs indicates an expected call of GetMountRefs +func (mr *MockInterfaceMockRecorder) GetMountRefs(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMountRefs", reflect.TypeOf((*MockInterface)(nil).GetMountRefs), arg0) +} + +// GetSELinuxSupport mocks base method +func (m *MockInterface) GetSELinuxSupport(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSELinuxSupport", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSELinuxSupport indicates an expected call of GetSELinuxSupport +func (mr *MockInterfaceMockRecorder) GetSELinuxSupport(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSELinuxSupport", reflect.TypeOf((*MockInterface)(nil).GetSELinuxSupport), arg0) +} + +// IsLikelyNotMountPoint mocks base method +func (m *MockInterface) IsLikelyNotMountPoint(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsLikelyNotMountPoint", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsLikelyNotMountPoint indicates an expected call of IsLikelyNotMountPoint +func (mr *MockInterfaceMockRecorder) IsLikelyNotMountPoint(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLikelyNotMountPoint", reflect.TypeOf((*MockInterface)(nil).IsLikelyNotMountPoint), arg0) +} + +// IsMountPointMatch mocks base method +func (m *MockInterface) IsMountPointMatch(arg0 mount.MountPoint, arg1 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsMountPointMatch", arg0, arg1) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsMountPointMatch indicates an expected call of IsMountPointMatch +func (mr *MockInterfaceMockRecorder) IsMountPointMatch(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMountPointMatch", reflect.TypeOf((*MockInterface)(nil).IsMountPointMatch), arg0, arg1) +} + +// IsNotMountPoint mocks base method +func (m *MockInterface) IsNotMountPoint(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsNotMountPoint", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsNotMountPoint indicates an expected call of IsNotMountPoint +func (mr *MockInterfaceMockRecorder) IsNotMountPoint(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNotMountPoint", reflect.TypeOf((*MockInterface)(nil).IsNotMountPoint), arg0) +} + +// List mocks base method +func (m *MockInterface) List() ([]mount.MountPoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].([]mount.MountPoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List +func (mr *MockInterfaceMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List)) +} + +// MakeDir mocks base method +func (m *MockInterface) MakeDir(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MakeDir", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// MakeDir indicates an expected call of MakeDir +func (mr *MockInterfaceMockRecorder) MakeDir(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeDir", reflect.TypeOf((*MockInterface)(nil).MakeDir), arg0) +} + +// MakeFile mocks base method +func (m *MockInterface) MakeFile(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MakeFile", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// MakeFile indicates an expected call of MakeFile +func (mr *MockInterfaceMockRecorder) MakeFile(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeFile", reflect.TypeOf((*MockInterface)(nil).MakeFile), arg0) +} + +// MakeRShared mocks base method +func (m *MockInterface) MakeRShared(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MakeRShared", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// MakeRShared indicates an expected call of MakeRShared +func (mr *MockInterfaceMockRecorder) MakeRShared(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeRShared", reflect.TypeOf((*MockInterface)(nil).MakeRShared), arg0) +} + +// Mount mocks base method +func (m *MockInterface) Mount(arg0, arg1, arg2 string, arg3 []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mount", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// Mount indicates an expected call of Mount +func (mr *MockInterfaceMockRecorder) Mount(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mount", reflect.TypeOf((*MockInterface)(nil).Mount), arg0, arg1, arg2, arg3) +} + +// PathIsDevice mocks base method +func (m *MockInterface) PathIsDevice(arg0 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PathIsDevice", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PathIsDevice indicates an expected call of PathIsDevice +func (mr *MockInterfaceMockRecorder) PathIsDevice(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PathIsDevice", reflect.TypeOf((*MockInterface)(nil).PathIsDevice), arg0) +} + +// PrepareSafeSubpath mocks base method +func (m *MockInterface) PrepareSafeSubpath(arg0 mount.Subpath) (string, func(), error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrepareSafeSubpath", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(func()) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// PrepareSafeSubpath indicates an expected call of PrepareSafeSubpath +func (mr *MockInterfaceMockRecorder) PrepareSafeSubpath(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareSafeSubpath", reflect.TypeOf((*MockInterface)(nil).PrepareSafeSubpath), arg0) +} + +// SafeMakeDir mocks base method +func (m *MockInterface) SafeMakeDir(arg0, arg1 string, arg2 os.FileMode) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SafeMakeDir", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// SafeMakeDir indicates an expected call of SafeMakeDir +func (mr *MockInterfaceMockRecorder) SafeMakeDir(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeMakeDir", reflect.TypeOf((*MockInterface)(nil).SafeMakeDir), arg0, arg1, arg2) +} + +// Unmount mocks base method +func (m *MockInterface) Unmount(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unmount", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unmount indicates an expected call of Unmount +func (mr *MockInterfaceMockRecorder) Unmount(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmount", reflect.TypeOf((*MockInterface)(nil).Unmount), arg0) +} diff --git a/pkg/driver/node.go b/pkg/driver/node.go new file mode 100644 index 000000000..dc36ecc8d --- /dev/null +++ b/pkg/driver/node.go @@ -0,0 +1,146 @@ +/* +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" + "fmt" + "os" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" + "github.com/golang/glog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + nodeCaps = []csi.NodeServiceCapability_RPC_Type{} +) + +func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +func (d *Driver) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { + return nil, status.Error(codes.Unimplemented, "") +} + +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() + source := fmt.Sprintf("%s:/", volumeId) + + target := req.GetTargetPath() + if len(target) == 0 { + return nil, status.Error(codes.InvalidArgument, "Target path not provided") + } + + volCap := req.GetVolumeCapability() + if volCap == nil { + return nil, status.Error(codes.InvalidArgument, "Volume capability not provided") + } + + if !d.isValidVolumeCapabilities([]*csi.VolumeCapability{volCap}) { + return nil, status.Error(codes.InvalidArgument, "Volume capability not supported") + } + + options := []string{} + if req.GetReadonly() { + options = append(options, "ro") + } + + glog.V(5).Infof("NodePublishVolume: creating dir %s", target) + if err := d.mounter.MakeDir(target); err != nil { + return nil, status.Errorf(codes.Internal, "Could not create dir %q: %v", target, err) + } + + glog.V(5).Infof("NodePublishVolume: mounting %s at %s", source, target) + if err := d.mounter.Mount(source, target, "efs", options); err != nil { + os.Remove(target) + return nil, status.Errorf(codes.Internal, "Could not mount %q at %q: %v", source, target, err) + } + + return &csi.NodePublishVolumeResponse{}, nil +} + +func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { + glog.V(4).Infof("NodeUnpublishVolume: called with args %#v", req) + + target := req.GetTargetPath() + if len(target) == 0 { + return nil, status.Error(codes.InvalidArgument, "Target path not provided") + } + + glog.V(5).Infof("NodeUnpublishVolume: unmounting %s", target) + err := d.mounter.Unmount(target) + if err != nil { + return nil, status.Errorf(codes.Internal, "Could not unmount %q: %v", target, err) + } + + return &csi.NodeUnpublishVolumeResponse{}, nil +} + +func (d *Driver) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { + glog.V(4).Infof("NodeGetCapabilities: called with args %#v", req) + var caps []*csi.NodeServiceCapability + for _, cap := range nodeCaps { + c := &csi.NodeServiceCapability{ + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: cap, + }, + }, + } + caps = append(caps, c) + } + return &csi.NodeGetCapabilitiesResponse{Capabilities: caps}, nil +} + +func (d *Driver) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { + glog.V(4).Infof("NodeGetInfo: called with args %#v", req) + + return &csi.NodeGetInfoResponse{ + NodeId: d.nodeID, + }, nil +} + +func (d *Driver) NodeGetId(ctx context.Context, req *csi.NodeGetIdRequest) (*csi.NodeGetIdResponse, error) { + glog.V(4).Infof("NodeGetId: called with args %#v", req) + return &csi.NodeGetIdResponse{ + 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/driver/node_test.go b/pkg/driver/node_test.go new file mode 100644 index 000000000..4754a4435 --- /dev/null +++ b/pkg/driver/node_test.go @@ -0,0 +1,325 @@ +/* +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" + "fmt" + "testing" + + csi "github.com/container-storage-interface/spec/lib/go/csi/v0" + "github.com/golang/mock/gomock" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/driver/mocks" +) + +func TestNodePublishVolume(t *testing.T) { + + var ( + endpoint = "endpoint" + nodeID = "nodeID" + volumeId = "volumeId" + targetPath = "/target/path" + 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) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + source := volumeId + ":/" + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + VolumeCapability: stdVolCap, + TargetPath: targetPath, + } + + mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) + mockMounter.EXPECT().Mount(gomock.Eq(source), gomock.Eq(targetPath), gomock.Eq("lustre"), gomock.Any()).Return(nil) + _, err := driver.NodePublishVolume(ctx, req) + if err != nil { + t.Fatalf("NodePublishVolume is failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + { + name: "fail: missing target path", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + VolumeCapability: stdVolCap, + } + + _, err := driver.NodePublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodePublishVolume is not failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + { + name: "fail: missing volume capability", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + TargetPath: targetPath, + } + + _, err := driver.NodePublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodePublishVolume is not failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + { + name: "fail: unsupported volume capability", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + VolumeCapability: &csi.VolumeCapability{ + AccessType: &csi.VolumeCapability_Mount{ + Mount: &csi.VolumeCapability_MountVolume{}, + }, + AccessMode: &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY, + }, + }, + TargetPath: targetPath, + } + + _, err := driver.NodePublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodePublishVolume is not failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + { + name: "fail: mounter failed to MakeDir", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + VolumeCapability: stdVolCap, + TargetPath: targetPath, + } + + err := fmt.Errorf("failed to MakeDir") + mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(err) + + _, err = driver.NodePublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodePublishVolume is not failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + { + name: "fail: mounter failed to Mount", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodePublishVolumeRequest{ + VolumeId: volumeId, + VolumeAttributes: map[string]string{}, + VolumeCapability: stdVolCap, + TargetPath: targetPath, + } + source := volumeId + ":/" + + err := fmt.Errorf("failed to Mount") + mockMounter.EXPECT().MakeDir(gomock.Eq(targetPath)).Return(nil) + mockMounter.EXPECT().Mount(gomock.Eq(source), gomock.Eq(targetPath), gomock.Eq("lustre"), gomock.Any()).Return(err) + + _, err = driver.NodePublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodePublishVolume is not failed: %v", err) + } + + mockCtrl.Finish() + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + +func TestNodeUnpublishVolume(t *testing.T) { + + var ( + endpoint = "endpoint" + nodeID = "nodeID" + volumeId = "volumeId" + targetPath = "/target/path" + ) + + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "success: normal", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodeUnpublishVolumeRequest{ + VolumeId: volumeId, + TargetPath: targetPath, + } + + mockMounter.EXPECT().Unmount(gomock.Eq(targetPath)).Return(nil) + + _, err := driver.NodeUnpublishVolume(ctx, req) + if err != nil { + t.Fatalf("NodeUnpublishVolume is failed: %v", err) + } + }, + }, + { + name: "fail: targetPath is missing", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodeUnpublishVolumeRequest{ + VolumeId: volumeId, + } + + _, err := driver.NodeUnpublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodeUnpublishVolume is not failed: %v", err) + } + }, + }, + { + name: "fail: mounter failed to umount", + testFunc: func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockMounter := mocks.NewMockInterface(mockCtrl) + driver := &Driver{ + endpoint: endpoint, + nodeID: nodeID, + mounter: mockMounter, + } + + ctx := context.Background() + req := &csi.NodeUnpublishVolumeRequest{ + VolumeId: volumeId, + TargetPath: targetPath, + } + + mountErr := fmt.Errorf("Unmount failed") + mockMounter.EXPECT().Unmount(gomock.Eq(targetPath)).Return(mountErr) + + _, err := driver.NodeUnpublishVolume(ctx, req) + if err == nil { + t.Fatalf("NodeUnpublishVolume is not failed: %v", err) + } + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 000000000..3bb2f827a --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,49 @@ +/* +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 ( + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "strings" +) + +func ParseEndpoint(endpoint string) (string, string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", "", fmt.Errorf("could not parse endpoint: %v", err) + } + + addr := path.Join(u.Host, filepath.FromSlash(u.Path)) + + scheme := strings.ToLower(u.Scheme) + switch scheme { + case "tcp": + case "unix": + addr = path.Join("/", addr) + if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { + return "", "", fmt.Errorf("could not remove unix domain socket %q: %v", addr, err) + } + default: + return "", "", fmt.Errorf("unsupported protocol: %s", scheme) + } + + return scheme, addr, nil +}