diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 147cf33db..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: go -go_import_path: github.com/kubernetes-sigs/aws-efs-csi-driver - -env: - global: - - GO111MODULE=on - -go: - - "1.13.4" - -before_install: - - go get github.com/mattn/goveralls - -script: - - make - - go test -covermode=count -coverprofile=profile.cov ./pkg/... - - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/charts/aws-efs-csi-driver/templates/node-daemonset.yaml b/charts/aws-efs-csi-driver/templates/node-daemonset.yaml index acaebe22a..5eb8a9bde 100644 --- a/charts/aws-efs-csi-driver/templates/node-daemonset.yaml +++ b/charts/aws-efs-csi-driver/templates/node-daemonset.yaml @@ -47,6 +47,7 @@ spec: {{- with .Values.node.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} + hostNetwork: true dnsPolicy: {{ .Values.node.dnsPolicy }} {{- with .Values.node.dnsConfig }} dnsConfig: {{- toYaml . | nindent 8 }} diff --git a/charts/aws-efs-csi-driver/values.yaml b/charts/aws-efs-csi-driver/values.yaml index 1784503db..eefa68778 100644 --- a/charts/aws-efs-csi-driver/values.yaml +++ b/charts/aws-efs-csi-driver/values.yaml @@ -167,5 +167,7 @@ storageClasses: [] # gidRangeStart: "1000" # gidRangeEnd: "2000" # basePath: "/dynamic_provisioning" +# subPathPattern: "/subPath" +# ensureUniqueDirectory: true # reclaimPolicy: Delete # volumeBindingMode: Immediate diff --git a/deploy/kubernetes/base/node-daemonset.yaml b/deploy/kubernetes/base/node-daemonset.yaml index 1f80c536f..29d271de1 100644 --- a/deploy/kubernetes/base/node-daemonset.yaml +++ b/deploy/kubernetes/base/node-daemonset.yaml @@ -31,6 +31,7 @@ spec: operator: NotIn values: - fargate + hostNetwork: true dnsPolicy: ClusterFirst serviceAccountName: efs-csi-node-sa priorityClassName: system-node-critical @@ -57,6 +58,10 @@ spec: env: - name: CSI_ENDPOINT value: unix:/csi/csi.sock + - name: CSI_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName volumeMounts: - name: kubelet-dir mountPath: /var/lib/kubelet diff --git a/docs/README.md b/docs/README.md index 8f6a3f4e9..478549bbb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.org/kubernetes-sigs/aws-efs-csi-driver.svg?branch=master)](https://travis-ci.org/kubernetes-sigs/aws-efs-csi-driver) -[![Coverage Status](https://coveralls.io/repos/github/kubernetes-sigs/aws-efs-csi-driver/badge.svg?branch=master)](https://coveralls.io/github/kubernetes-sigs/aws-efs-csi-driver?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/kubernetes-sigs/aws-efs-csi-driver)](https://goreportcard.com/report/github.com/kubernetes-sigs/aws-efs-csi-driver) ## Amazon EFS CSI Driver @@ -26,19 +24,22 @@ The following CSI interfaces are implemented: * Identity Service: GetPluginInfo, GetPluginCapabilities, Probe ### Storage Class Parameters for Dynamic Provisioning -| Parameters | Values | Default | Optional | Description | -|---------------------|--------|---------|-----------|-------------| -| provisioningMode | efs-ap | | false | Type of volume provisioned by efs. Currently, Access Points are supported. | -| fileSystemId | | | false | File System under which access points are created. | -| directoryPerms | | | false | Directory permissions for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation. | -| uid | | | true | POSIX user Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation and for [user identity enforcement](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-identity-access-points). | -| gid | | | true | POSIX group Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation and for [user identity enforcement](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-identity-access-points). | -| gidRangeStart | | 50000 | true | Start range of the POSIX group Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation and for [user identity enforcement](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-identity-access-points). Not used if uid/gid is set. For user identity enforcement, this value will be applied as both the uid and the gid. | -| gidRangeEnd | | 7000000 | true | End range of the POSIX group Id. Not used if uid/gid is set. | -| basePath | | | true | Path under which access points for dynamic provisioning is created. If this parameter is not specified, access points are created under the root directory of the file system | -| az | | "" | true | Used for cross-account mount. `az` under storage class parameter is optional. If specified, mount target associated with the az will be used for cross-account mount. If not specified, a random mount target will be picked for cross account mount | +| Parameters | Values | Default | Optional | Description | +|-----------------------|--------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| provisioningMode | efs-ap | | false | Type of volume provisioned by efs. Currently, Access Points are supported. | +| fileSystemId | | | false | File System under which access points are created. | +| directoryPerms | | | false | Directory permissions for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation. | +| uid | | | true | POSIX user Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation. | +| gid | | | true | POSIX group Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation. | +| gidRangeStart | | 50000 | true | Start range of the POSIX group Id to be applied for [Access Point root directory](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html#enforce-root-directory-access-point) creation. Not used if uid/gid is set. | +| gidRangeEnd | | 7000000 | true | End range of the POSIX group Id. Not used if uid/gid is set. | +| basePath | | | true | Path under which access points for dynamic provisioning is created. If this parameter is not specified, access points are created under the root directory of the file system | +| subPathPattern | | `/${.PV.name}` | true | The template used to construct the subPath under which each of the access points created under Dynamic Provisioning. Can be made up of fixed strings and limited variables, is akin to the 'subPathPattern' variable on the [nfs-subdir-external-provisioner](https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner) chart. Supports `.PVC.name`,`.PVC.namespace` and `.PV.name` | +| ensureUniqueDirectory | | true | true | **NOTE: Only set this to false if you're sure this is the behaviour you want**.
Used when dynamic provisioning is enabled, if set to true, appends the a UID to the pattern specified in `subPathPattern` to ensure that access points will not accidentally point at the same directory. | +| az | | "" | true | Used for cross-account mount. `az` under storage class parameter is optional. If specified, mount target associated with the az will be used for cross-account mount. If not specified, a random mount target will be picked for cross account mount | +| reuseAccessPoint | | false | true | When set to true, it creates Accesspoint client-token from the provided PVC name. So that the AccessPoint can be re-used from a differen cluster if same PVC name and storageclass configuration are used. | -**Notes**: +**Note** * Custom Posix group Id range for Access Point root directory must include both `gidRangeStart` and `gidRangeEnd` parameters. These parameters are optional only if both are omitted. If you specify one, the other becomes mandatory. * When using a custom Posix group ID range, there is a possibility for the driver to run out of available POSIX group Ids. We suggest ensuring custom group ID range is large enough or create a new storage class with a new file system to provision additional volumes. * `az` under storage class parameter is not be confused with efs-utils mount option `az`. The `az` mount option is used for cross-az mount or efs one zone file system mount within the same aws account as the cluster. @@ -167,12 +168,137 @@ helm repo update helm upgrade --install aws-efs-csi-driver --namespace kube-system aws-efs-csi-driver/aws-efs-csi-driver ``` -To force the efs-csi-driver to use FIPS, you can add an argument to the helm upgrade command: -``` -helm upgrade --install aws-efs-csi-driver --namespace kube-system aws-efs-csi-driver/aws-efs-csi-driver --set useFips=true -``` -**Notes**: -* `hostNetwork: true` (should be added under spec/deployment on kubernetes installations where AWS metadata is not reachable from pod network. To fix the following error `NoCredentialProviders: no valid providers in chain` this parameter should be added.) +This procedure requires Helm V3 or later. To install or upgrade Helm, see [Using Helm with Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/helm.html). + +**To install the driver using Helm** + +1. Add the Helm repo. + + ```sh + helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/ + ``` + +2. Update the repo. + + ```sh + helm repo update aws-efs-csi-driver + ``` + +3. Install a release of the driver using the Helm chart. + + ```sh + helm upgrade --install aws-efs-csi-driver --namespace kube-system aws-efs-csi-driver/aws-efs-csi-driver + ``` + + To specify an image repository, add the following argument. Replace the repository address with the cluster's [container image address](https://docs.aws.amazon.com/eks/latest/userguide/add-ons-images.html). + ```sh + --set image.repository=602401143452.dkr.ecr.region-code.amazonaws.com/eks/aws-efs-csi-driver + ``` + + If you already created a service account by following [Create an IAM policy and role for Amazon EKS](./iam-policy-create.md), then add the following arguments. + ```sh + --set controller.serviceAccount.create=false \ + --set controller.serviceAccount.name=efs-csi-controller-sa + ``` + + If you don't have outbound access to the Internet, add the following arguments. + ```sh + --set sidecars.livenessProbe.image.repository=602401143452.dkr.ecr.region-code.amazonaws.com/eks/livenessprobe \ + --set sidecars.node-driver-registrar.image.repository=602401143452.dkr.ecr.region-code.amazonaws.com/eks/csi-node-driver-registrar \ + --set sidecars.csiProvisioner.image.repository=602401143452.dkr.ecr.region-code.amazonaws.com/eks/csi-provisioner + ``` + + To force the Amazon EFS CSI driver to use FIPS for mounting the file system, add the following argument. + ```sh + --set useFips=true + ``` +**Note** +`hostNetwork: true` (should be added under spec/deployment on kubernetes installations where AWS metadata is not reachable from pod network. To fix the following error `NoCredentialProviders: no valid providers in chain` this parameter should be added.) + +------ +##### [ Manifest \(private registry\) ] + +If you want to download the image with a manifest, we recommend first trying these steps to pull secured images from the private Amazon ECR registry. + +**To install the driver using images stored in the private Amazon ECR registry** + +1. Download the manifest. Replace `release-X.X` with your desired branch. We recommend using the latest released version. For a list of active branches, see [Branches](../../../branches/active). + + ```sh + kubectl kustomize \ + "github.com/kubernetes-sigs/aws-efs-csi-driver/deploy/kubernetes/overlays/stable/ecr/?ref=release-1.X" > private-ecr-driver.yaml + ``` + **Note** + If you encounter an issue that you aren't able to resolve by adding IAM permissions, try the [Manifest \(public registry\)](#-manifest-public-registry-) steps instead. + +2. In the following command, replace `region-code` with the AWS Region that your cluster is in. Then run the modified command to replace `us-west-2` in the file with your AWS Region. + + ```sh + sed -i.bak -e 's|us-west-2|region-code|' private-ecr-driver.yaml + ``` + +3. Replace `account` in the following command with the account from [Amazon container image registries](add-ons-images.md) for the AWS Region that your cluster is in and then run the modified command to replace `602401143452` in the file. + + ```sh + sed -i.bak -e 's|602401143452|account|' private-ecr-driver.yaml + ``` + +4. If you already created a service account by following [Create an IAM policy and role for Amazon EKS](./iam-policy-create.md), then edit the `private-ecr-driver.yaml` file. Remove the following lines that create a Kubernetes service account. + + ``` + apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/name: aws-efs-csi-driver + name: efs-csi-controller-sa + namespace: kube-system + --- + ``` + +5. Apply the manifest. + + ```sh + kubectl apply -f private-ecr-driver.yaml + ``` + +------ +##### [ Manifest \(public registry\) ] + +For some situations, you may not be able to add the necessary IAM permissions to pull from the private Amazon ECR registry. One example of this scenario is if your [IAM principal](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html) isn't allowed to authenticate with someone else's account. When this is true, you can use the public Amazon ECR registry. + +**To install the driver using images stored in the public Amazon ECR registry** + +1. Download the manifest. Replace `release-X.X` with your desired branch. We recommend using the latest released version. For a list of active branches, see [Branches](../../../branches/active). + + ```sh + kubectl kustomize \ + "github.com/kubernetes-sigs/aws-efs-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.X" > public-ecr-driver.yaml + ``` + +2. If you already created a service account by following [Create an IAM policy and role](./iam-policy-create.md), then edit the `private-ecr-driver.yaml` file. Remove the following lines that create a Kubernetes service account. + + ```sh + apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/name: aws-efs-csi-driver + name: efs-csi-controller-sa + namespace: kube-system + --- + ``` + +3. Apply the manifest. + + ```sh + kubectl apply -f public-ecr-driver.yaml + ``` +------ + +After deploying the driver, you can continue to these sections: +* [Create an Amazon EFS file system for Amazon EKS](./efs-create-filesystem.md) +* [Examples](#examples) ### Container Arguments for efs-plugin of efs-csi-node daemonset | Parameters | Values | Default | Optional | Description | diff --git a/examples/kubernetes/dynamic_provisioning/README.md b/examples/kubernetes/dynamic_provisioning/README.md index 35ec5e483..90528a299 100644 --- a/examples/kubernetes/dynamic_provisioning/README.md +++ b/examples/kubernetes/dynamic_provisioning/README.md @@ -44,8 +44,139 @@ After the objects are created, verify that pod is running: Also you can verify that data is written onto EFS filesystem: -```sh ->> kubectl exec -ti efs-app -- tail -f /data/out -``` -### Note: -When you want to delete an access point in a file system when deleting PVC, you should specify `elasticfilesystem:ClientRootAccess` to the file system access policy to provide the root permissions. \ No newline at end of file + 2. Download a `StorageClass` manifest for Amazon EFS. + + ```sh + curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml + ``` + + 3. Edit [the file](./specs/storageclass.yaml). Find the following line, and replace the value for `fileSystemId` with your file system ID. + + ``` + fileSystemId: fs-582a03f3 + ``` + Modify the other values as needed: + * `provisioningMode` - The type of volume to be provisioned by Amazon EFS. Currently, only access point based provisioning is supported (`efs-ap`). + * `fileSystemId` - The file system under which the access point is created. + * `directoryPerms` - The directory permissions of the root directory created by the access point. + * `gidRangeStart` (Optional) - The starting range of the Posix group ID to be applied onto the root directory of the access point. The default value is `50000`. + * `gidRangeEnd` (Optional) - The ending range of the Posix group ID. The default value is `7000000`. + * `basePath` (Optional) - The path on the file system under which the access point root directory is created. If the path isn't provided, the access points root directory is created under the root of the file system. + * `subPathPattern` (Optional) - A pattern that describes the subPath under which an access point should be created. So if the pattern were `${.PVC.namespace}/${PVC.name}`, the PVC namespace is `foo` and the PVC name is `pvc-123-456`, and the `basePath` is `/dynamic_provisioner` the access point would be + created at `/dynamic_provisioner/foo/pvc-123-456`. + * `ensureUniqueDirectory` (Optional) - A boolean that ensures that, if set, a UUID is appended to the final element of + any dynamically provisioned path, as in the above example. This can be turned off but this requires you as the + administrator to ensure that your storage classes are set up correctly. Otherwise, it's possible that 2 pods could + end up writing to the same directory by accident. **Please think very carefully before setting this to false!** + + 4. Deploy the storage class. + + ```sh + kubectl apply -f storageclass.yaml + ``` + +2. Test automatic provisioning by deploying a Pod that makes use of the PVC: + + 1. Download a manifest that deploys a Pod and a PVC. + + ```sh + curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/pod.yaml + ``` + + 2. Deploy the Pod with a sample app and the PVC used by the Pod. + + ```sh + kubectl apply -f pod.yaml + ``` +3. Determine the names of the Pods running the controller. + ```sh + kubectl get pods -n kube-system | grep efs-csi-controller + ``` + + The example output is as follows. + + ``` + efs-csi-controller-74ccf9f566-q5989 3/3 Running 0 40m + efs-csi-controller-74ccf9f566-wswg9 3/3 Running 0 40m + ``` + +4. After few seconds, you can observe the controller picking up the change \(edited for readability\). Replace `74ccf9f566-q5989` with a value from one of the Pods in your output from the previous command. + + ```sh + kubectl logs efs-csi-controller-74ccf9f566-q5989 \ + -n kube-system \ + -c csi-provisioner \ + --tail 10 + ``` + + The example output is as follows. + + ``` + [...] + 1 controller.go:737] successfully created PV pvc-5983ffec-96cf-40c1-9cd6-e5686ca84eca for PVC efs-claim and csi volume name fs-95bcec92::fsap-02a88145b865d3a87 + ``` + + If you don't see the previous output, run the previous command using one of the other controller Pods. + +5. Confirm that a persistent volume was created with a status of `Bound` to a `PersistentVolumeClaim`: + + ```sh + kubectl get pv + ``` + + The example output is as follows. + + ``` + NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE + pvc-5983ffec-96cf-40c1-9cd6-e5686ca84eca 20Gi RWX Delete Bound default/efs-claim efs-sc 7m57s + ``` + +6. View details about the `PersistentVolumeClaim` that was created. + + ```sh + kubectl get pvc + ``` + + The example output is as follows. + + ``` + NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE + efs-claim Bound pvc-5983ffec-96cf-40c1-9cd6-e5686ca84eca 20Gi RWX efs-sc 9m7s + ``` + +7. View the sample app Pod's status until the `STATUS` becomes `Running`. + + ```sh + kubectl get pods -o wide + ``` + + The example output is as follows. + + ``` + NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES + efs-app 1/1 Running 0 10m 192.168.78.156 ip-192-168-73-191.region-code.compute.internal + ``` +**Note** +If a Pod doesn't have an IP address listed, make sure that you added a mount target for the subnet that your node is in \(as described at the end of [Create an Amazon EFS file system](#efs-create-filesystem)\). Otherwise the Pod won't leave `ContainerCreating` status. When an IP address is listed, it may take a few minutes for a Pod to reach the `Running` status. + +1. Confirm that the data is written to the volume. + + ```sh + kubectl exec efs-app -- bash -c "cat data/out" + ``` + + The example output is as follows. + + ``` + [...] + Tue Mar 23 14:29:16 UTC 2021 + Tue Mar 23 14:29:21 UTC 2021 + Tue Mar 23 14:29:26 UTC 2021 + Tue Mar 23 14:29:31 UTC 2021 + [...] + ``` + +2. \(Optional\) Terminate the Amazon EKS node that your Pod is running on and wait for the Pod to be re\-scheduled. Alternately, you can delete the Pod and redeploy it. Complete the previous step again, confirming that the output includes the previous output. + +**Note** +When you want to delete an access point in a file system when deleting PVC, you should specify `elasticfilesystem:ClientRootAccess` to the file system access policy to provide the root permissions. diff --git a/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml b/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml index 0fcb0cc8f..6bf895af6 100644 --- a/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml +++ b/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml @@ -9,4 +9,7 @@ parameters: directoryPerms: "700" gidRangeStart: "1000" # optional gidRangeEnd: "2000" # optional - basePath: "/dynamic_provisioning" # optional \ No newline at end of file + basePath: "/dynamic_provisioning" # optional + subPathPattern: "${.PVC.namespace}/${.PVC.name}" # optional + ensureUniqueDirectory: "true" # optional + reuseAccessPoint: "false" # optional \ No newline at end of file diff --git a/go.mod b/go.mod index e79adff93..db407b5fe 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ require ( github.com/aws/aws-sdk-go v1.44.76 github.com/container-storage-interface/spec v1.6.0 github.com/golang/mock v1.6.0 + github.com/google/uuid v1.3.0 github.com/kubernetes-csi/csi-test/v5 v5.0.0 github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 github.com/onsi/ginkgo/v2 v2.9.0 github.com/onsi/gomega v1.27.1 + golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 google.golang.org/grpc v1.53.0 k8s.io/api v0.25.6 k8s.io/apimachinery v0.25.6 @@ -41,7 +43,6 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -81,7 +82,7 @@ require ( golang.org/x/term v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index ee54fc300..3e614c819 100644 --- a/go.sum +++ b/go.sum @@ -382,6 +382,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -404,6 +406,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -585,8 +588,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= +golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/hack/e2e/run.sh b/hack/e2e/run.sh index 8431bab8b..42a8e2e2a 100755 --- a/hack/e2e/run.sh +++ b/hack/e2e/run.sh @@ -53,7 +53,7 @@ K8S_VERSION_KOPS=${K8S_VERSION_KOPS:-${K8S_VERSION:-1.27.3}} K8S_VERSION_EKSCTL=${K8S_VERSION_EKSCTL:-${K8S_VERSION:-1.27}} KOPS_VERSION=${KOPS_VERSION:-1.27.0-beta.3} -KOPS_STATE_FILE=${KOPS_STATE_FILE:-s3://k8s-kops-csi-e2e} +KOPS_STATE_FILE=${KOPS_STATE_FILE:-s3://k8s-kops-csi-shared-e2e} KOPS_PATCH_FILE=${KOPS_PATCH_FILE:-./hack/kops-patch.yaml} KOPS_PATCH_NODE_FILE=${KOPS_PATCH_NODE_FILE:-./hack/kops-patch-node.yaml} diff --git a/hack/kops-patch.yaml b/hack/kops-patch.yaml index 39d183239..ee224b692 100644 --- a/hack/kops-patch.yaml +++ b/hack/kops-patch.yaml @@ -7,10 +7,19 @@ spec: "Action": [ "elasticfilesystem:CreateAccessPoint", "elasticfilesystem:DeleteAccessPoint", - "elasticfilesystem:DescribeFileSystems", - "elasticfilesystem:DescribeAccessPoints", "elasticfilesystem:DescribeMountTargets", - "ec2:DescribeAvailabilityZones" + "ec2:DescribeAvailabilityZones", + "elasticfilesystem:DescribeMountTargets", + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:ClientMount", + "elasticfilesystem:ClientWrite", + "elasticfilesystem:CreateTags", + "elasticfilesystem:CreateMountTarget", + "elasticfilesystem:DeleteMountTarget", + "elasticfilesystem:DeleteTags", + "elasticfilesystem:TagResource", + "elasticfilesystem:UntagResource" ], "Resource": "*" } diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index 011caa7dd..6d34e6561 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -35,7 +35,9 @@ import ( ) const ( - AccessDeniedException = "AccessDeniedException" + AccessDeniedException = "AccessDeniedException" + AccessPointAlreadyExists = "AccessPointAlreadyExists" + PvcNameTagKey = "pvcName" ) var ( @@ -55,6 +57,12 @@ type AccessPoint struct { // Capacity is used for testing purpose only // EFS does not consider capacity while provisioning new file systems or access points CapacityGiB int64 + PosixUser *PosixUser +} + +type PosixUser struct { + Gid int64 + Uid int64 } type AccessPointOptions struct { @@ -88,9 +96,10 @@ type Efs interface { type Cloud interface { GetMetadata() MetadataService - CreateAccessPoint(ctx context.Context, volumeName string, accessPointOpts *AccessPointOptions) (accessPoint *AccessPoint, err error) + CreateAccessPoint(ctx context.Context, clientToken string, accessPointOpts *AccessPointOptions, reuseAccessPoint bool) (accessPoint *AccessPoint, err error) DeleteAccessPoint(ctx context.Context, accessPointId string) (err error) DescribeAccessPoint(ctx context.Context, accessPointId string) (accessPoint *AccessPoint, err error) + ListAccessPoints(ctx context.Context, fileSystemId string) (accessPoints []*AccessPoint, err error) DescribeFileSystem(ctx context.Context, fileSystemId string) (fs *FileSystem, err error) DescribeMountTargets(ctx context.Context, fileSystemId, az string) (fs *MountTarget, err error) } @@ -154,10 +163,28 @@ func (c *cloud) GetMetadata() MetadataService { return c.metadata } -func (c *cloud) CreateAccessPoint(ctx context.Context, volumeName string, accessPointOpts *AccessPointOptions) (accessPoint *AccessPoint, err error) { +func (c *cloud) CreateAccessPoint(ctx context.Context, clientToken string, accessPointOpts *AccessPointOptions, reuseAccessPoint bool) (accessPoint *AccessPoint, err error) { efsTags := parseEfsTags(accessPointOpts.Tags) + + //if reuseAccessPoint is true, check for AP with same Root Directory exists in efs + // if found reuse that AP + if reuseAccessPoint { + existingAP, err := c.findAccessPointByClientToken(ctx, clientToken, accessPointOpts) + if err != nil { + return nil, fmt.Errorf("failed to find access point: %v", err) + } + if existingAP != nil { + //AP path already exists + klog.V(2).Infof("Existing AccessPoint found : %+v", existingAP) + return &AccessPoint{ + AccessPointId: existingAP.AccessPointId, + FileSystemId: existingAP.FileSystemId, + CapacityGiB: accessPointOpts.CapacityGiB, + }, nil + } + } createAPInput := &efs.CreateAccessPointInput{ - ClientToken: &volumeName, + ClientToken: &clientToken, FileSystemId: &accessPointOpts.FileSystemId, PosixUser: &efs.PosixUser{ Gid: &accessPointOpts.Gid, @@ -182,6 +209,7 @@ func (c *cloud) CreateAccessPoint(ctx context.Context, volumeName string, access } return nil, fmt.Errorf("Failed to create access point: %v", err) } + klog.V(5).Infof("Create AP response : %+v", res) return &AccessPoint{ AccessPointId: *res.AccessPointId, @@ -233,6 +261,69 @@ func (c *cloud) DescribeAccessPoint(ctx context.Context, accessPointId string) ( }, nil } +func (c *cloud) findAccessPointByClientToken(ctx context.Context, clientToken string, accessPointOpts *AccessPointOptions) (accessPoint *AccessPoint, err error) { + klog.V(5).Infof("AccessPointOptions to find AP : %+v", accessPointOpts) + klog.V(2).Infof("ClientToken to find AP : %s", clientToken) + describeAPInput := &efs.DescribeAccessPointsInput{ + FileSystemId: &accessPointOpts.FileSystemId, + MaxResults: aws.Int64(1000), + } + res, err := c.efs.DescribeAccessPointsWithContext(ctx, describeAPInput) + if err != nil { + if isAccessDenied(err) { + return + } + if isFileSystemNotFound(err) { + return + } + err = fmt.Errorf("failed to list Access Points of efs = %s : %v", accessPointOpts.FileSystemId, err) + return + } + for _, ap := range res.AccessPoints { + // check if AP exists with same client token + if aws.StringValue(ap.ClientToken) == clientToken { + return &AccessPoint{ + AccessPointId: *ap.AccessPointId, + FileSystemId: *ap.FileSystemId, + AccessPointRootDir: *ap.RootDirectory.Path, + }, nil + } + } + klog.V(2).Infof("Access point does not exist") + return nil, nil +} + +func (c *cloud) ListAccessPoints(ctx context.Context, fileSystemId string) (accessPoints []*AccessPoint, err error) { + describeAPInput := &efs.DescribeAccessPointsInput{ + FileSystemId: &fileSystemId, + } + res, err := c.efs.DescribeAccessPointsWithContext(ctx, describeAPInput) + if err != nil { + if isAccessDenied(err) { + return + } + if isFileSystemNotFound(err) { + return + } + err = fmt.Errorf("List Access Points failed: %v", err) + return + } + + for _, accessPointDescription := range res.AccessPoints { + accessPoint := &AccessPoint{ + AccessPointId: *accessPointDescription.AccessPointId, + FileSystemId: *accessPointDescription.FileSystemId, + PosixUser: &PosixUser{ + Gid: *accessPointDescription.PosixUser.Gid, + Uid: *accessPointDescription.PosixUser.Gid, + }, + } + accessPoints = append(accessPoints, accessPoint) + } + + return +} + func (c *cloud) DescribeFileSystem(ctx context.Context, fileSystemId string) (fs *FileSystem, err error) { describeFsInput := &efs.DescribeFileSystemsInput{FileSystemId: &fileSystemId} klog.V(5).Infof("Calling DescribeFileSystems with input: %+v", *describeFsInput) diff --git a/pkg/cloud/cloud_test.go b/pkg/cloud/cloud_test.go index 6e2079751..9e6dfbeb2 100644 --- a/pkg/cloud/cloud_test.go +++ b/pkg/cloud/cloud_test.go @@ -3,6 +3,7 @@ package cloud import ( "context" "errors" + "reflect" "testing" "github.com/aws/aws-sdk-go/aws" @@ -27,13 +28,14 @@ func TestCreateAccessPoint(t *testing.T) { directoryPerms = "0777" directoryPath = "/test" volName = "volName" + clientToken = volName ) testCases := []struct { name string testFunc func(t *testing.T) }{ { - name: "Success", + name: "Success - AP does not exist", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockEfs := mocks.NewMockEfs(mockCtl) @@ -72,9 +74,63 @@ func TestCreateAccessPoint(t *testing.T) { }, } + describeAPOutput := &efs.DescribeAccessPointsOutput{ + AccessPoints: nil, + } + ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(describeAPOutput, nil) mockEfs.EXPECT().CreateAccessPointWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) - res, err := c.CreateAccessPoint(ctx, volName, req) + res, err := c.CreateAccessPoint(ctx, clientToken, req, true) + + if err != nil { + t.Fatalf("CreateAccessPointFailed is failed: %v", err) + } + + if res == nil { + t.Fatal("Result is nil") + } + + if accessPointId != res.AccessPointId { + t.Fatalf("AccessPointId mismatched. Expected: %v, Actual: %v", accessPointId, res.AccessPointId) + } + + if fsId != res.FileSystemId { + t.Fatalf("FileSystemId mismatched. Expected: %v, Actual: %v", fsId, res.FileSystemId) + } + mockCtl.Finish() + }, + }, + { + name: "Success - AP already exists", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockEfs := mocks.NewMockEfs(mockCtl) + c := &cloud{ + efs: mockEfs, + } + + tags := make(map[string]string) + tags["cluster"] = "efs" + + req := &AccessPointOptions{ + FileSystemId: fsId, + Uid: uid, + Gid: gid, + DirectoryPerms: directoryPerms, + DirectoryPath: directoryPath, + Tags: tags, + } + + describeAPOutput := &efs.DescribeAccessPointsOutput{ + AccessPoints: []*efs.AccessPointDescription{ + {AccessPointId: aws.String(accessPointId), FileSystemId: aws.String(fsId), ClientToken: aws.String(clientToken), RootDirectory: &efs.RootDirectory{Path: aws.String(directoryPath)}, Tags: []*efs.Tag{{Key: aws.String(PvcNameTagKey), Value: aws.String(volName)}}}, + }, + } + + ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(describeAPOutput, nil) + res, err := c.CreateAccessPoint(ctx, clientToken, req, true) if err != nil { t.Fatalf("CreateAccessPointFailed is failed: %v", err) @@ -108,10 +164,14 @@ func TestCreateAccessPoint(t *testing.T) { DirectoryPerms: directoryPerms, DirectoryPath: directoryPath, } + describeAPOutput := &efs.DescribeAccessPointsOutput{ + AccessPoints: nil, + } ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(describeAPOutput, nil) mockEfs.EXPECT().CreateAccessPointWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("CreateAccessPointWithContext failed")) - _, err := c.CreateAccessPoint(ctx, volName, req) + _, err := c.CreateAccessPoint(ctx, clientToken, req, true) if err == nil { t.Fatalf("CreateAccessPoint did not fail") } @@ -135,7 +195,7 @@ func TestCreateAccessPoint(t *testing.T) { ctx := context.Background() mockEfs.EXPECT().CreateAccessPointWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, awserr.New(AccessDeniedException, "Access Denied", errors.New("Access Denied"))) - _, err := c.CreateAccessPoint(ctx, volName, req) + _, err := c.CreateAccessPoint(ctx, clientToken, req, false) if err == nil { t.Fatalf("CreateAccessPoint did not fail") } @@ -443,6 +503,125 @@ func TestDescribeAccessPoint(t *testing.T) { } } +func TestListAccessPoints(t *testing.T) { + var ( + fsId = "fs-abcd1234" + accessPointId = "ap-abc123" + Gid int64 = 1000 + Uid int64 = 1000 + ) + testCases := []struct { + name string + testFunc func(t *testing.T) + }{ + { + name: "Success", + testFunc: func(t *testing.T) { + mockctl := gomock.NewController(t) + mockEfs := mocks.NewMockEfs(mockctl) + c := &cloud{efs: mockEfs} + + output := &efs.DescribeAccessPointsOutput{ + AccessPoints: []*efs.AccessPointDescription{ + { + AccessPointId: aws.String(accessPointId), + FileSystemId: aws.String(fsId), + PosixUser: &efs.PosixUser{ + Gid: aws.Int64(Gid), + Uid: aws.Int64(Uid), + }, + }, + }, + NextToken: nil, + } + + ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) + res, err := c.ListAccessPoints(ctx, fsId) + if err != nil { + t.Fatalf("List Access Points failed: %v", err) + } + + if res == nil { + t.Fatal("Result is nil") + } + + if len(res) != 1 { + t.Fatalf("Expected only one AccessPoint in response but got: %v", res) + } + + mockctl.Finish() + }, + }, + { + name: "Success - multiple access points", + testFunc: func(t *testing.T) { + mockctl := gomock.NewController(t) + mockEfs := mocks.NewMockEfs(mockctl) + c := &cloud{efs: mockEfs} + + output := &efs.DescribeAccessPointsOutput{ + AccessPoints: []*efs.AccessPointDescription{ + { + AccessPointId: aws.String(accessPointId), + FileSystemId: aws.String(fsId), + PosixUser: &efs.PosixUser{ + Gid: aws.Int64(Gid), + Uid: aws.Int64(Uid), + }, + }, + { + AccessPointId: aws.String(accessPointId), + FileSystemId: aws.String(fsId), + PosixUser: &efs.PosixUser{ + Gid: aws.Int64(1001), + Uid: aws.Int64(1001), + }, + }, + }, + NextToken: nil, + } + + ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(output, nil) + res, err := c.ListAccessPoints(ctx, fsId) + if err != nil { + t.Fatalf("List Access Points failed: %v", err) + } + + if res == nil { + t.Fatal("Result is nil") + } + + if len(res) != 2 { + t.Fatalf("Expected two AccessPoints in response but got: %v", res) + } + + mockctl.Finish() + }, + }, + { + name: "Fail - Access Denied", + testFunc: func(t *testing.T) { + mockctl := gomock.NewController(t) + mockEfs := mocks.NewMockEfs(mockctl) + c := &cloud{efs: mockEfs} + ctx := context.Background() + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Eq(ctx), gomock.Any()).Return(nil, awserr.New(AccessDeniedException, "Access Denied", errors.New("Access Denied"))) + _, err := c.ListAccessPoints(ctx, fsId) + if err == nil { + t.Fatalf("List Access Points should have failed: %v", err) + } + + mockctl.Finish() + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, tc.testFunc) + } +} + func TestDescribeFileSystem(t *testing.T) { var ( fsId = "fs-abcd1234" @@ -743,3 +922,68 @@ func testResult(t *testing.T, funcName string, ret interface{}, err error, expec } } } + +func Test_findAccessPointByPath(t *testing.T) { + fsId := "testFsId" + clientToken := "testPvcName" + dirPath := "testPath" + diffClientToken := aws.String("diff") + + mockctl := gomock.NewController(t) + defer mockctl.Finish() + mockEfs := mocks.NewMockEfs(mockctl) + + expectedSingleAP := &AccessPoint{ + AccessPointId: "testApId", + AccessPointRootDir: dirPath, + FileSystemId: fsId, + } + + type args struct { + clientToken string + accessPointOpts *AccessPointOptions + } + tests := []struct { + name string + args args + prepare func(*mocks.MockEfs) + wantAccessPoint *AccessPoint + wantErr bool + }{ + {name: "Expected_ClientToken_Not_Found", args: args{clientToken, &AccessPointOptions{FileSystemId: fsId, DirectoryPath: dirPath}}, prepare: func(mockEfs *mocks.MockEfs) { + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Any(), gomock.Any()).Return(&efs.DescribeAccessPointsOutput{ + AccessPoints: []*efs.AccessPointDescription{{FileSystemId: aws.String(fsId), ClientToken: diffClientToken, AccessPointId: aws.String(expectedSingleAP.AccessPointId), RootDirectory: &efs.RootDirectory{Path: aws.String("differentPath")}}}, + }, nil) + }, wantAccessPoint: nil, wantErr: false}, + {name: "Expected_Path_Found_In_Multiple_APs_And_One_AP_Filtered_By_ClientToken", args: args{clientToken, &AccessPointOptions{FileSystemId: fsId, DirectoryPath: dirPath}}, prepare: func(mockEfs *mocks.MockEfs) { + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Any(), gomock.Any()).Return(&efs.DescribeAccessPointsOutput{ + AccessPoints: []*efs.AccessPointDescription{ + {FileSystemId: aws.String(fsId), ClientToken: diffClientToken, AccessPointId: aws.String("differentApId"), RootDirectory: &efs.RootDirectory{Path: aws.String(expectedSingleAP.AccessPointRootDir)}}, + {FileSystemId: aws.String(fsId), ClientToken: &clientToken, AccessPointId: aws.String(expectedSingleAP.AccessPointId), RootDirectory: &efs.RootDirectory{Path: aws.String(expectedSingleAP.AccessPointRootDir)}}, + }, + }, nil) + }, wantAccessPoint: expectedSingleAP, wantErr: false}, + {name: "Fail_DescribeAccessPoints", args: args{clientToken, &AccessPointOptions{FileSystemId: fsId, DirectoryPath: dirPath}}, prepare: func(mockEfs *mocks.MockEfs) { + mockEfs.EXPECT().DescribeAccessPointsWithContext(gomock.Any(), gomock.Any()).Return(nil, errors.New("access_denied")) + }, wantAccessPoint: nil, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &cloud{efs: mockEfs} + ctx := context.Background() + + if tt.prepare != nil { + tt.prepare(mockEfs) + } + + gotAccessPoint, err := c.findAccessPointByClientToken(ctx, tt.args.clientToken, tt.args.accessPointOpts) + if (err != nil) != tt.wantErr { + t.Errorf("findAccessPointByClientToken() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotAccessPoint, tt.wantAccessPoint) { + t.Errorf("findAccessPointByClientToken() gotAccessPoint = %v, want %v", gotAccessPoint, tt.wantAccessPoint) + } + }) + } +} diff --git a/pkg/cloud/fakes.go b/pkg/cloud/fakes.go index d05e1a38e..49953665b 100644 --- a/pkg/cloud/fakes.go +++ b/pkg/cloud/fakes.go @@ -27,8 +27,8 @@ func (c *FakeCloudProvider) GetMetadata() MetadataService { return c.m } -func (c *FakeCloudProvider) CreateAccessPoint(ctx context.Context, volumeName string, accessPointOpts *AccessPointOptions) (accessPoint *AccessPoint, err error) { - ap, exists := c.accessPoints[volumeName] +func (c *FakeCloudProvider) CreateAccessPoint(ctx context.Context, clientToken string, accessPointOpts *AccessPointOptions, usePvcName bool) (accessPoint *AccessPoint, err error) { + ap, exists := c.accessPoints[clientToken] if exists { if accessPointOpts.CapacityGiB == ap.CapacityGiB { return ap, nil @@ -45,7 +45,7 @@ func (c *FakeCloudProvider) CreateAccessPoint(ctx context.Context, volumeName st CapacityGiB: accessPointOpts.CapacityGiB, } - c.accessPoints[volumeName] = ap + c.accessPoints[clientToken] = ap return ap, nil } @@ -97,3 +97,10 @@ func (c *FakeCloudProvider) DescribeMountTargets(ctx context.Context, fileSystem return nil, ErrNotFound } + +func (c *FakeCloudProvider) ListAccessPoints(ctx context.Context, fileSystemId string) ([]*AccessPoint, error) { + accessPoints := []*AccessPoint{ + c.accessPoints[fileSystemId], + } + return accessPoints, nil +} diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index 68b9077f0..695c4e2fa 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -18,8 +18,12 @@ package driver import ( "context" + "crypto/sha256" "fmt" + "github.com/google/uuid" "os" + "path" + "sort" "strconv" "strings" @@ -31,23 +35,30 @@ import ( ) const ( - AccessPointMode = "efs-ap" - AzName = "az" - BasePath = "basePath" - DefaultGidMin = 50000 - DefaultGidMax = 7000000 - DefaultTagKey = "efs.csi.aws.com/cluster" - DefaultTagValue = "true" - DirectoryPerms = "directoryPerms" - FsId = "fileSystemId" - Gid = "gid" - GidMin = "gidRangeStart" - GidMax = "gidRangeEnd" - MountTargetIp = "mounttargetip" - ProvisioningMode = "provisioningMode" - RoleArn = "awsRoleArn" - TempMountPathPrefix = "/var/lib/csi/pv" - Uid = "uid" + AccessPointMode = "efs-ap" + AzName = "az" + BasePath = "basePath" + DefaultGidMin = 50000 + DefaultGidMax = 7000000 + DefaultTagKey = "efs.csi.aws.com/cluster" + DefaultTagValue = "true" + DirectoryPerms = "directoryPerms" + EnsureUniqueDirectory = "ensureUniqueDirectory" + FsId = "fileSystemId" + Gid = "gid" + GidMin = "gidRangeStart" + GidMax = "gidRangeEnd" + MountTargetIp = "mounttargetip" + ProvisioningMode = "provisioningMode" + PvName = "csi.storage.k8s.io/pv/name" + PvcName = "csi.storage.k8s.io/pvc/name" + PvcNamespace = "csi.storage.k8s.io/pvc/namespace" + RoleArn = "awsRoleArn" + SubPathPattern = "subPathPattern" + TempMountPathPrefix = "/var/lib/csi/pv" + Uid = "uid" + ReuseAccessPointKey = "reuseAccessPoint" + PvcNameKey = "csi.storage.k8s.io/pvc/name" ) var ( @@ -55,11 +66,36 @@ var ( controllerCaps = []csi.ControllerServiceCapability_RPC_Type{ csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, } + // subPathPatternComponents shows the elements that we allow to be in the construction of the root directory + // of the access point, as well as the values we need to extract them from the Volume Parameters. + subPathPatternComponents = map[string]string{ + ".PVC.name": PvcName, + ".PVC.namespace": PvcNamespace, + ".PV.name": PvName, + } ) func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { klog.V(4).Infof("CreateVolume: called with args %+v", *req) + + var reuseAccessPoint bool + var err error + volumeParams := req.GetParameters() volName := req.GetName() + clientToken := volName + + // if true, then use sha256 hash of pvcName as clientToken instead of PVC Id + // This allows users to reconnect to the same AP from different k8s cluster + if reuseAccessPointStr, ok := volumeParams[ReuseAccessPointKey]; ok { + reuseAccessPoint, err = strconv.ParseBool(reuseAccessPointStr) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "Invalid value for reuseAccessPoint parameter") + } + if reuseAccessPoint { + clientToken = get64LenHash(volumeParams[PvcNameKey]) + klog.V(5).Infof("Client token : %s", clientToken) + } + } if volName == "" { return nil, status.Error(codes.InvalidArgument, "Volume name not provided") } @@ -83,18 +119,16 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) var ( azName string basePath string - err error - gid int + gid int64 gidMin int gidMax int localCloud cloud.Cloud provisioningMode string roleArn string - uid int + uid int64 ) //Parse parameters - volumeParams := req.GetParameters() if value, ok := volumeParams[ProvisioningMode]; ok { provisioningMode = value //TODO: Add FS provisioning mode check when implemented @@ -134,7 +168,7 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) uid = -1 if value, ok := volumeParams[Uid]; ok { - uid, err = strconv.Atoi(value) + uid, err = strconv.ParseInt(value, 10, 64) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "Failed to parse invalid %v: %v", Uid, err) } @@ -145,7 +179,7 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) gid = -1 if value, ok := volumeParams[Gid]; ok { - gid, err = strconv.Atoi(value) + gid, err = strconv.ParseInt(value, 10, 64) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "Failed to parse invalid %v: %v", Gid, err) } @@ -193,10 +227,6 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) accessPointsOptions.DirectoryPerms = value } - if value, ok := volumeParams[BasePath]; ok { - basePath = value - } - // Storage class parameter `az` will be used to fetch preferred mount target for cross account mount. // If the `az` storage class parameter is not provided, a random mount target will be picked for mounting. // This storage class parameter different from `az` mount option provided by efs-utils https://github.com/aws/efs-utils/blob/v1.31.1/src/mount_efs/__init__.py#L195 @@ -222,9 +252,9 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) return nil, status.Errorf(codes.Internal, "Failed to fetch File System info: %v", err) } - var allocatedGid int + var allocatedGid int64 if uid == -1 || gid == -1 { - allocatedGid, err = d.gidAllocator.getNextGid(accessPointsOptions.FileSystemId, gidMin, gidMax) + allocatedGid, err = d.gidAllocator.getNextGid(ctx, accessPointsOptions.FileSystemId, gidMin, gidMax) if err != nil { return nil, err } @@ -236,18 +266,48 @@ func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) gid = allocatedGid } + if value, ok := volumeParams[BasePath]; ok { + basePath = value + } + rootDirName := volName - rootDir := basePath + "/" + rootDirName + // Check if a custom structure should be imposed on the access point directory + if value, ok := volumeParams[SubPathPattern]; ok { + // Try and construct the root directory and check it only contains supported components + val, err := interpolateRootDirectoryName(value, volumeParams) + if err == nil { + klog.Infof("Using user-specified structure for access point directory.") + rootDirName = val + if value, ok := volumeParams[EnsureUniqueDirectory]; ok { + if ensureUniqueDirectory, err := strconv.ParseBool(value); !ensureUniqueDirectory && err == nil { + klog.Infof("Not appending PVC UID to path.") + } else { + klog.Infof("Appending PVC UID to path.") + rootDirName = fmt.Sprintf("%s-%s", val, uuid.New().String()) + } + } else { + klog.Infof("Appending PVC UID to path.") + rootDirName = fmt.Sprintf("%s-%s", val, uuid.New().String()) + } + } else { + return nil, err + } + } else { + klog.Infof("Using PV name for access point directory.") + } + + rootDir := path.Join("/", basePath, rootDirName) + if ok, err := validateEfsPathRequirements(rootDir); !ok { + return nil, err + } + klog.Infof("Using %v as the access point directory.", rootDir) - accessPointsOptions.Uid = int64(uid) - accessPointsOptions.Gid = int64(gid) + accessPointsOptions.Uid = uid + accessPointsOptions.Gid = gid accessPointsOptions.DirectoryPath = rootDir - accessPointId, err := localCloud.CreateAccessPoint(ctx, volName, accessPointsOptions) + accessPointId, err := localCloud.CreateAccessPoint(ctx, clientToken, accessPointsOptions, reuseAccessPoint) if err != nil { - if allocatedGid != 0 { - d.gidAllocator.releaseGid(accessPointsOptions.FileSystemId, gid) - } if err == cloud.ErrAccessDenied { return nil, status.Errorf(codes.Unauthenticated, "Access Denied. Please ensure you have the right AWS permissions: %v", err) } @@ -476,3 +536,57 @@ func getCloud(secrets map[string]string, driver *Driver) (cloud.Cloud, string, e return localCloud, roleArn, nil } + +func interpolateRootDirectoryName(rootDirectoryPath string, volumeParams map[string]string) (string, error) { + r := strings.NewReplacer(createListOfVariableSubstitutions(volumeParams)...) + result := r.Replace(rootDirectoryPath) + + // Check if any templating characters still exist + if strings.Contains(result, "${") || strings.Contains(result, "}") { + return "", status.Errorf(codes.InvalidArgument, + "Path specified \"%v\" contains invalid elements. Can only contain %v", rootDirectoryPath, + getSupportedComponentNames()) + } + return result, nil +} + +func createListOfVariableSubstitutions(volumeParams map[string]string) []string { + variableSubstitutions := make([]string, 2*len(subPathPatternComponents)) + i := 0 + for key, volumeParamsKey := range subPathPatternComponents { + variableSubstitutions[i] = "${" + key + "}" + variableSubstitutions[i+1] = volumeParams[volumeParamsKey] + i += 2 + } + return variableSubstitutions +} + +func getSupportedComponentNames() []string { + keys := make([]string, len(subPathPatternComponents)) + + i := 0 + for key := range subPathPatternComponents { + keys[i] = key + i++ + } + sort.Strings(keys) + return keys +} + +func validateEfsPathRequirements(proposedPath string) (bool, error) { + if len(proposedPath) > 100 { + // Check the proposed path is 100 characters or fewer + return false, status.Errorf(codes.InvalidArgument, "Proposed path '%s' exceeds EFS limit of 100 characters", proposedPath) + } else if strings.Count(proposedPath, "/") > 5 { + // Check the proposed path contains at most 4 subdirectories + return false, status.Errorf(codes.InvalidArgument, "Proposed path '%s' EFS limit of 4 subdirectories", proposedPath) + } else { + return true, nil + } +} + +func get64LenHash(text string) string { + h := sha256.New() + h.Write([]byte(text)) + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/pkg/driver/controller_test.go b/pkg/driver/controller_test.go index 151757b97..3d702f522 100644 --- a/pkg/driver/controller_test.go +++ b/pkg/driver/controller_test.go @@ -3,10 +3,18 @@ package driver import ( "context" "errors" + "fmt" + "regexp" + "strconv" "testing" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/container-storage-interface/spec/lib/go/csi" "github.com/golang/mock/gomock" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud" "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/driver/mocks" ) @@ -33,7 +41,847 @@ func TestCreateVolume(t *testing.T) { testFunc func(t *testing.T) }{ { - name: "Success: Using fixed UID/GID", + name: "Success: Using fixed UID/GID", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + Uid: "1000", + Gid: "1001", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + } + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Eq(volumeName), gomock.Any(), gomock.Eq(false)).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != 1000 { + t.Fatalf("Uid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1000) + } + if accessPointOpts.Gid != 1001 { + t.Fatalf("Gid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1001) + } + }) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: Using fixed UID/GID and GID range", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + GidMin: "5000", + GidMax: "10000", + Uid: "1000", + Gid: "1001", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + } + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != 1000 { + t.Fatalf("Uid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1000) + } + if accessPointOpts.Gid != 1001 { + t.Fatalf("Gid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1001) + } + }) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: avoiding GID collision", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + GidMin: "1000", + GidMax: "1003", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + } + accessPoints := []*cloud.AccessPoint{ + { + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1003, + Uid: 1003, + }, + }, + { + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1002, + Uid: 1002, + }, + }, + } + + var expectedGid int64 = 1001 //1003 and 1002 are taken, next available is 1001 + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != expectedGid { + t.Fatalf("Uid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Uid) + } + if accessPointOpts.Gid != expectedGid { + t.Fatalf("Gid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Gid) + } + }) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: reuse released GID", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + GidMin: "1000", + GidMax: "1004", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + ap1 := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1001, + Uid: 1001, + }, + } + ap2 := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1002, + Uid: 1002, + }, + } + ap3 := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1003, + Uid: 1003, + }, + } + ap4 := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1004, + Uid: 1004, + }, + } + + // Let allocator jump over some GIDS. + accessPoints := []*cloud.AccessPoint{ap3, ap4} + var expectedGid int64 = 1002 // 1003 and 1004 is taken. + + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(ap2, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != expectedGid { + t.Fatalf("Uid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Uid) + } + if accessPointOpts.Gid != expectedGid { + t.Fatalf("Gid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Gid) + } + }) + + res, err := driver.CreateVolume(ctx, req) + + // 2. Simulate access point removal and verify their GIDs returned to allocator. + accessPoints = []*cloud.AccessPoint{} + expectedGid = 1004 // 1003 and 1004 are now free, if no GID return would happen allocator would pick 1001. + + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(ap3, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != expectedGid { + t.Fatalf("Uid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Uid) + } + if accessPointOpts.Gid != expectedGid { + t.Fatalf("Gid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Gid) + } + }) + + res, err = driver.CreateVolume(ctx, req) + //// + accessPoints = []*cloud.AccessPoint{ap1, ap4} + + expectedGid = 1003 + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(ap2, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != expectedGid { + t.Fatalf("Uid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Uid) + } + if accessPointOpts.Gid != expectedGid { + t.Fatalf("Gid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Gid) + } + }) + + res, err = driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: EFS access point limit", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + GidMin: "1000", + GidMax: "2000", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + + accessPoints := []*cloud.AccessPoint{} + for i := 0; i < ACCESS_POINT_PER_FS_LIMIT-1; i++ { + gidMax, err := strconv.Atoi(req.Parameters[GidMax]) + if err != nil { + t.Fatalf("Failed to convert GidMax Parameter to int.") + } + userGid := gidMax - i + ap := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: int64(userGid), + Uid: int64(userGid), + }, + } + accessPoints = append(accessPoints, ap) + } + + lastAccessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1001, + Uid: 1001, + }, + } + + expectedGid := 1001 + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(lastAccessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.Uid != int64(expectedGid) { + t.Fatalf("Uid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Uid) + } + if accessPointOpts.Gid != int64(expectedGid) { + t.Fatalf("Gid mismatched. Expected: %v, actual: %v", expectedGid, accessPointOpts.Gid) + } + }) + + var err error + + // Allocate last available GID + _, err = driver.CreateVolume(ctx, req) + if err != nil { + t.Fatalf("CreateVolume failed.") + } + + accessPoints = append(accessPoints, lastAccessPoint) + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + + // All 1000 GIDs are taken now, internal limit should take effect causing CreateVolume to fail. + _, err = driver.CreateVolume(ctx, req) + if err == nil { + t.Fatalf("CreateVolume should have failed.") + } + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + AzName: "us-east-1a", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1000, + Uid: 1000, + }, + } + accessPoints := []*cloud.AccessPoint{accessPoint} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Eq(volumeName), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: Using Default GID ranges", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + BasePath: "test", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: DefaultGidMin - 1, //use GID that is not in default range + }, + } + accessPoints := []*cloud.AccessPoint{accessPoint} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow with tags", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr("cluster:efs"), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1000, + Uid: 1000, + }, + } + accessPoints := []*cloud.AccessPoint{accessPoint} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow with invalid tags", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr("cluster-efs"), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1000, + Uid: 1000, + }, + } + accessPoints := []*cloud.AccessPoint{accessPoint} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + mockCtl.Finish() + }, + }, + { + name: "Success: reuseAccessPointName is true", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), + } + pvcNameVal := "test-pvc" + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + AzName: "us-east-1a", + ReuseAccessPointKey: "true", + PvcNameKey: pvcNameVal, + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1000, + Uid: 1000, + }, + } + accessPoints := []*cloud.AccessPoint{accessPoint} + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(accessPoints, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Eq(get64LenHash(pvcNameVal)), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow with a valid directory structure set", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), + } + + pvName := "foo" + pvcName := "bar" + directoryCreated := fmt.Sprintf("/%s/%s", pvName, pvcName) + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "${.PV.name}/${.PVC.name}", + PvName: pvName, + PvcName: pvcName, + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + } + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPoint bool) { + if !verifyPathWhenUUIDIncluded(accessPointOpts.DirectoryPath, directoryCreated) { + t.Fatalf("Root directory mismatch. Expected: %v (with UID appended), actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) + } + }) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow with a valid directory structure set, using a single element", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -41,9 +889,13 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), } + pvcName := "foo" + directoryCreated := fmt.Sprintf("/%s", pvcName) + req := &csi.CreateVolumeRequest{ Name: volumeName, VolumeCapabilities: []*csi.VolumeCapability{ @@ -55,10 +907,11 @@ func TestCreateVolume(t *testing.T) { Parameters: map[string]string{ ProvisioningMode: "efs-ap", FsId: fsId, + GidMin: "1000", + GidMax: "2000", DirectoryPerms: "777", - BasePath: "test", - Uid: "1000", - Gid: "1001", + SubPathPattern: "${.PVC.name}", + PvcName: pvcName, }, } @@ -71,13 +924,14 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil). - Do(func(ctx context.Context, volumeName string, accessPointOpts *cloud.AccessPointOptions) { - if accessPointOpts.Uid != 1000 { - t.Fatalf("Uid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1000) - } - if accessPointOpts.Gid != 1001 { - t.Fatalf("Gid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1001) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if !verifyPathWhenUUIDIncluded(accessPointOpts.DirectoryPath, directoryCreated) { + t.Fatalf("Root directory mismatch. Expected: %v (with UID appended), actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) } }) @@ -94,11 +948,12 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, { - name: "Success: Using fixed UID/GID and GID range", + name: "Success: Normal flow with a valid directory structure set, and a basePath", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -106,9 +961,14 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), } + pvcName := "foo" + basePath := "bash" + directoryCreated := fmt.Sprintf("/%s/%s", basePath, pvcName) + req := &csi.CreateVolumeRequest{ Name: volumeName, VolumeCapabilities: []*csi.VolumeCapability{ @@ -118,14 +978,15 @@ func TestCreateVolume(t *testing.T) { RequiredBytes: capacityRange, }, Parameters: map[string]string{ - ProvisioningMode: "efs-ap", - FsId: fsId, - DirectoryPerms: "777", - BasePath: "test", - GidMin: "5000", - GidMax: "10000", - Uid: "1000", - Gid: "1001", + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "${.PVC.name}", + BasePath: basePath, + EnsureUniqueDirectory: "true", + PvcName: pvcName, }, } @@ -138,13 +999,89 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil). - Do(func(ctx context.Context, volumeName string, accessPointOpts *cloud.AccessPointOptions) { - if accessPointOpts.Uid != 1000 { - t.Fatalf("Uid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1000) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if !verifyPathWhenUUIDIncluded(accessPointOpts.DirectoryPath, directoryCreated) { + t.Fatalf("Root directory mismatch. Expected: %v (with UID appended), actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) } - if accessPointOpts.Gid != 1001 { - t.Fatalf("Gid mimatched. Expected: %v, actual: %v", accessPointOpts.Uid, 1001) + }) + + res, err := driver.CreateVolume(ctx, req) + + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if res.Volume == nil { + t.Fatal("Volume is nil") + } + + if res.Volume.VolumeId != volumeId { + t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) + } + + mockCtl.Finish() + }, + }, + { + name: "Success: Normal flow with a valid directory structure set, and a basePath, and uniqueness guarantees turned off", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), + } + + pvcName := "foo" + basePath := "bash" + directoryCreated := fmt.Sprintf("/%s/%s", basePath, pvcName) + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "${.PVC.name}", + BasePath: basePath, + EnsureUniqueDirectory: "false", + PvcName: pvcName, + }, + } + + ctx := context.Background() + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + accessPoint := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + } + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.DirectoryPath != directoryCreated { + t.Fatalf("Root directory mismatch. Expected: %v, actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) } }) @@ -161,11 +1098,13 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, { - name: "Success: Normal flow", + name: "Success: Normal flow with a valid directory structure set, but ensuring uniqueness is set incorrectly, so default of true is used." + + "", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -173,10 +1112,13 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), tags: parseTagsFromStr(""), } + pvcName := "foo" + directoryCreated := fmt.Sprintf("/%s", pvcName) + req := &csi.CreateVolumeRequest{ Name: volumeName, VolumeCapabilities: []*csi.VolumeCapability{ @@ -186,12 +1128,14 @@ func TestCreateVolume(t *testing.T) { RequiredBytes: capacityRange, }, Parameters: map[string]string{ - ProvisioningMode: "efs-ap", - FsId: fsId, - GidMin: "1000", - GidMax: "2000", - DirectoryPerms: "777", - AzName: "us-east-1a", + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "${.PVC.name}", + EnsureUniqueDirectory: "banana", + PvcName: pvcName, }, } @@ -204,7 +1148,16 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if !verifyPathWhenUUIDIncluded(accessPointOpts.DirectoryPath, directoryCreated) { + t.Fatalf("Root directory mismatch. Expected: %v (with UID appended), actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) + } + }) res, err := driver.CreateVolume(ctx, req) @@ -219,11 +1172,12 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, { - name: "Success: Using Default GID ranges", + name: "Success: Normal flow with an empty subPath Pattern, no basePath and uniqueness guarantees turned off", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -231,7 +1185,8 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), } req := &csi.CreateVolumeRequest{ @@ -243,10 +1198,13 @@ func TestCreateVolume(t *testing.T) { RequiredBytes: capacityRange, }, Parameters: map[string]string{ - ProvisioningMode: "efs-ap", - FsId: fsId, - DirectoryPerms: "777", - BasePath: "test", + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "", + EnsureUniqueDirectory: "false", }, } @@ -259,7 +1217,16 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.DirectoryPath != "/" { + t.Fatalf("Root directory mismatch. Expected: %v, actual: %v", + "/", + accessPointOpts.DirectoryPath) + } + }) res, err := driver.CreateVolume(ctx, req) @@ -274,11 +1241,12 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, { - name: "Success: Normal flow with tags", + name: "Success: Normal flow with an empty subPath Pattern, and basePath set to /", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -286,8 +1254,8 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), - tags: parseTagsFromStr("cluster:efs"), + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), } req := &csi.CreateVolumeRequest{ @@ -299,11 +1267,14 @@ func TestCreateVolume(t *testing.T) { RequiredBytes: capacityRange, }, Parameters: map[string]string{ - ProvisioningMode: "efs-ap", - FsId: fsId, - GidMin: "1000", - GidMax: "2000", - DirectoryPerms: "777", + ProvisioningMode: "efs-ap", + FsId: fsId, + GidMin: "1000", + GidMax: "2000", + DirectoryPerms: "777", + SubPathPattern: "", + BasePath: "/", + EnsureUniqueDirectory: "false", }, } @@ -316,7 +1287,16 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if accessPointOpts.DirectoryPath != "/" { + t.Fatalf("Root directory mismatch. Expected: %v, actual: %v", + "/", + accessPointOpts.DirectoryPath) + } + }) res, err := driver.CreateVolume(ctx, req) @@ -331,11 +1311,12 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, { - name: "Success: Normal flow with invalid tags", + name: "Success: Normal flow with a valid directory structure set, using repeated elements (uses PVC Name in subpath pattern multiple times)", testFunc: func(t *testing.T) { mockCtl := gomock.NewController(t) mockCloud := mocks.NewMockCloud(mockCtl) @@ -343,10 +1324,13 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), - tags: parseTagsFromStr("cluster-efs"), + gidAllocator: NewGidAllocator(mockCloud), + tags: parseTagsFromStr(""), } + pvcName := "foo" + directoryCreated := fmt.Sprintf("/%s/%s", pvcName, pvcName) + req := &csi.CreateVolumeRequest{ Name: volumeName, VolumeCapabilities: []*csi.VolumeCapability{ @@ -361,6 +1345,8 @@ func TestCreateVolume(t *testing.T) { GidMin: "1000", GidMax: "2000", DirectoryPerms: "777", + SubPathPattern: "${.PVC.name}/${.PVC.name}", + PvcName: pvcName, }, } @@ -373,7 +1359,16 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(accessPoint, nil). + Do(func(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, reuseAccessPointName bool) { + if !verifyPathWhenUUIDIncluded(accessPointOpts.DirectoryPath, directoryCreated) { + t.Fatalf("Root directory mismatch. Expected: %v (with UID appended), actual: %v", + directoryCreated, + accessPointOpts.DirectoryPath) + } + }) res, err := driver.CreateVolume(ctx, req) @@ -388,6 +1383,7 @@ func TestCreateVolume(t *testing.T) { if res.Volume.VolumeId != volumeId { t.Fatalf("Volume Id mismatched. Expected: %v, Actual: %v", volumeId, res.Volume.VolumeId) } + mockCtl.Finish() }, }, @@ -400,7 +1396,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -428,7 +1424,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -457,7 +1453,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -489,7 +1485,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -531,7 +1527,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), tags: parseTagsFromStr(""), } @@ -579,7 +1575,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -621,7 +1617,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -656,7 +1652,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -690,7 +1686,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -724,7 +1720,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -759,7 +1755,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -795,7 +1791,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -831,7 +1827,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -867,7 +1863,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -903,7 +1899,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -939,7 +1935,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -975,7 +1971,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1012,7 +2008,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1049,7 +2045,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1085,7 +2081,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1121,7 +2117,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1159,7 +2155,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1197,7 +2193,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1235,7 +2231,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1260,7 +2256,8 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(nil, errors.New("CreateAccessPoint call failed")) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return([]*cloud.AccessPoint{}, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("CreateAccessPoint call failed")) _, err := driver.CreateVolume(ctx, req) if err == nil { t.Fatal("CreateVolume did not fail") @@ -1277,7 +2274,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1302,7 +2299,8 @@ func TestCreateVolume(t *testing.T) { FileSystemId: fsId, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(nil, cloud.ErrAccessDenied) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return([]*cloud.AccessPoint{}, nil) + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, cloud.ErrAccessDenied) _, err := driver.CreateVolume(ctx, req) if err == nil { t.Fatal("CreateVolume did not fail") @@ -1319,7 +2317,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.CreateVolumeRequest{ @@ -1343,18 +2341,29 @@ func TestCreateVolume(t *testing.T) { fileSystem := &cloud.FileSystem{ FileSystemId: fsId, } - accessPoint := &cloud.AccessPoint{ + ap1 := &cloud.AccessPoint{ + AccessPointId: apId, + FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1000, + Uid: 1000, + }, + } + ap2 := &cloud.AccessPoint{ AccessPointId: apId, FileSystemId: fsId, + PosixUser: &cloud.PosixUser{ + Gid: 1001, + Uid: 1001, + }, } mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil).AnyTimes() - mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any()).Return(accessPoint, nil).AnyTimes() + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return([]*cloud.AccessPoint{ap1, ap2}, nil).AnyTimes() + mockCloud.EXPECT().CreateAccessPoint(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Any()).Return(ap2, nil).AnyTimes() var err error - // Input grants 2 GIDS, third CreateVolume call should result in error - for i := 0; i < 3; i++ { - _, err = driver.CreateVolume(ctx, req) - } + // All GIDs from available range are taken, CreateVolume should fail. + _, err = driver.CreateVolume(ctx, req) if err == nil { t.Fatalf("CreateVolume did not fail") @@ -1371,7 +2380,7 @@ func TestCreateVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), tags: parseTagsFromStr(""), } @@ -1408,6 +2417,153 @@ func TestCreateVolume(t *testing.T) { mockCtl.Finish() }, }, + { + name: "Fail: subPathPattern is specified but uses unsupported attributes", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + subPathPattern := "${.PVC.name}/${foo}" + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + SubPathPattern: subPathPattern, + }, + } + + ctx := context.Background() + + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume did not fail") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("Did not throw InvalidArgument error, instead threw %v", err) + } + mockCtl.Finish() + }, + }, + { + name: "Fail: resulting accessPointDirectory is too over 100 characters", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + subPathPattern := "this-directory-name-is-far-too-long-for-any-practical-purposes-and-only-serves-to-prove-a-point" + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + SubPathPattern: subPathPattern, + }, + } + + ctx := context.Background() + + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume did not fail") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("Did not throw InvalidArgument error, instead threw %v", err) + } + mockCtl.Finish() + }, + }, + { + name: "Fail: resulting accessPointDirectory contains over 4 subdirectories", + testFunc: func(t *testing.T) { + mockCtl := gomock.NewController(t) + mockCloud := mocks.NewMockCloud(mockCtl) + + subPathPattern := "a/b/c/d/e/f" + + driver := &Driver{ + endpoint: endpoint, + cloud: mockCloud, + gidAllocator: NewGidAllocator(mockCloud), + } + + req := &csi.CreateVolumeRequest{ + Name: volumeName, + VolumeCapabilities: []*csi.VolumeCapability{ + stdVolCap, + }, + CapacityRange: &csi.CapacityRange{ + RequiredBytes: capacityRange, + }, + Parameters: map[string]string{ + ProvisioningMode: "efs-ap", + FsId: fsId, + DirectoryPerms: "777", + SubPathPattern: subPathPattern, + }, + } + + ctx := context.Background() + + fileSystem := &cloud.FileSystem{ + FileSystemId: fsId, + } + + mockCloud.EXPECT().DescribeFileSystem(gomock.Eq(ctx), gomock.Any()).Return(fileSystem, nil) + mockCloud.EXPECT().ListAccessPoints(gomock.Eq(ctx), gomock.Any()).Return(nil, nil) + + _, err := driver.CreateVolume(ctx, req) + if err == nil { + t.Fatal("CreateVolume did not fail") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("Did not throw InvalidArgument error, instead threw %v", err) + } + mockCtl.Finish() + }, + }, } for _, tc := range testCases { @@ -1436,7 +2592,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.DeleteVolumeRequest{ @@ -1463,7 +2619,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1502,7 +2658,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1530,7 +2686,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1558,7 +2714,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1586,7 +2742,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1622,7 +2778,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1659,7 +2815,7 @@ func TestDeleteVolume(t *testing.T) { endpoint: endpoint, cloud: mockCloud, mounter: mockMounter, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), deleteAccessPointRootDir: true, } @@ -1695,7 +2851,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.DeleteVolumeRequest{ @@ -1720,7 +2876,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.DeleteVolumeRequest{ @@ -1745,7 +2901,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.DeleteVolumeRequest{ @@ -1770,7 +2926,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } req := &csi.DeleteVolumeRequest{ @@ -1794,7 +2950,7 @@ func TestDeleteVolume(t *testing.T) { driver := &Driver{ endpoint: endpoint, cloud: mockCloud, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), tags: parseTagsFromStr(""), } @@ -1980,3 +3136,11 @@ func TestControllerGetCapabilities(t *testing.T) { t.Fatalf("ControllerGetCapabilities failed: %v", err) } } + +func verifyPathWhenUUIDIncluded(pathToVerify string, expectedPathWithoutUUID string) bool { + r := regexp.MustCompile("(.*)-([0-9A-fA-F]+-[0-9A-fA-F]+-[0-9A-fA-F]+-[0-9A-fA-F]+-[0-9A-fA-F]+$)") + matches := r.FindStringSubmatch(pathToVerify) + doesPathMatchWithUuid := matches[1] == expectedPathWithoutUUID + _, err := uuid.Parse(matches[2]) + return err == nil && doesPathMatchWithUuid +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index c7a974b7d..0fa414c14 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -69,7 +69,7 @@ func NewDriver(endpoint, efsUtilsCfgPath, efsUtilsStaticFilesPath, tags string, volMetricsOptIn: volMetricsOptIn, volMetricsRefreshPeriod: volMetricsRefreshPeriod, volMetricsFsRateLimit: volMetricsFsRateLimit, - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(cloud), deleteAccessPointRootDir: deleteAccessPointRootDir, tags: parseTagsFromStr(strings.TrimSpace(tags)), } diff --git a/pkg/driver/gid_allocator.go b/pkg/driver/gid_allocator.go index f672cdf08..e45f0b687 100644 --- a/pkg/driver/gid_allocator.go +++ b/pkg/driver/gid_allocator.go @@ -1,84 +1,58 @@ package driver import ( - "container/heap" + "context" + "fmt" "sync" + "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud" + "golang.org/x/exp/slices" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/klog/v2" ) -type IntHeap []int +var ACCESS_POINT_PER_FS_LIMIT int = 1000 -func (h IntHeap) Len() int { - return len(h) -} -func (h IntHeap) Less(i, j int) bool { - return h[i] < h[j] -} -func (h IntHeap) Swap(i, j int) { - h[i], h[j] = h[j], h[i] -} - -func (h *IntHeap) Push(x interface{}) { - *h = append(*h, x.(int)) -} - -func (h *IntHeap) Pop() interface{} { - old := *h - n := len(old) - x := old[n-1] - *h = old[0 : n-1] - return x +type FilesystemID struct { + gidMin int + gidMax int } type GidAllocator struct { - fsIdGidMap map[string]*IntHeap + cloud cloud.Cloud + fsIdGidMap map[string]*FilesystemID mu sync.Mutex } -func NewGidAllocator() GidAllocator { +func NewGidAllocator(cloud cloud.Cloud) GidAllocator { return GidAllocator{ - fsIdGidMap: make(map[string]*IntHeap), + cloud: cloud, + fsIdGidMap: make(map[string]*FilesystemID), } } // Retrieves the next available GID -func (g *GidAllocator) getNextGid(fsId string, gidMin, gidMax int) (int, error) { +func (g *GidAllocator) getNextGid(ctx context.Context, fsId string, gidMin, gidMax int) (int64, error) { g.mu.Lock() defer g.mu.Unlock() klog.V(5).Infof("Recieved getNextGid for fsId: %v, min: %v, max: %v", fsId, gidMin, gidMax) - if _, ok := g.fsIdGidMap[fsId]; !ok { - klog.V(5).Infof("FS Id doesn't exist, initializing...") - g.initFsId(fsId, gidMin, gidMax) + usedGids, err := g.getUsedGids(ctx, fsId) + if err != nil { + return 0, status.Errorf(codes.Internal, "Failed to discover used GIDs for filesystem: %v: %v ", fsId, err) } - gidHeap := g.fsIdGidMap[fsId] + gid, err := getNextUnusedGid(usedGids, gidMin, gidMax) - if gidHeap.Len() > 0 { - return heap.Pop(gidHeap).(int), nil - } else { - return 0, status.Errorf(codes.Internal, "Failed to locate a free GID for given the file system: %v. "+ + if err != nil { + return 0, status.Errorf(codes.Internal, "Failed to locate a free GID for given file system: %v. "+ "Please create a new storage class with a new file-system", fsId) } -} -func (g *GidAllocator) releaseGid(fsId string, gid int) { - g.mu.Lock() - defer g.mu.Unlock() - - gidHeap := g.fsIdGidMap[fsId] - gidHeap.Push(gid) -} + return int64(gid), nil -// Creates an entry fsIdGidMap if fsId does not exist. -func (g *GidAllocator) initFsId(fsId string, gidMin, gidMax int) { - h := initHeap(gidMin, gidMax) - heap.Init(h) - g.fsIdGidMap[fsId] = h } func (g *GidAllocator) removeFsId(fsId string) { @@ -87,13 +61,58 @@ func (g *GidAllocator) removeFsId(fsId string) { delete(g.fsIdGidMap, fsId) } -// Initializes a heap inclusive of min & max -func initHeap(min, max int) *IntHeap { - h := make(IntHeap, max-min+1) - val := min - for i := range h { - h[i] = val - val += 1 +func (g *GidAllocator) getUsedGids(ctx context.Context, fsId string) (gids []int64, err error) { + gids = []int64{} + accessPoints, err := g.cloud.ListAccessPoints(ctx, fsId) + if err != nil { + err = fmt.Errorf("failed to list access points: %v", err) + return } - return &h + if len(accessPoints) == 0 { + return gids, nil + } + for _, ap := range accessPoints { + // This should happen only in tests - skip nil pointers. + if ap == nil { + continue + } + if ap != nil && ap.PosixUser == nil { + err = fmt.Errorf("failed to discover used GID because PosixUser is nil for AccessPoint: %s", ap.AccessPointId) + return + } + gids = append(gids, ap.PosixUser.Gid) + } + klog.V(5).Infof("Discovered used GIDs: %+v for FS ID: %v", gids, fsId) + return +} + +func getNextUnusedGid(usedGids []int64, gidMin, gidMax int) (nextGid int, err error) { + requestedRange := gidMax - gidMin + + if requestedRange > ACCESS_POINT_PER_FS_LIMIT { + klog.Warningf("Requested GID range (%v:%v) exceeds EFS Access Point limit (%v) per Filesystem. Driver will not allocate GIDs outside of this limit.", gidMin, gidMax, ACCESS_POINT_PER_FS_LIMIT) + gidMin = gidMax - ACCESS_POINT_PER_FS_LIMIT + } + + var lookup func(usedGids []int64) + lookup = func(usedGids []int64) { + for gid := gidMax; gid > gidMin; gid-- { + if !slices.Contains(usedGids, int64(gid)) { + nextGid = gid + return + } + klog.V(5).Infof("Allocator found GID which is already in use: %v - trying next one.", nextGid) + } + return + } + + nextGid = -1 + lookup(usedGids) + if nextGid == -1 { + err = fmt.Errorf("allocator failed to find available GID") + return + } + + klog.V(5).Infof("Allocator found unused GID: %v", nextGid) + return } diff --git a/pkg/driver/mocks/mock_cloud.go b/pkg/driver/mocks/mock_cloud.go index 85a40d1ce..eacf69fae 100644 --- a/pkg/driver/mocks/mock_cloud.go +++ b/pkg/driver/mocks/mock_cloud.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud (interfaces: Cloud) +// Source: ./pkg/cloud/cloud.go // Package mock_cloud is a generated GoMock package. package mocks @@ -8,90 +8,216 @@ import ( context "context" reflect "reflect" + aws "github.com/aws/aws-sdk-go/aws" + request "github.com/aws/aws-sdk-go/aws/request" + efs "github.com/aws/aws-sdk-go/service/efs" gomock "github.com/golang/mock/gomock" cloud "github.com/kubernetes-sigs/aws-efs-csi-driver/pkg/cloud" ) -// MockCloud is a mock of Cloud interface +// MockEfs is a mock of Efs interface. +type MockEfs struct { + ctrl *gomock.Controller + recorder *MockEfsMockRecorder +} + +// MockEfsMockRecorder is the mock recorder for MockEfs. +type MockEfsMockRecorder struct { + mock *MockEfs +} + +// NewMockEfs creates a new mock instance. +func NewMockEfs(ctrl *gomock.Controller) *MockEfs { + mock := &MockEfs{ctrl: ctrl} + mock.recorder = &MockEfsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEfs) EXPECT() *MockEfsMockRecorder { + return m.recorder +} + +// CreateAccessPointWithContext mocks base method. +func (m *MockEfs) CreateAccessPointWithContext(arg0 aws.Context, arg1 *efs.CreateAccessPointInput, arg2 ...request.Option) (*efs.CreateAccessPointOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateAccessPointWithContext", varargs...) + ret0, _ := ret[0].(*efs.CreateAccessPointOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateAccessPointWithContext indicates an expected call of CreateAccessPointWithContext. +func (mr *MockEfsMockRecorder) CreateAccessPointWithContext(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, "CreateAccessPointWithContext", reflect.TypeOf((*MockEfs)(nil).CreateAccessPointWithContext), varargs...) +} + +// DeleteAccessPointWithContext mocks base method. +func (m *MockEfs) DeleteAccessPointWithContext(arg0 aws.Context, arg1 *efs.DeleteAccessPointInput, arg2 ...request.Option) (*efs.DeleteAccessPointOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteAccessPointWithContext", varargs...) + ret0, _ := ret[0].(*efs.DeleteAccessPointOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAccessPointWithContext indicates an expected call of DeleteAccessPointWithContext. +func (mr *MockEfsMockRecorder) DeleteAccessPointWithContext(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, "DeleteAccessPointWithContext", reflect.TypeOf((*MockEfs)(nil).DeleteAccessPointWithContext), varargs...) +} + +// DescribeAccessPointsWithContext mocks base method. +func (m *MockEfs) DescribeAccessPointsWithContext(arg0 aws.Context, arg1 *efs.DescribeAccessPointsInput, arg2 ...request.Option) (*efs.DescribeAccessPointsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeAccessPointsWithContext", varargs...) + ret0, _ := ret[0].(*efs.DescribeAccessPointsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeAccessPointsWithContext indicates an expected call of DescribeAccessPointsWithContext. +func (mr *MockEfsMockRecorder) DescribeAccessPointsWithContext(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, "DescribeAccessPointsWithContext", reflect.TypeOf((*MockEfs)(nil).DescribeAccessPointsWithContext), varargs...) +} + +// DescribeFileSystemsWithContext mocks base method. +func (m *MockEfs) DescribeFileSystemsWithContext(arg0 aws.Context, arg1 *efs.DescribeFileSystemsInput, arg2 ...request.Option) (*efs.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].(*efs.DescribeFileSystemsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeFileSystemsWithContext indicates an expected call of DescribeFileSystemsWithContext. +func (mr *MockEfsMockRecorder) 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((*MockEfs)(nil).DescribeFileSystemsWithContext), varargs...) +} + +// DescribeMountTargetsWithContext mocks base method. +func (m *MockEfs) DescribeMountTargetsWithContext(arg0 aws.Context, arg1 *efs.DescribeMountTargetsInput, arg2 ...request.Option) (*efs.DescribeMountTargetsOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeMountTargetsWithContext", varargs...) + ret0, _ := ret[0].(*efs.DescribeMountTargetsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeMountTargetsWithContext indicates an expected call of DescribeMountTargetsWithContext. +func (mr *MockEfsMockRecorder) DescribeMountTargetsWithContext(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, "DescribeMountTargetsWithContext", reflect.TypeOf((*MockEfs)(nil).DescribeMountTargetsWithContext), varargs...) +} + +// MockCloud is a mock of Cloud interface. type MockCloud struct { ctrl *gomock.Controller recorder *MockCloudMockRecorder } -// MockCloudMockRecorder is the mock recorder for MockCloud +// MockCloudMockRecorder is the mock recorder for MockCloud. type MockCloudMockRecorder struct { mock *MockCloud } -// NewMockCloud creates a new mock instance +// 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 +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCloud) EXPECT() *MockCloudMockRecorder { return m.recorder } -// CreateAccessPoint mocks base method -func (m *MockCloud) CreateAccessPoint(ctx context.Context, volumeName string, accessPointOpts *cloud.AccessPointOptions) (*cloud.AccessPoint, error) { +// CreateAccessPoint mocks base method. +func (m *MockCloud) CreateAccessPoint(ctx context.Context, clientToken string, accessPointOpts *cloud.AccessPointOptions, usePvcName bool) (*cloud.AccessPoint, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAccessPoint", ctx, volumeName, accessPointOpts) + ret := m.ctrl.Call(m, "CreateAccessPoint", ctx, clientToken, accessPointOpts, usePvcName) ret0, _ := ret[0].(*cloud.AccessPoint) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreateAccessPoint indicates an expected call of CreateAccessPoint -func (mr *MockCloudMockRecorder) CreateAccessPoint(ctx, volumeName, accessPointOpts interface{}) *gomock.Call { +// CreateAccessPoint indicates an expected call of CreateAccessPoint. +func (mr *MockCloudMockRecorder) CreateAccessPoint(ctx, clientToken, accessPointOpts, usePvcName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessPoint", reflect.TypeOf((*MockCloud)(nil).CreateAccessPoint), ctx, volumeName, accessPointOpts) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessPoint", reflect.TypeOf((*MockCloud)(nil).CreateAccessPoint), ctx, clientToken, accessPointOpts, usePvcName) } -// DeleteAccessPoint mocks base method -func (m *MockCloud) DeleteAccessPoint(arg0 context.Context, arg1 string) error { +// DeleteAccessPoint mocks base method. +func (m *MockCloud) DeleteAccessPoint(ctx context.Context, accessPointId string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAccessPoint", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteAccessPoint", ctx, accessPointId) ret0, _ := ret[0].(error) return ret0 } -// DeleteAccessPoint indicates an expected call of DeleteAccessPoint -func (mr *MockCloudMockRecorder) DeleteAccessPoint(arg0, arg1 interface{}) *gomock.Call { +// DeleteAccessPoint indicates an expected call of DeleteAccessPoint. +func (mr *MockCloudMockRecorder) DeleteAccessPoint(ctx, accessPointId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessPoint", reflect.TypeOf((*MockCloud)(nil).DeleteAccessPoint), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessPoint", reflect.TypeOf((*MockCloud)(nil).DeleteAccessPoint), ctx, accessPointId) } -// DescribeAccessPoint mocks base method -func (m *MockCloud) DescribeAccessPoint(arg0 context.Context, arg1 string) (*cloud.AccessPoint, error) { +// DescribeAccessPoint mocks base method. +func (m *MockCloud) DescribeAccessPoint(ctx context.Context, accessPointId string) (*cloud.AccessPoint, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeAccessPoint", arg0, arg1) + ret := m.ctrl.Call(m, "DescribeAccessPoint", ctx, accessPointId) ret0, _ := ret[0].(*cloud.AccessPoint) ret1, _ := ret[1].(error) return ret0, ret1 } -// DescribeAccessPoint indicates an expected call of DescribeAccessPoint -func (mr *MockCloudMockRecorder) DescribeAccessPoint(arg0, arg1 interface{}) *gomock.Call { +// DescribeAccessPoint indicates an expected call of DescribeAccessPoint. +func (mr *MockCloudMockRecorder) DescribeAccessPoint(ctx, accessPointId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeAccessPoint", reflect.TypeOf((*MockCloud)(nil).DescribeAccessPoint), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeAccessPoint", reflect.TypeOf((*MockCloud)(nil).DescribeAccessPoint), ctx, accessPointId) } -// DescribeFileSystem mocks base method -func (m *MockCloud) DescribeFileSystem(arg0 context.Context, arg1 string) (*cloud.FileSystem, error) { +// DescribeFileSystem mocks base method. +func (m *MockCloud) DescribeFileSystem(ctx context.Context, fileSystemId string) (*cloud.FileSystem, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeFileSystem", arg0, arg1) + ret := m.ctrl.Call(m, "DescribeFileSystem", ctx, fileSystemId) 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 { +// DescribeFileSystem indicates an expected call of DescribeFileSystem. +func (mr *MockCloudMockRecorder) DescribeFileSystem(ctx, fileSystemId interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeFileSystem", reflect.TypeOf((*MockCloud)(nil).DescribeFileSystem), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeFileSystem", reflect.TypeOf((*MockCloud)(nil).DescribeFileSystem), ctx, fileSystemId) } // DescribeMountTargets mocks base method. @@ -109,7 +235,7 @@ func (mr *MockCloudMockRecorder) DescribeMountTargets(ctx, fileSystemId, az inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeMountTargets", reflect.TypeOf((*MockCloud)(nil).DescribeMountTargets), ctx, fileSystemId, az) } -// GetMetadata mocks base method +// GetMetadata mocks base method. func (m *MockCloud) GetMetadata() cloud.MetadataService { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetMetadata") @@ -117,8 +243,23 @@ func (m *MockCloud) GetMetadata() cloud.MetadataService { return ret0 } -// GetMetadata indicates an expected call of GetMetadata +// 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)) } + +// ListAccessPoints mocks base method. +func (m *MockCloud) ListAccessPoints(arg0 context.Context, arg1 string) ([]*cloud.AccessPoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAccessPoints", arg0, arg1) + ret0, _ := ret[0].([]*cloud.AccessPoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAccessPoints indicates an expected call of ListAccessPoints. +func (mr *MockCloudMockRecorder) ListAccessPoints(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccessPoints", reflect.TypeOf((*MockCloud)(nil).ListAccessPoints), arg0, arg1) +} diff --git a/pkg/driver/sanity_test.go b/pkg/driver/sanity_test.go index 09d9770ca..c2c55bf65 100644 --- a/pkg/driver/sanity_test.go +++ b/pkg/driver/sanity_test.go @@ -56,6 +56,7 @@ func TestSanityEFSCSI(t *testing.T) { parameters[FsId] = "fs-1234abcd" parameters[ProvisioningMode] = "efs-ap" parameters[DirectoryPerms] = "777" + parameters[SubPathPattern] = "/foo" config := sanity.NewTestConfig() config.TargetPath = targetPath @@ -68,16 +69,17 @@ func TestSanityEFSCSI(t *testing.T) { nodeCaps := SetNodeCapOptInFeatures(true) mockCtrl := gomock.NewController(t) + mockCloud := cloud.NewFakeCloudProvider() drv := Driver{ endpoint: endpoint, nodeID: "sanity", mounter: NewFakeMounter(), efsWatchdog: &mockWatchdog{}, - cloud: cloud.NewFakeCloudProvider(), + cloud: mockCloud, nodeCaps: nodeCaps, volMetricsOptIn: true, volStatter: NewVolStatter(), - gidAllocator: NewGidAllocator(), + gidAllocator: NewGidAllocator(mockCloud), } defer func() { if r := recover(); r != nil { diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index ac228a7a9..bc65e81e1 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -360,9 +360,108 @@ var _ = ginkgo.Describe("[efs-csi] EFS CSI", func() { encryptInTransit := false testEncryptInTransit(f, &encryptInTransit) }) + + ginkgo.It("should successfully perform dynamic provisioning", func() { + + ginkgo.By("Creating EFS Storage Class, PVC and associated PV") + params := map[string]string{ + "provisioningMode": "efs-ap", + "fileSystemId": FileSystemId, + "subPathPattern": "${.PVC.name}", + "directoryPerms": "700", + "gidRangeStart": "1000", + "gidRangeEnd": "2000", + "basePath": "/dynamic_provisioning", + "ensureUniqueDirectory": "true", + } + + sc := GetStorageClass(params) + sc, err := f.ClientSet.StorageV1().StorageClasses().Create(context.TODO(), sc, metav1.CreateOptions{}) + framework.ExpectNoError(err, "creating storage class") + pvc, err := createEFSPVCPVDynamicProvisioning(f.ClientSet, f.Namespace.Name, f.Namespace.Name, sc.Name) + framework.ExpectNoError(err, "creating pvc") + + ginkgo.By("Deploying a pod that applies the PVC and writes data") + testData := "DP TEST" + writePath := "/mnt/volume1/out" + writeCommand := fmt.Sprintf("echo \"%s\" >> %s", testData, writePath) + pod := e2epod.MakePod(f.Namespace.Name, nil, []*v1.PersistentVolumeClaim{pvc}, false, writeCommand) + pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(), pod, metav1.CreateOptions{}) + framework.ExpectNoError(err, "creating pod") + framework.ExpectNoError(e2epod.WaitForPodSuccessInNamespace(f.ClientSet, pod.Name, f.Namespace.Name), "waiting for pod success") + _ = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) + + ginkgo.By("Deploying a second pod that reads the data") + pod = e2epod.MakePod(f.Namespace.Name, nil, []*v1.PersistentVolumeClaim{pvc}, false, "while true; do echo $(date -u); sleep 5; done") + pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(), pod, metav1.CreateOptions{}) + framework.ExpectNoError(err, "creating pod") + framework.ExpectNoError(e2epod.WaitForPodNameRunningInNamespace(f.ClientSet, pod.Name, f.Namespace.Name), "waiting for pod running") + + readCommand := fmt.Sprintf("cat %s", writePath) + output := framework.RunKubectlOrDie(f.Namespace.Name, "exec", pod.Name, "--", "/bin/sh", "-c", readCommand) + output = strings.TrimSuffix(output, "\n") + framework.Logf("The output is: %s", output) + + defer func() { + _ = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) + _ = f.ClientSet.CoreV1().PersistentVolumeClaims(f.Namespace.Name).Delete(context.TODO(), pvc.Name, metav1.DeleteOptions{}) + _ = f.ClientSet.StorageV1().StorageClasses().Delete(context.TODO(), sc.Name, metav1.DeleteOptions{}) + }() + + if output == "" { + ginkgo.Fail("Read data is empty.") + } + if output != testData { + ginkgo.Fail("Read data does not match write data.") + } + }) + }) }) +func createEFSPVCPVDynamicProvisioning(c clientset.Interface, namespace, name, storageClassName string) (*v1.PersistentVolumeClaim, error) { + pvc := makeEFSPVCDynamicProvisioning(namespace, name, storageClassName) + pvc, err := c.CoreV1().PersistentVolumeClaims(namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return pvc, nil +} + +func makeEFSPVCDynamicProvisioning(namespace, name string, storageClassName string) *v1.PersistentVolumeClaim { + return &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + StorageClassName: &storageClassName, + }, + } +} + +func GetStorageClass(params map[string]string) *storagev1.StorageClass { + parameters := params + + generateName := fmt.Sprintf("efs-csi-dynamic-sc-test1234-") + + defaultBindingMode := storagev1.VolumeBindingImmediate + return &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: generateName + generateRandomString(4), + }, + Provisioner: "efs.csi.aws.com", + Parameters: parameters, + VolumeBindingMode: &defaultBindingMode, + } +} + func createEFSPVCPV(c clientset.Interface, namespace, name, path string, volumeAttributes map[string]string) (*v1.PersistentVolumeClaim, *v1.PersistentVolume, error) { pvc, pv := makeEFSPVCPV(namespace, name, path, volumeAttributes) pvc, err := c.CoreV1().PersistentVolumeClaims(namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) diff --git a/vendor/golang.org/x/exp/LICENSE b/vendor/golang.org/x/exp/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/golang.org/x/exp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/exp/PATENTS b/vendor/golang.org/x/exp/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/vendor/golang.org/x/exp/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go new file mode 100644 index 000000000..2c033dff4 --- /dev/null +++ b/vendor/golang.org/x/exp/constraints/constraints.go @@ -0,0 +1,50 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type Ordered interface { + Integer | Float | ~string +} diff --git a/vendor/golang.org/x/exp/slices/cmp.go b/vendor/golang.org/x/exp/slices/cmp.go new file mode 100644 index 000000000..fbf1934a0 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/cmp.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +import "golang.org/x/exp/constraints" + +// min is a version of the predeclared function from the Go 1.21 release. +func min[T constraints.Ordered](a, b T) T { + if a < b || isNaN(a) { + return a + } + return b +} + +// max is a version of the predeclared function from the Go 1.21 release. +func max[T constraints.Ordered](a, b T) T { + if a > b || isNaN(a) { + return a + } + return b +} + +// cmpLess is a copy of cmp.Less from the Go 1.21 release. +func cmpLess[T constraints.Ordered](x, y T) bool { + return (isNaN(x) && !isNaN(y)) || x < y +} + +// cmpCompare is a copy of cmp.Compare from the Go 1.21 release. +func cmpCompare[T constraints.Ordered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN && yNaN { + return 0 + } + if xNaN || x < y { + return -1 + } + if yNaN || x > y { + return +1 + } + return 0 +} diff --git a/vendor/golang.org/x/exp/slices/slices.go b/vendor/golang.org/x/exp/slices/slices.go new file mode 100644 index 000000000..5e8158bba --- /dev/null +++ b/vendor/golang.org/x/exp/slices/slices.go @@ -0,0 +1,499 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package slices defines various functions useful with slices of any type. +package slices + +import ( + "unsafe" + + "golang.org/x/exp/constraints" +) + +// Equal reports whether two slices are equal: the same length and all +// elements equal. If the lengths are different, Equal returns false. +// Otherwise, the elements are compared in increasing index order, and the +// comparison stops at the first unequal pair. +// Floating point NaNs are not considered equal. +func Equal[S ~[]E, E comparable](s1, s2 S) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} + +// EqualFunc reports whether two slices are equal using an equality +// function on each pair of elements. If the lengths are different, +// EqualFunc returns false. Otherwise, the elements are compared in +// increasing index order, and the comparison stops at the first index +// for which eq returns false. +func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool { + if len(s1) != len(s2) { + return false + } + for i, v1 := range s1 { + v2 := s2[i] + if !eq(v1, v2) { + return false + } + } + return true +} + +// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair +// of elements. The elements are compared sequentially, starting at index 0, +// until one element is not equal to the other. +// The result of comparing the first non-matching elements is returned. +// If both slices are equal until one of them ends, the shorter slice is +// considered less than the longer one. +// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2. +func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmpCompare(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// CompareFunc is like [Compare] but uses a custom comparison function on each +// pair of elements. +// The result is the first non-zero result of cmp; if cmp always +// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2), +// and +1 if len(s1) > len(s2). +func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int { + for i, v1 := range s1 { + if i >= len(s2) { + return +1 + } + v2 := s2[i] + if c := cmp(v1, v2); c != 0 { + return c + } + } + if len(s1) < len(s2) { + return -1 + } + return 0 +} + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// IndexFunc returns the first index i satisfying f(s[i]), +// or -1 if none do. +func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +// ContainsFunc reports whether at least one +// element e of s satisfies f(e). +func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return IndexFunc(s, f) >= 0 +} + +// Insert inserts the values v... into s at index i, +// returning the modified slice. +// The elements at s[i:] are shifted up to make room. +// In the returned slice r, r[i] == v[0], +// and r[i+len(v)] == value originally at r[i]. +// Insert panics if i is out of range. +// This function is O(len(s) + len(v)). +func Insert[S ~[]E, E any](s S, i int, v ...E) S { + m := len(v) + if m == 0 { + return s + } + n := len(s) + if i == n { + return append(s, v...) + } + if n+m > cap(s) { + // Use append rather than make so that we bump the size of + // the slice up to the next storage class. + // This is what Grow does but we don't call Grow because + // that might copy the values twice. + s2 := append(s[:i], make(S, n+m-i)...) + copy(s2[i:], v) + copy(s2[i+m:], s[i:]) + return s2 + } + s = s[:n+m] + + // before: + // s: aaaaaaaabbbbccccccccdddd + // ^ ^ ^ ^ + // i i+m n n+m + // after: + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // + // a are the values that don't move in s. + // v are the values copied in from v. + // b and c are the values from s that are shifted up in index. + // d are the values that get overwritten, never to be seen again. + + if !overlaps(v, s[i+m:]) { + // Easy case - v does not overlap either the c or d regions. + // (It might be in some of a or b, or elsewhere entirely.) + // The data we copy up doesn't write to v at all, so just do it. + + copy(s[i+m:], s[i:]) + + // Now we have + // s: aaaaaaaabbbbbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // Note the b values are duplicated. + + copy(s[i:], v) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s + } + + // The hard case - v overlaps c or d. We can't just shift up + // the data because we'd move or clobber the values we're trying + // to insert. + // So instead, write v on top of d, then rotate. + copy(s[n:], v) + + // Now we have + // s: aaaaaaaabbbbccccccccvvvv + // ^ ^ ^ ^ + // i i+m n n+m + + rotateRight(s[i:], m) + + // Now we have + // s: aaaaaaaavvvvbbbbcccccccc + // ^ ^ ^ ^ + // i i+m n n+m + // That's the result we want. + return s +} + +// Delete removes the elements s[i:j] from s, returning the modified slice. +// Delete panics if s[i:j] is not a valid slice of s. +// Delete is O(len(s)-j), so if many items must be deleted, it is better to +// make a single call deleting them all together than to delete one at a time. +// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those +// elements contain pointers you might consider zeroing those elements so that +// objects they reference can be garbage collected. +func Delete[S ~[]E, E any](s S, i, j int) S { + _ = s[i:j] // bounds check + + return append(s[:i], s[j:]...) +} + +// DeleteFunc removes any elements from s for which del returns true, +// returning the modified slice. +// When DeleteFunc removes m elements, it might not modify the elements +// s[len(s)-m:len(s)]. If those elements contain pointers you might consider +// zeroing those elements so that objects they reference can be garbage +// collected. +func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { + i := IndexFunc(s, del) + if i == -1 { + return s + } + // Don't start copying elements until we find one to delete. + for j := i + 1; j < len(s); j++ { + if v := s[j]; !del(v) { + s[i] = v + i++ + } + } + return s[:i] +} + +// Replace replaces the elements s[i:j] by the given v, and returns the +// modified slice. Replace panics if s[i:j] is not a valid slice of s. +func Replace[S ~[]E, E any](s S, i, j int, v ...E) S { + _ = s[i:j] // verify that i:j is a valid subslice + + if i == j { + return Insert(s, i, v...) + } + if j == len(s) { + return append(s[:i], v...) + } + + tot := len(s[:i]) + len(v) + len(s[j:]) + if tot > cap(s) { + // Too big to fit, allocate and copy over. + s2 := append(s[:i], make(S, tot-i)...) // See Insert + copy(s2[i:], v) + copy(s2[i+len(v):], s[j:]) + return s2 + } + + r := s[:tot] + + if i+len(v) <= j { + // Easy, as v fits in the deleted portion. + copy(r[i:], v) + if i+len(v) != j { + copy(r[i+len(v):], s[j:]) + } + return r + } + + // We are expanding (v is bigger than j-i). + // The situation is something like this: + // (example has i=4,j=8,len(s)=16,len(v)=6) + // s: aaaaxxxxbbbbbbbbyy + // ^ ^ ^ ^ + // i j len(s) tot + // a: prefix of s + // x: deleted range + // b: more of s + // y: area to expand into + + if !overlaps(r[i+len(v):], v) { + // Easy, as v is not clobbered by the first copy. + copy(r[i+len(v):], s[j:]) + copy(r[i:], v) + return r + } + + // This is a situation where we don't have a single place to which + // we can copy v. Parts of it need to go to two different places. + // We want to copy the prefix of v into y and the suffix into x, then + // rotate |y| spots to the right. + // + // v[2:] v[:2] + // | | + // s: aaaavvvvbbbbbbbbvv + // ^ ^ ^ ^ + // i j len(s) tot + // + // If either of those two destinations don't alias v, then we're good. + y := len(v) - (j - i) // length of y portion + + if !overlaps(r[i:j], v) { + copy(r[i:j], v[y:]) + copy(r[len(s):], v[:y]) + rotateRight(r[i:], y) + return r + } + if !overlaps(r[len(s):], v) { + copy(r[len(s):], v[:y]) + copy(r[i:j], v[y:]) + rotateRight(r[i:], y) + return r + } + + // Now we know that v overlaps both x and y. + // That means that the entirety of b is *inside* v. + // So we don't need to preserve b at all; instead we + // can copy v first, then copy the b part of v out of + // v to the right destination. + k := startIdx(v, s[j:]) + copy(r[i:], v) + copy(r[i+len(v):], r[i+k:]) + return r +} + +// Clone returns a copy of the slice. +// The elements are copied using assignment, so this is a shallow clone. +func Clone[S ~[]E, E any](s S) S { + // Preserve nil in case it matters. + if s == nil { + return nil + } + return append(S([]E{}), s...) +} + +// Compact replaces consecutive runs of equal elements with a single copy. +// This is like the uniq command found on Unix. +// Compact modifies the contents of the slice s and returns the modified slice, +// which may have a smaller length. +// When Compact discards m elements in total, it might not modify the elements +// s[len(s)-m:len(s)]. If those elements contain pointers you might consider +// zeroing those elements so that objects they reference can be garbage collected. +func Compact[S ~[]E, E comparable](s S) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if s[k] != s[k-1] { + if i != k { + s[i] = s[k] + } + i++ + } + } + return s[:i] +} + +// CompactFunc is like [Compact] but uses an equality function to compare elements. +// For runs of elements that compare equal, CompactFunc keeps the first one. +func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if !eq(s[k], s[k-1]) { + if i != k { + s[i] = s[k] + } + i++ + } + } + return s[:i] +} + +// Grow increases the slice's capacity, if necessary, to guarantee space for +// another n elements. After Grow(n), at least n elements can be appended +// to the slice without another allocation. If n is negative or too large to +// allocate the memory, Grow panics. +func Grow[S ~[]E, E any](s S, n int) S { + if n < 0 { + panic("cannot be negative") + } + if n -= cap(s) - len(s); n > 0 { + // TODO(https://go.dev/issue/53888): Make using []E instead of S + // to workaround a compiler bug where the runtime.growslice optimization + // does not take effect. Revert when the compiler is fixed. + s = append([]E(s)[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +// Clip removes unused capacity from the slice, returning s[:len(s):len(s)]. +func Clip[S ~[]E, E any](s S) S { + return s[:len(s):len(s)] +} + +// Rotation algorithm explanation: +// +// rotate left by 2 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join first parts +// 89234567 01 +// recursively rotate first left part by 2 +// 23456789 01 +// join at the end +// 2345678901 +// +// rotate left by 8 +// start with +// 0123456789 +// split up like this +// 01 234567 89 +// swap first 2 and last 2 +// 89 234567 01 +// join last parts +// 89 23456701 +// recursively rotate second part left by 6 +// 89 01234567 +// join at the end +// 8901234567 + +// TODO: There are other rotate algorithms. +// This algorithm has the desirable property that it moves each element exactly twice. +// The triple-reverse algorithm is simpler and more cache friendly, but takes more writes. +// The follow-cycles algorithm can be 1-write but it is not very cache friendly. + +// rotateLeft rotates b left by n spaces. +// s_final[i] = s_orig[i+r], wrapping around. +func rotateLeft[E any](s []E, r int) { + for r != 0 && r != len(s) { + if r*2 <= len(s) { + swap(s[:r], s[len(s)-r:]) + s = s[:len(s)-r] + } else { + swap(s[:len(s)-r], s[r:]) + s, r = s[len(s)-r:], r*2-len(s) + } + } +} +func rotateRight[E any](s []E, r int) { + rotateLeft(s, len(s)-r) +} + +// swap swaps the contents of x and y. x and y must be equal length and disjoint. +func swap[E any](x, y []E) { + for i := 0; i < len(x); i++ { + x[i], y[i] = y[i], x[i] + } +} + +// overlaps reports whether the memory ranges a[0:len(a)] and b[0:len(b)] overlap. +func overlaps[E any](a, b []E) bool { + if len(a) == 0 || len(b) == 0 { + return false + } + elemSize := unsafe.Sizeof(a[0]) + if elemSize == 0 { + return false + } + // TODO: use a runtime/unsafe facility once one becomes available. See issue 12445. + // Also see crypto/internal/alias/alias.go:AnyOverlap + return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) && + uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1) +} + +// startIdx returns the index in haystack where the needle starts. +// prerequisite: the needle must be aliased entirely inside the haystack. +func startIdx[E any](haystack, needle []E) int { + p := &needle[0] + for i := range haystack { + if p == &haystack[i] { + return i + } + } + // TODO: what if the overlap is by a non-integral number of Es? + panic("needle not found") +} + +// Reverse reverses the elements of the slice in place. +func Reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} diff --git a/vendor/golang.org/x/exp/slices/sort.go b/vendor/golang.org/x/exp/slices/sort.go new file mode 100644 index 000000000..b67897f76 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/sort.go @@ -0,0 +1,195 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp + +package slices + +import ( + "math/bits" + + "golang.org/x/exp/constraints" +) + +// Sort sorts a slice of any ordered type in ascending order. +// When sorting floating-point numbers, NaNs are ordered before other values. +func Sort[S ~[]E, E constraints.Ordered](x S) { + n := len(x) + pdqsortOrdered(x, 0, n, bits.Len(uint(n))) +} + +// SortFunc sorts the slice x in ascending order as determined by the cmp +// function. This sort is not guaranteed to be stable. +// cmp(a, b) should return a negative number when a < b, a positive number when +// a > b and zero when a == b. +// +// SortFunc requires that cmp is a strict weak ordering. +// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings. +func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + n := len(x) + pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp) +} + +// SortStableFunc sorts the slice x while keeping the original order of equal +// elements, using cmp to compare elements in the same way as [SortFunc]. +func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + stableCmpFunc(x, len(x), cmp) +} + +// IsSorted reports whether x is sorted in ascending order. +func IsSorted[S ~[]E, E constraints.Ordered](x S) bool { + for i := len(x) - 1; i > 0; i-- { + if cmpLess(x[i], x[i-1]) { + return false + } + } + return true +} + +// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the +// comparison function as defined by [SortFunc]. +func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { + for i := len(x) - 1; i > 0; i-- { + if cmp(x[i], x[i-1]) < 0 { + return false + } + } + return true +} + +// Min returns the minimal value in x. It panics if x is empty. +// For floating-point numbers, Min propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Min[S ~[]E, E constraints.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Min: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + m = min(m, x[i]) + } + return m +} + +// MinFunc returns the minimal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one minimal element +// according to the cmp function, MinFunc returns the first one. +func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MinFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) < 0 { + m = x[i] + } + } + return m +} + +// Max returns the maximal value in x. It panics if x is empty. +// For floating-point E, Max propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Max[S ~[]E, E constraints.Ordered](x S) E { + if len(x) < 1 { + panic("slices.Max: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + m = max(m, x[i]) + } + return m +} + +// MaxFunc returns the maximal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one maximal element +// according to the cmp function, MaxFunc returns the first one. +func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + if len(x) < 1 { + panic("slices.MaxFunc: empty list") + } + m := x[0] + for i := 1; i < len(x); i++ { + if cmp(x[i], m) > 0 { + m = x[i] + } + } + return m +} + +// BinarySearch searches for target in a sorted slice and returns the position +// where target is found, or the position where target would appear in the +// sort order; it also returns a bool saying whether the target is really found +// in the slice. The slice must be sorted in increasing order. +func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) { + // Inlining is faster than calling BinarySearchFunc with a lambda. + n := len(x) + // Define x[-1] < target and x[n] >= target. + // Invariant: x[i-1] < target, x[j] >= target. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmpLess(x[h], target) { + i = h + 1 // preserves x[i-1] < target + } else { + j = h // preserves x[j] >= target + } + } + // i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i. + return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target))) +} + +// BinarySearchFunc works like [BinarySearch], but uses a custom comparison +// function. The slice must be sorted in increasing order, where "increasing" +// is defined by cmp. cmp should return 0 if the slice element matches +// the target, a negative number if the slice element precedes the target, +// or a positive number if the slice element follows the target. +// cmp must implement the same ordering as the slice, such that if +// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice. +func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) { + n := len(x) + // Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 . + // Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0. + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) // avoid overflow when computing h + // i ≤ h < j + if cmp(x[h], target) < 0 { + i = h + 1 // preserves cmp(x[i - 1], target) < 0 + } else { + j = h // preserves cmp(x[j], target) >= 0 + } + } + // i == j, cmp(x[i-1], target) < 0, and cmp(x[j], target) (= cmp(x[i], target)) >= 0 => answer is i. + return i, i < n && cmp(x[i], target) == 0 +} + +type sortedHint int // hint for pdqsort when choosing the pivot + +const ( + unknownHint sortedHint = iota + increasingHint + decreasingHint +) + +// xorshift paper: https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf +type xorshift uint64 + +func (r *xorshift) Next() uint64 { + *r ^= *r << 13 + *r ^= *r >> 17 + *r ^= *r << 5 + return uint64(*r) +} + +func nextPowerOfTwo(length int) uint { + return 1 << bits.Len(uint(length)) +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T constraints.Ordered](x T) bool { + return x != x +} diff --git a/vendor/golang.org/x/exp/slices/zsortanyfunc.go b/vendor/golang.org/x/exp/slices/zsortanyfunc.go new file mode 100644 index 000000000..06f2c7a24 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/zsortanyfunc.go @@ -0,0 +1,479 @@ +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +// insertionSortCmpFunc sorts data[a:b] using insertion sort. +func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownCmpFunc implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownCmpFunc[E any](data []E, lo, hi, first int, cmp func(a, b E) int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && (cmp(data[first+child], data[first+child+1]) < 0) { + child++ + } + if !(cmp(data[first+root], data[first+child]) < 0) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownCmpFunc(data, i, hi, first, cmp) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownCmpFunc(data, lo, i, first, cmp) + } +} + +// pdqsortCmpFunc sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortCmpFunc(data, a, b, cmp) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortCmpFunc(data, a, b, cmp) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsCmpFunc(data, a, b, cmp) + limit-- + } + + pivot, hint := choosePivotCmpFunc(data, a, b, cmp) + if hint == decreasingHint { + reverseRangeCmpFunc(data, a, b, cmp) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortCmpFunc(data, a, b, cmp) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) { + mid := partitionEqualCmpFunc(data, a, b, pivot, cmp) + a = mid + continue + } + + mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortCmpFunc(data, a, mid, limit, cmp) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortCmpFunc(data, mid+1, b, limit, cmp) + b = mid + } + } +} + +// partitionCmpFunc does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && (cmp(data[i], data[a]) < 0) { + i++ + } + for i <= j && !(cmp(data[j], data[a]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualCmpFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !(cmp(data[a], data[i]) < 0) { + i++ + } + for i <= j && (cmp(data[a], data[j]) < 0) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !(cmp(data[i], data[i-1]) < 0) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !(cmp(data[j], data[j-1]) < 0) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotCmpFunc chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentCmpFunc(data, i, &swaps, cmp) + j = medianAdjacentCmpFunc(data, j, &swaps, cmp) + k = medianAdjacentCmpFunc(data, k, &swaps, cmp) + } + // Find the median among i, j, k and stores it into j. + j = medianCmpFunc(data, i, j, k, &swaps, cmp) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) { + if cmp(data[b], data[a]) < 0 { + *swaps++ + return b, a + } + return a, b +} + +// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int { + a, b = order2CmpFunc(data, a, b, swaps, cmp) + b, c = order2CmpFunc(data, b, c, swaps, cmp) + a, b = order2CmpFunc(data, a, b, swaps, cmp) + return b +} + +// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int { + return medianCmpFunc(data, a-1, a, a+1, swaps, cmp) +} + +func reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortCmpFunc(data, a, b, cmp) + a = b + b += blockSize + } + insertionSortCmpFunc(data, a, n, cmp) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeCmpFunc(data, a, a+blockSize, b, cmp) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeCmpFunc(data, a, m, n, cmp) + } + blockSize *= 2 + } +} + +// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmp(data[h], data[a]) < 0 { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !(cmp(data[m], data[h]) < 0) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !(cmp(data[p-c], data[c]) < 0) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateCmpFunc(data, start, m, end, cmp) + } + if a < start && start < mid { + symMergeCmpFunc(data, a, start, mid, cmp) + } + if mid < end && end < b { + symMergeCmpFunc(data, mid, end, b, cmp) + } +} + +// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeCmpFunc(data, m-i, m, j, cmp) + i -= j + } else { + swapRangeCmpFunc(data, m-i, m+j-i, i, cmp) + j -= i + } + } + // i == j + swapRangeCmpFunc(data, m-i, m, i, cmp) +} diff --git a/vendor/golang.org/x/exp/slices/zsortordered.go b/vendor/golang.org/x/exp/slices/zsortordered.go new file mode 100644 index 000000000..99b47c398 --- /dev/null +++ b/vendor/golang.org/x/exp/slices/zsortordered.go @@ -0,0 +1,481 @@ +// Code generated by gen_sort_variants.go; DO NOT EDIT. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package slices + +import "golang.org/x/exp/constraints" + +// insertionSortOrdered sorts data[a:b] using insertion sort. +func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && cmpLess(data[j], data[j-1]); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +// siftDownOrdered implements the heap property on data[lo:hi]. +// first is an offset into the array where the root of the heap lies. +func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) { + root := lo + for { + child := 2*root + 1 + if child >= hi { + break + } + if child+1 < hi && cmpLess(data[first+child], data[first+child+1]) { + child++ + } + if !cmpLess(data[first+root], data[first+child]) { + return + } + data[first+root], data[first+child] = data[first+child], data[first+root] + root = child + } +} + +func heapSortOrdered[E constraints.Ordered](data []E, a, b int) { + first := a + lo := 0 + hi := b - a + + // Build heap with greatest element at top. + for i := (hi - 1) / 2; i >= 0; i-- { + siftDownOrdered(data, i, hi, first) + } + + // Pop elements, largest first, into end of data. + for i := hi - 1; i >= 0; i-- { + data[first], data[first+i] = data[first+i], data[first] + siftDownOrdered(data, lo, i, first) + } +} + +// pdqsortOrdered sorts data[a:b]. +// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort. +// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf +// C++ implementation: https://github.com/orlp/pdqsort +// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/ +// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort. +func pdqsortOrdered[E constraints.Ordered](data []E, a, b, limit int) { + const maxInsertion = 12 + + var ( + wasBalanced = true // whether the last partitioning was reasonably balanced + wasPartitioned = true // whether the slice was already partitioned + ) + + for { + length := b - a + + if length <= maxInsertion { + insertionSortOrdered(data, a, b) + return + } + + // Fall back to heapsort if too many bad choices were made. + if limit == 0 { + heapSortOrdered(data, a, b) + return + } + + // If the last partitioning was imbalanced, we need to breaking patterns. + if !wasBalanced { + breakPatternsOrdered(data, a, b) + limit-- + } + + pivot, hint := choosePivotOrdered(data, a, b) + if hint == decreasingHint { + reverseRangeOrdered(data, a, b) + // The chosen pivot was pivot-a elements after the start of the array. + // After reversing it is pivot-a elements before the end of the array. + // The idea came from Rust's implementation. + pivot = (b - 1) - (pivot - a) + hint = increasingHint + } + + // The slice is likely already sorted. + if wasBalanced && wasPartitioned && hint == increasingHint { + if partialInsertionSortOrdered(data, a, b) { + return + } + } + + // Probably the slice contains many duplicate elements, partition the slice into + // elements equal to and elements greater than the pivot. + if a > 0 && !cmpLess(data[a-1], data[pivot]) { + mid := partitionEqualOrdered(data, a, b, pivot) + a = mid + continue + } + + mid, alreadyPartitioned := partitionOrdered(data, a, b, pivot) + wasPartitioned = alreadyPartitioned + + leftLen, rightLen := mid-a, b-mid + balanceThreshold := length / 8 + if leftLen < rightLen { + wasBalanced = leftLen >= balanceThreshold + pdqsortOrdered(data, a, mid, limit) + a = mid + 1 + } else { + wasBalanced = rightLen >= balanceThreshold + pdqsortOrdered(data, mid+1, b, limit) + b = mid + } + } +} + +// partitionOrdered does one quicksort partition. +// Let p = data[pivot] +// Moves elements in data[a:b] around, so that data[i]

=p for inewpivot. +// On return, data[newpivot] = p +func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivot int, alreadyPartitioned bool) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for i <= j && cmpLess(data[i], data[a]) { + i++ + } + for i <= j && !cmpLess(data[j], data[a]) { + j-- + } + if i > j { + data[j], data[a] = data[a], data[j] + return j, true + } + data[i], data[j] = data[j], data[i] + i++ + j-- + + for { + for i <= j && cmpLess(data[i], data[a]) { + i++ + } + for i <= j && !cmpLess(data[j], data[a]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + data[j], data[a] = data[a], data[j] + return j, false +} + +// partitionEqualOrdered partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot]. +// It assumed that data[a:b] does not contain elements smaller than the data[pivot]. +func partitionEqualOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivot int) { + data[a], data[pivot] = data[pivot], data[a] + i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned + + for { + for i <= j && !cmpLess(data[a], data[i]) { + i++ + } + for i <= j && cmpLess(data[a], data[j]) { + j-- + } + if i > j { + break + } + data[i], data[j] = data[j], data[i] + i++ + j-- + } + return i +} + +// partialInsertionSortOrdered partially sorts a slice, returns true if the slice is sorted at the end. +func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool { + const ( + maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted + shortestShifting = 50 // don't shift any elements on short arrays + ) + i := a + 1 + for j := 0; j < maxSteps; j++ { + for i < b && !cmpLess(data[i], data[i-1]) { + i++ + } + + if i == b { + return true + } + + if b-a < shortestShifting { + return false + } + + data[i], data[i-1] = data[i-1], data[i] + + // Shift the smaller one to the left. + if i-a >= 2 { + for j := i - 1; j >= 1; j-- { + if !cmpLess(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + // Shift the greater one to the right. + if b-i >= 2 { + for j := i + 1; j < b; j++ { + if !cmpLess(data[j], data[j-1]) { + break + } + data[j], data[j-1] = data[j-1], data[j] + } + } + } + return false +} + +// breakPatternsOrdered scatters some elements around in an attempt to break some patterns +// that might cause imbalanced partitions in quicksort. +func breakPatternsOrdered[E constraints.Ordered](data []E, a, b int) { + length := b - a + if length >= 8 { + random := xorshift(length) + modulus := nextPowerOfTwo(length) + + for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ { + other := int(uint(random.Next()) & (modulus - 1)) + if other >= length { + other -= length + } + data[idx], data[a+other] = data[a+other], data[idx] + } + } +} + +// choosePivotOrdered chooses a pivot in data[a:b]. +// +// [0,8): chooses a static pivot. +// [8,shortestNinther): uses the simple median-of-three method. +// [shortestNinther,∞): uses the Tukey ninther method. +func choosePivotOrdered[E constraints.Ordered](data []E, a, b int) (pivot int, hint sortedHint) { + const ( + shortestNinther = 50 + maxSwaps = 4 * 3 + ) + + l := b - a + + var ( + swaps int + i = a + l/4*1 + j = a + l/4*2 + k = a + l/4*3 + ) + + if l >= 8 { + if l >= shortestNinther { + // Tukey ninther method, the idea came from Rust's implementation. + i = medianAdjacentOrdered(data, i, &swaps) + j = medianAdjacentOrdered(data, j, &swaps) + k = medianAdjacentOrdered(data, k, &swaps) + } + // Find the median among i, j, k and stores it into j. + j = medianOrdered(data, i, j, k, &swaps) + } + + switch swaps { + case 0: + return j, increasingHint + case maxSwaps: + return j, decreasingHint + default: + return j, unknownHint + } +} + +// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a. +func order2Ordered[E constraints.Ordered](data []E, a, b int, swaps *int) (int, int) { + if cmpLess(data[b], data[a]) { + *swaps++ + return b, a + } + return a, b +} + +// medianOrdered returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c. +func medianOrdered[E constraints.Ordered](data []E, a, b, c int, swaps *int) int { + a, b = order2Ordered(data, a, b, swaps) + b, c = order2Ordered(data, b, c, swaps) + a, b = order2Ordered(data, a, b, swaps) + return b +} + +// medianAdjacentOrdered finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a. +func medianAdjacentOrdered[E constraints.Ordered](data []E, a int, swaps *int) int { + return medianOrdered(data, a-1, a, a+1, swaps) +} + +func reverseRangeOrdered[E constraints.Ordered](data []E, a, b int) { + i := a + j := b - 1 + for i < j { + data[i], data[j] = data[j], data[i] + i++ + j-- + } +} + +func swapRangeOrdered[E constraints.Ordered](data []E, a, b, n int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +func stableOrdered[E constraints.Ordered](data []E, n int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortOrdered(data, a, b) + a = b + b += blockSize + } + insertionSortOrdered(data, a, n) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeOrdered(data, a, a+blockSize, b) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeOrdered(data, a, m, n) + } + blockSize *= 2 + } +} + +// symMergeOrdered merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmpLess(data[h], data[a]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !cmpLess(data[m], data[h]) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !cmpLess(data[p-c], data[c]) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateOrdered(data, start, m, end) + } + if a < start && start < mid { + symMergeOrdered(data, a, start, mid) + } + if mid < end && end < b { + symMergeOrdered(data, mid, end, b) + } +} + +// rotateOrdered rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateOrdered[E constraints.Ordered](data []E, a, m, b int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeOrdered(data, m-i, m, j) + i -= j + } else { + swapRangeOrdered(data, m-i, m+j-i, i) + j -= i + } + } + // i == j + swapRangeOrdered(data, m-i, m, i) +} diff --git a/vendor/golang.org/x/tools/go/ast/inspector/inspector.go b/vendor/golang.org/x/tools/go/ast/inspector/inspector.go index 3fbfebf36..1fc1de0bd 100644 --- a/vendor/golang.org/x/tools/go/ast/inspector/inspector.go +++ b/vendor/golang.org/x/tools/go/ast/inspector/inspector.go @@ -64,8 +64,9 @@ type event struct { // depth-first order. It calls f(n) for each node n before it visits // n's children. // +// The complete traversal sequence is determined by ast.Inspect. // The types argument, if non-empty, enables type-based filtering of -// events. The function f if is called only for nodes whose type +// events. The function f is called only for nodes whose type // matches an element of the types slice. func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { // Because it avoids postorder calls to f, and the pruning @@ -97,6 +98,7 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { // of the non-nil children of the node, followed by a call of // f(n, false). // +// The complete traversal sequence is determined by ast.Inspect. // The types argument, if non-empty, enables type-based filtering of // events. The function f if is called only for nodes whose type // matches an element of the types slice. diff --git a/vendor/golang.org/x/tools/internal/typeparams/common.go b/vendor/golang.org/x/tools/internal/typeparams/common.go index 25a1426d3..d0d0649fe 100644 --- a/vendor/golang.org/x/tools/internal/typeparams/common.go +++ b/vendor/golang.org/x/tools/internal/typeparams/common.go @@ -23,6 +23,7 @@ package typeparams import ( + "fmt" "go/ast" "go/token" "go/types" @@ -87,7 +88,6 @@ func IsTypeParam(t types.Type) bool { func OriginMethod(fn *types.Func) *types.Func { recv := fn.Type().(*types.Signature).Recv() if recv == nil { - return fn } base := recv.Type() @@ -106,6 +106,31 @@ func OriginMethod(fn *types.Func) *types.Func { } orig := NamedTypeOrigin(named) gfn, _, _ := types.LookupFieldOrMethod(orig, true, fn.Pkg(), fn.Name()) + + // This is a fix for a gopls crash (#60628) due to a go/types bug (#60634). In: + // package p + // type T *int + // func (*T) f() {} + // LookupFieldOrMethod(T, true, p, f)=nil, but NewMethodSet(*T)={(*T).f}. + // Here we make them consistent by force. + // (The go/types bug is general, but this workaround is reached only + // for generic T thanks to the early return above.) + if gfn == nil { + mset := types.NewMethodSet(types.NewPointer(orig)) + for i := 0; i < mset.Len(); i++ { + m := mset.At(i) + if m.Obj().Id() == fn.Id() { + gfn = m.Obj() + break + } + } + } + + // In golang/go#61196, we observe another crash, this time inexplicable. + if gfn == nil { + panic(fmt.Sprintf("missing origin method for %s.%s; named == origin: %t, named.NumMethods(): %d, origin.NumMethods(): %d", named, fn, named == orig, named.NumMethods(), orig.NumMethods())) + } + return gfn.(*types.Func) } diff --git a/vendor/golang.org/x/tools/internal/typeparams/typeparams_go117.go b/vendor/golang.org/x/tools/internal/typeparams/typeparams_go117.go index b4788978f..7ed86e171 100644 --- a/vendor/golang.org/x/tools/internal/typeparams/typeparams_go117.go +++ b/vendor/golang.org/x/tools/internal/typeparams/typeparams_go117.go @@ -129,7 +129,7 @@ func NamedTypeArgs(*types.Named) *TypeList { } // NamedTypeOrigin is the identity method at this Go version. -func NamedTypeOrigin(named *types.Named) types.Type { +func NamedTypeOrigin(named *types.Named) *types.Named { return named } diff --git a/vendor/golang.org/x/tools/internal/typeparams/typeparams_go118.go b/vendor/golang.org/x/tools/internal/typeparams/typeparams_go118.go index 114a36b86..cf301af1d 100644 --- a/vendor/golang.org/x/tools/internal/typeparams/typeparams_go118.go +++ b/vendor/golang.org/x/tools/internal/typeparams/typeparams_go118.go @@ -103,7 +103,7 @@ func NamedTypeArgs(named *types.Named) *TypeList { } // NamedTypeOrigin returns named.Orig(). -func NamedTypeOrigin(named *types.Named) types.Type { +func NamedTypeOrigin(named *types.Named) *types.Named { return named.Origin() } diff --git a/vendor/k8s.io/kubernetes/test/e2e/storage/testsuites/provisioning.go b/vendor/k8s.io/kubernetes/test/e2e/storage/testsuites/provisioning.go index 0b1c434ea..326df17c0 100644 --- a/vendor/k8s.io/kubernetes/test/e2e/storage/testsuites/provisioning.go +++ b/vendor/k8s.io/kubernetes/test/e2e/storage/testsuites/provisioning.go @@ -267,9 +267,9 @@ func (p *provisioningTestSuite) DefineTests(driver storageframework.TestDriver, ginkgo.By("Deploying validator") valManifests := []string{ - "test/e2e/testing-manifests/storage-csi/any-volume-datasource/crd/populator.storage.k8s.io_volumepopulators.yaml", - "test/e2e/testing-manifests/storage-csi/any-volume-datasource/volume-data-source-validator/rbac-data-source-validator.yaml", - "test/e2e/testing-manifests/storage-csi/any-volume-datasource/volume-data-source-validator/setup-data-source-validator.yaml", + "vendor/k8s.io/kubernetes/test/e2e/testing-manifests/storage-csi/any-volume-datasource/crd/populator.storage.k8s.io_volumepopulators.yaml", + "vendor/k8s.io/kubernetes/test/e2e/testing-manifests/storage-csi/any-volume-datasource/volume-data-source-validator/rbac-data-source-validator.yaml", + "vendor/k8s.io/kubernetes/test/e2e/testing-manifests/storage-csi/any-volume-datasource/volume-data-source-validator/setup-data-source-validator.yaml", } valCleanup, err := storageutils.CreateFromManifests(f, valNamespace, func(item interface{}) error { return nil }, @@ -291,8 +291,8 @@ func (p *provisioningTestSuite) DefineTests(driver storageframework.TestDriver, ginkgo.By("Deploying hello-populator") popManifests := []string{ - "test/e2e/testing-manifests/storage-csi/any-volume-datasource/crd/hello-populator-crd.yaml", - "test/e2e/testing-manifests/storage-csi/any-volume-datasource/hello-populator-deploy.yaml", + "vendor/k8s.io/kubernetes/test/e2e/testing-manifests/storage-csi/any-volume-datasource/crd/hello-populator-crd.yaml", + "vendor/k8s.io/kubernetes/test/e2e/testing-manifests/storage-csi/any-volume-datasource/hello-populator-deploy.yaml", } popCleanup, err := storageutils.CreateFromManifests(f, popNamespace, func(item interface{}) error { diff --git a/vendor/modules.txt b/vendor/modules.txt index fe08aa0a2..70edf497d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -338,6 +338,10 @@ golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf +# golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 +## explicit; go 1.20 +golang.org/x/exp/constraints +golang.org/x/exp/slices # golang.org/x/net v0.14.0 ## explicit; go 1.17 golang.org/x/net/context @@ -393,7 +397,7 @@ golang.org/x/text/width # golang.org/x/time v0.0.0-20220609170525-579cf78fd858 ## explicit golang.org/x/time/rate -# golang.org/x/tools v0.6.0 +# golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 ## explicit; go 1.18 golang.org/x/tools/go/ast/inspector golang.org/x/tools/internal/typeparams