├── .github └── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md ├── LICENSE ├── OWNERS ├── README.md ├── SECURITY_CONTACTS ├── code-of-conduct.md ├── go.mod ├── go.sum ├── plugins ├── aws_ebs.go ├── aws_ebs_test.go ├── azure_disk.go ├── azure_disk_test.go ├── azure_file.go ├── azure_file_test.go ├── const.go ├── gce_pd.go ├── gce_pd_test.go ├── in_tree_volume.go ├── in_tree_volume_test.go ├── openstack_cinder.go ├── openstack_cinder_test.go ├── portworx.go ├── portworx_test.go ├── vsphere_volume.go └── vsphere_volume_test.go ├── translate.go └── translate_test.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Sorry, we do not accept changes directly against this repository. Please see 2 | CONTRIBUTING.md for information on where and how to contribute instead. 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes. 4 | 5 | This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/csi-translation-lib](https://git.k8s.io/kubernetes/staging/src/k8s.io/csi-translation-lib) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot). 6 | 7 | Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | reviewers: 4 | - sig-storage-reviewers 5 | - andyzhangx 6 | approvers: 7 | - sig-storage-approvers 8 | labels: 9 | - sig/storage 10 | emeritus_approvers: 11 | - davidz627 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | This repository contains functions to be consumed by various Kubernetes and 4 | out-of-tree CSI components like external provisioner to facilitate migration of 5 | code from Kubernetes In-tree plugin code to CSI plugin repositories. 6 | 7 | Consumers of this repository can make use of functions like `TranslateToCSI` and 8 | `TranslateToInTree` functions to translate PV sources. 9 | 10 | ## Community, discussion, contribution, and support 11 | 12 | Learn how to engage with the Kubernetes community on the [community 13 | page](http://kubernetes.io/community/). 14 | 15 | You can reach the maintainers of this repository at: 16 | 17 | - Slack: #sig-storage (on https://kubernetes.slack.com -- get an 18 | invite at slack.kubernetes.io) 19 | - Mailing List: 20 | https://groups.google.com/forum/#!forum/kubernetes-sig-storage 21 | 22 | ### Code of Conduct 23 | 24 | Participation in the Kubernetes community is governed by the [Kubernetes 25 | Code of Conduct](code-of-conduct.md). 26 | 27 | ### Contibution Guidelines 28 | 29 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. 30 | 31 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | saad-ali 14 | cjcullen 15 | joelsmith 16 | liggitt 17 | philips 18 | tallclair 19 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // This is a generated file. Do not edit directly. 2 | 3 | module k8s.io/csi-translation-lib 4 | 5 | go 1.24.0 6 | 7 | godebug default=go1.24 8 | 9 | require ( 10 | github.com/stretchr/testify v1.10.0 11 | k8s.io/api v0.0.0-20250703010437-9ca4bf8538e0 12 | k8s.io/apimachinery v0.0.0-20250703010150-b86b632271cf 13 | k8s.io/klog/v2 v2.130.1 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/gogo/protobuf v1.3.2 // indirect 21 | github.com/google/uuid v1.6.0 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/kr/text v0.2.0 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/x448/float16 v0.8.4 // indirect 28 | go.yaml.in/yaml/v2 v2.4.2 // indirect 29 | golang.org/x/net v0.38.0 // indirect 30 | golang.org/x/text v0.23.0 // indirect 31 | gopkg.in/inf.v0 v0.9.1 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 34 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 35 | sigs.k8s.io/randfill v1.0.0 // indirect 36 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 37 | sigs.k8s.io/yaml v1.5.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 6 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 7 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 8 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 10 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 11 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 13 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 14 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 18 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 19 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 20 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 25 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 28 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 29 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 30 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 34 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 35 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 36 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 39 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 40 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 41 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 42 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 43 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 44 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 45 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 46 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 49 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 50 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 51 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 52 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 53 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 54 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 55 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 56 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 57 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 58 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 65 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 66 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 67 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 70 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 71 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 78 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 79 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 80 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 81 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 82 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | k8s.io/api v0.0.0-20250703010437-9ca4bf8538e0 h1:iS/S3wfNTxgeC+HNybNhVSLt7y9E1XDkUlzPRXd1c6U= 84 | k8s.io/api v0.0.0-20250703010437-9ca4bf8538e0/go.mod h1:2FUvtol5X8X7D4iFOQdd1W2Q6BoMhvE/DSSRzyWQ2yU= 85 | k8s.io/apimachinery v0.0.0-20250703010150-b86b632271cf h1:5z7lkImscG/qu7KON0TOD0aSsycwXXiWue9mrjDasu4= 86 | k8s.io/apimachinery v0.0.0-20250703010150-b86b632271cf/go.mod h1:Th679JJyaVRDNFk3vKPKY43ypziDeoGnbEiEgBCz8s4= 87 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 88 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 89 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= 90 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 91 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 92 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 93 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 94 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 95 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 96 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 97 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 98 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 99 | sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= 100 | sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= 101 | -------------------------------------------------------------------------------- /plugins/aws_ebs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "regexp" 23 | "strconv" 24 | "strings" 25 | 26 | v1 "k8s.io/api/core/v1" 27 | storage "k8s.io/api/storage/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/util/sets" 30 | "k8s.io/klog/v2" 31 | ) 32 | 33 | const ( 34 | // AWSEBSDriverName is the name of the CSI driver for EBS 35 | AWSEBSDriverName = "ebs.csi.aws.com" 36 | // AWSEBSInTreePluginName is the name of the intree plugin for EBS 37 | AWSEBSInTreePluginName = "kubernetes.io/aws-ebs" 38 | // AWSEBSTopologyKey is the zonal topology key for AWS EBS CSI driver 39 | AWSEBSTopologyKey = "topology." + AWSEBSDriverName + "/zone" 40 | // iopsPerGBKey is StorageClass parameter name that specifies IOPS 41 | // Per GB. 42 | iopsPerGBKey = "iopspergb" 43 | // allowIncreaseIOPSKey is parameter name that allows the CSI driver 44 | // to increase IOPS to the minimum value supported by AWS when IOPS 45 | // Per GB is too low for a given volume size. This preserves current 46 | // in-tree volume plugin behavior. 47 | allowIncreaseIOPSKey = "allowautoiopspergbincrease" 48 | ) 49 | 50 | var _ InTreePlugin = &awsElasticBlockStoreCSITranslator{} 51 | 52 | // awsElasticBlockStoreTranslator handles translation of PV spec from In-tree EBS to CSI EBS and vice versa 53 | type awsElasticBlockStoreCSITranslator struct{} 54 | 55 | // NewAWSElasticBlockStoreCSITranslator returns a new instance of awsElasticBlockStoreTranslator 56 | func NewAWSElasticBlockStoreCSITranslator() InTreePlugin { 57 | return &awsElasticBlockStoreCSITranslator{} 58 | } 59 | 60 | // TranslateInTreeStorageClassToCSI translates InTree EBS storage class parameters to CSI storage class 61 | func (t *awsElasticBlockStoreCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 62 | var ( 63 | generatedTopologies []v1.TopologySelectorTerm 64 | params = map[string]string{} 65 | ) 66 | for k, v := range sc.Parameters { 67 | switch strings.ToLower(k) { 68 | case fsTypeKey: 69 | params[csiFsTypeKey] = v 70 | case zoneKey: 71 | generatedTopologies = generateToplogySelectors(AWSEBSTopologyKey, []string{v}) 72 | case zonesKey: 73 | generatedTopologies = generateToplogySelectors(AWSEBSTopologyKey, strings.Split(v, ",")) 74 | case iopsPerGBKey: 75 | // Keep iopsPerGBKey 76 | params[k] = v 77 | // Preserve current in-tree volume plugin behavior and allow the CSI 78 | // driver to bump volume IOPS when volume size * iopsPerGB is too low. 79 | params[allowIncreaseIOPSKey] = "true" 80 | default: 81 | params[k] = v 82 | } 83 | } 84 | 85 | if len(generatedTopologies) > 0 && len(sc.AllowedTopologies) > 0 { 86 | return nil, fmt.Errorf("cannot simultaneously set allowed topologies and zone/zones parameters") 87 | } else if len(generatedTopologies) > 0 { 88 | sc.AllowedTopologies = generatedTopologies 89 | } else if len(sc.AllowedTopologies) > 0 { 90 | newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, AWSEBSTopologyKey) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed translating allowed topologies: %v", err) 93 | } 94 | sc.AllowedTopologies = newTopologies 95 | } 96 | 97 | sc.Parameters = params 98 | 99 | return sc, nil 100 | } 101 | 102 | // TranslateInTreeInlineVolumeToCSI takes a Volume with AWSElasticBlockStore set from in-tree 103 | // and converts the AWSElasticBlockStore source to a CSIPersistentVolumeSource 104 | func (t *awsElasticBlockStoreCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 105 | if volume == nil || volume.AWSElasticBlockStore == nil { 106 | return nil, fmt.Errorf("volume is nil or AWS EBS not defined on volume") 107 | } 108 | ebsSource := volume.AWSElasticBlockStore 109 | volumeHandle, err := KubernetesVolumeIDToEBSVolumeID(ebsSource.VolumeID) 110 | if err != nil { 111 | return nil, fmt.Errorf("failed to translate Kubernetes ID to EBS Volume ID %v", err) 112 | } 113 | pv := &v1.PersistentVolume{ 114 | ObjectMeta: metav1.ObjectMeta{ 115 | // Must be unique per disk as it is used as the unique part of the 116 | // staging path 117 | Name: fmt.Sprintf("%s-%s", AWSEBSDriverName, volumeHandle), 118 | }, 119 | Spec: v1.PersistentVolumeSpec{ 120 | PersistentVolumeSource: v1.PersistentVolumeSource{ 121 | CSI: &v1.CSIPersistentVolumeSource{ 122 | Driver: AWSEBSDriverName, 123 | VolumeHandle: volumeHandle, 124 | ReadOnly: ebsSource.ReadOnly, 125 | FSType: ebsSource.FSType, 126 | VolumeAttributes: map[string]string{ 127 | "partition": strconv.FormatInt(int64(ebsSource.Partition), 10), 128 | }, 129 | }, 130 | }, 131 | AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 132 | }, 133 | } 134 | return pv, nil 135 | } 136 | 137 | // TranslateInTreePVToCSI takes a PV with AWSElasticBlockStore set from in-tree 138 | // and converts the AWSElasticBlockStore source to a CSIPersistentVolumeSource 139 | func (t *awsElasticBlockStoreCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 140 | if pv == nil || pv.Spec.AWSElasticBlockStore == nil { 141 | return nil, fmt.Errorf("pv is nil or AWS EBS not defined on pv") 142 | } 143 | 144 | ebsSource := pv.Spec.AWSElasticBlockStore 145 | 146 | volumeHandle, err := KubernetesVolumeIDToEBSVolumeID(ebsSource.VolumeID) 147 | if err != nil { 148 | return nil, fmt.Errorf("failed to translate Kubernetes ID to EBS Volume ID %v", err) 149 | } 150 | 151 | csiSource := &v1.CSIPersistentVolumeSource{ 152 | Driver: AWSEBSDriverName, 153 | VolumeHandle: volumeHandle, 154 | ReadOnly: ebsSource.ReadOnly, 155 | FSType: ebsSource.FSType, 156 | VolumeAttributes: map[string]string{ 157 | "partition": strconv.FormatInt(int64(ebsSource.Partition), 10), 158 | }, 159 | } 160 | 161 | if err := translateTopologyFromInTreeToCSI(pv, AWSEBSTopologyKey); err != nil { 162 | return nil, fmt.Errorf("failed to translate topology: %v", err) 163 | } 164 | 165 | pv.Spec.AWSElasticBlockStore = nil 166 | pv.Spec.CSI = csiSource 167 | return pv, nil 168 | } 169 | 170 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 171 | // translates the EBS CSI source to a AWSElasticBlockStore source. 172 | func (t *awsElasticBlockStoreCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 173 | if pv == nil || pv.Spec.CSI == nil { 174 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 175 | } 176 | 177 | csiSource := pv.Spec.CSI 178 | 179 | ebsSource := &v1.AWSElasticBlockStoreVolumeSource{ 180 | VolumeID: csiSource.VolumeHandle, 181 | FSType: csiSource.FSType, 182 | ReadOnly: csiSource.ReadOnly, 183 | } 184 | 185 | if partition, ok := csiSource.VolumeAttributes["partition"]; ok { 186 | partValue, err := strconv.Atoi(partition) 187 | if err != nil { 188 | return nil, fmt.Errorf("failed to convert partition %v to integer: %v", partition, err) 189 | } 190 | ebsSource.Partition = int32(partValue) 191 | } 192 | 193 | // translate CSI topology to In-tree topology for rollback compatibility 194 | if err := translateTopologyFromCSIToInTree(pv, AWSEBSTopologyKey, getAwsRegionFromZones); err != nil { 195 | return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err) 196 | } 197 | 198 | pv.Spec.CSI = nil 199 | pv.Spec.AWSElasticBlockStore = ebsSource 200 | return pv, nil 201 | } 202 | 203 | // CanSupport tests whether the plugin supports a given persistent volume 204 | // specification from the API. The spec pointer should be considered 205 | // const. 206 | func (t *awsElasticBlockStoreCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 207 | return pv != nil && pv.Spec.AWSElasticBlockStore != nil 208 | } 209 | 210 | // CanSupportInline tests whether the plugin supports a given inline volume 211 | // specification from the API. The spec pointer should be considered 212 | // const. 213 | func (t *awsElasticBlockStoreCSITranslator) CanSupportInline(volume *v1.Volume) bool { 214 | return volume != nil && volume.AWSElasticBlockStore != nil 215 | } 216 | 217 | // GetInTreePluginName returns the name of the intree plugin driver 218 | func (t *awsElasticBlockStoreCSITranslator) GetInTreePluginName() string { 219 | return AWSEBSInTreePluginName 220 | } 221 | 222 | // GetCSIPluginName returns the name of the CSI plugin 223 | func (t *awsElasticBlockStoreCSITranslator) GetCSIPluginName() string { 224 | return AWSEBSDriverName 225 | } 226 | 227 | func (t *awsElasticBlockStoreCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 228 | return volumeHandle, nil 229 | } 230 | 231 | // awsVolumeRegMatch represents Regex Match for AWS volume. 232 | var awsVolumeRegMatch = regexp.MustCompile("^vol-[^/]*$") 233 | 234 | // KubernetesVolumeIDToEBSVolumeID translates Kubernetes volume ID to EBS volume ID 235 | // KubernetesVolumeID forms: 236 | // - aws:/// 237 | // - aws:/// 238 | // - 239 | // 240 | // EBS Volume ID form: 241 | // - vol- 242 | // 243 | // This translation shouldn't be needed and should be fixed in long run 244 | // See https://github.com/kubernetes/kubernetes/issues/73730 245 | func KubernetesVolumeIDToEBSVolumeID(kubernetesID string) (string, error) { 246 | // name looks like aws://availability-zone/awsVolumeId 247 | 248 | // The original idea of the URL-style name was to put the AZ into the 249 | // host, so we could find the AZ immediately from the name without 250 | // querying the API. But it turns out we don't actually need it for 251 | // multi-AZ clusters, as we put the AZ into the labels on the PV instead. 252 | // However, if in future we want to support multi-AZ cluster 253 | // volume-awareness without using PersistentVolumes, we likely will 254 | // want the AZ in the host. 255 | if !strings.HasPrefix(kubernetesID, "aws://") { 256 | // Assume a bare aws volume id (vol-1234...) 257 | return kubernetesID, nil 258 | } 259 | url, err := url.Parse(kubernetesID) 260 | if err != nil { 261 | // TODO: Maybe we should pass a URL into the Volume functions 262 | return "", fmt.Errorf("Invalid disk name (%s): %v", kubernetesID, err) 263 | } 264 | if url.Scheme != "aws" { 265 | return "", fmt.Errorf("Invalid scheme for AWS volume (%s)", kubernetesID) 266 | } 267 | 268 | awsID := url.Path 269 | awsID = strings.Trim(awsID, "/") 270 | 271 | // We sanity check the resulting volume; the two known formats are 272 | // vol-12345678 and vol-12345678abcdef01 273 | if !awsVolumeRegMatch.MatchString(awsID) { 274 | return "", fmt.Errorf("Invalid format for AWS volume (%s)", kubernetesID) 275 | } 276 | 277 | return awsID, nil 278 | } 279 | 280 | func getAwsRegionFromZones(zones []string) (string, error) { 281 | regions := sets.String{} 282 | if len(zones) < 1 { 283 | return "", fmt.Errorf("no zones specified") 284 | } 285 | 286 | // AWS zones can be in four forms: 287 | // us-west-2a, us-gov-east-1a, us-west-2-lax-1a (local zone) and us-east-1-wl1-bos-wlz-1 (wavelength). 288 | for _, zone := range zones { 289 | splitZone := strings.Split(zone, "-") 290 | if (len(splitZone) == 3 || len(splitZone) == 4) && len(splitZone[len(splitZone)-1]) == 2 { 291 | // this would break if we ever have a location with more than 9 regions, ie us-west-10. 292 | splitZone[len(splitZone)-1] = splitZone[len(splitZone)-1][:1] 293 | regions.Insert(strings.Join(splitZone, "-")) 294 | } else if len(splitZone) == 5 || len(splitZone) == 7 { 295 | // local zone or wavelength 296 | regions.Insert(strings.Join(splitZone[:3], "-")) 297 | } else { 298 | return "", fmt.Errorf("Unexpected zone format: %v is not a valid AWS zone", zone) 299 | } 300 | } 301 | if regions.Len() != 1 { 302 | return "", fmt.Errorf("multiple or no regions gotten from zones, got: %v", regions) 303 | } 304 | return regions.UnsortedList()[0], nil 305 | } 306 | -------------------------------------------------------------------------------- /plugins/aws_ebs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | 25 | storage "k8s.io/api/storage/v1" 26 | 27 | "k8s.io/klog/v2/ktesting" 28 | _ "k8s.io/klog/v2/ktesting/init" 29 | ) 30 | 31 | const ( 32 | normalVolumeID = "vol-02399794d890f9375" 33 | awsVolumeID = "aws:///vol-02399794d890f9375" 34 | awsZoneVolumeID = "aws://us-west-2a/vol-02399794d890f9375" 35 | invalidVolumeID = "aws://us-west-2a/02399794d890f9375" 36 | ) 37 | 38 | func TestKubernetesVolumeIDToEBSVolumeID(t *testing.T) { 39 | testCases := []struct { 40 | name string 41 | kubernetesID string 42 | ebsVolumeID string 43 | expErr bool 44 | }{ 45 | { 46 | name: "Normal ID format", 47 | kubernetesID: normalVolumeID, 48 | ebsVolumeID: normalVolumeID, 49 | }, 50 | { 51 | name: "aws:///{volumeId} format", 52 | kubernetesID: awsVolumeID, 53 | ebsVolumeID: normalVolumeID, 54 | }, 55 | { 56 | name: "aws://{zone}/{volumeId} format", 57 | kubernetesID: awsZoneVolumeID, 58 | ebsVolumeID: normalVolumeID, 59 | }, 60 | { 61 | name: "fails on invalid volume ID", 62 | kubernetesID: invalidVolumeID, 63 | expErr: true, 64 | }, 65 | } 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | actual, err := KubernetesVolumeIDToEBSVolumeID(tc.kubernetesID) 70 | if err != nil { 71 | if !tc.expErr { 72 | t.Errorf("KubernetesVolumeIDToEBSVolumeID failed %v", err) 73 | } 74 | } else { 75 | if actual != tc.ebsVolumeID { 76 | t.Errorf("Wrong EBS Volume ID. actual: %s expected: %s", actual, tc.ebsVolumeID) 77 | } 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestTranslateEBSInTreeStorageClassToCSI(t *testing.T) { 84 | translator := NewAWSElasticBlockStoreCSITranslator() 85 | logger, _ := ktesting.NewTestContext(t) 86 | 87 | cases := []struct { 88 | name string 89 | sc *storage.StorageClass 90 | expSc *storage.StorageClass 91 | expErr bool 92 | }{ 93 | { 94 | name: "translate normal", 95 | sc: NewStorageClass(map[string]string{"foo": "bar"}, nil), 96 | expSc: NewStorageClass(map[string]string{"foo": "bar"}, nil), 97 | }, 98 | { 99 | name: "translate empty map", 100 | sc: NewStorageClass(map[string]string{}, nil), 101 | expSc: NewStorageClass(map[string]string{}, nil), 102 | }, 103 | 104 | { 105 | name: "translate with fstype", 106 | sc: NewStorageClass(map[string]string{"fstype": "ext3"}, nil), 107 | expSc: NewStorageClass(map[string]string{"csi.storage.k8s.io/fstype": "ext3"}, nil), 108 | }, 109 | { 110 | name: "translate with iops", 111 | sc: NewStorageClass(map[string]string{"iopsPerGB": "100"}, nil), 112 | expSc: NewStorageClass(map[string]string{"iopsPerGB": "100", "allowautoiopspergbincrease": "true"}, nil), 113 | }, 114 | } 115 | 116 | for _, tc := range cases { 117 | t.Logf("Testing %v", tc.name) 118 | got, err := translator.TranslateInTreeStorageClassToCSI(logger, tc.sc) 119 | if err != nil && !tc.expErr { 120 | t.Errorf("Did not expect error but got: %v", err) 121 | } 122 | 123 | if err == nil && tc.expErr { 124 | t.Errorf("Expected error, but did not get one.") 125 | } 126 | 127 | if !reflect.DeepEqual(got, tc.expSc) { 128 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expSc) 129 | } 130 | 131 | } 132 | } 133 | 134 | func TestTranslateInTreeInlineVolumeToCSI(t *testing.T) { 135 | translator := NewAWSElasticBlockStoreCSITranslator() 136 | logger, _ := ktesting.NewTestContext(t) 137 | 138 | cases := []struct { 139 | name string 140 | volumeSource v1.VolumeSource 141 | expPVName string 142 | expErr bool 143 | }{ 144 | { 145 | name: "Normal ID format", 146 | volumeSource: v1.VolumeSource{ 147 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 148 | VolumeID: normalVolumeID, 149 | }, 150 | }, 151 | expPVName: "ebs.csi.aws.com-" + normalVolumeID, 152 | }, 153 | { 154 | name: "aws:///{volumeId} format", 155 | volumeSource: v1.VolumeSource{ 156 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 157 | VolumeID: awsVolumeID, 158 | }, 159 | }, 160 | expPVName: "ebs.csi.aws.com-" + normalVolumeID, 161 | }, 162 | { 163 | name: "aws://{zone}/{volumeId} format", 164 | volumeSource: v1.VolumeSource{ 165 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 166 | VolumeID: awsZoneVolumeID, 167 | }, 168 | }, 169 | expPVName: "ebs.csi.aws.com-" + normalVolumeID, 170 | }, 171 | { 172 | name: "fails on invalid volume ID", 173 | volumeSource: v1.VolumeSource{ 174 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 175 | VolumeID: invalidVolumeID, 176 | }, 177 | }, 178 | expErr: true, 179 | }, 180 | { 181 | name: "fails on empty volume source", 182 | volumeSource: v1.VolumeSource{}, 183 | expErr: true, 184 | }, 185 | } 186 | 187 | for _, tc := range cases { 188 | t.Run(tc.name, func(t *testing.T) { 189 | t.Logf("Testing %v", tc.name) 190 | got, err := translator.TranslateInTreeInlineVolumeToCSI(logger, &v1.Volume{Name: "volume", VolumeSource: tc.volumeSource}, "") 191 | if err != nil && !tc.expErr { 192 | t.Fatalf("Did not expect error but got: %v", err) 193 | } 194 | 195 | if err == nil && tc.expErr { 196 | t.Fatalf("Expected error, but did not get one.") 197 | } 198 | 199 | if err == nil { 200 | if !reflect.DeepEqual(got.Name, tc.expPVName) { 201 | t.Errorf("Got PV name: %v, expected :%v", got.Name, tc.expPVName) 202 | } 203 | 204 | if !reflect.DeepEqual(got.Spec.CSI.VolumeHandle, normalVolumeID) { 205 | t.Errorf("Got PV volumeHandle: %v, expected :%v", got.Spec.CSI.VolumeHandle, normalVolumeID) 206 | } 207 | } 208 | 209 | }) 210 | } 211 | } 212 | 213 | func TestGetAwsRegionFromZones(t *testing.T) { 214 | 215 | cases := []struct { 216 | name string 217 | zones []string 218 | expRegion string 219 | expErr bool 220 | }{ 221 | { 222 | name: "Commercial zone", 223 | zones: []string{"us-west-2a", "us-west-2b"}, 224 | expRegion: "us-west-2", 225 | }, 226 | { 227 | name: "Govcloud zone", 228 | zones: []string{"us-gov-east-1a"}, 229 | expRegion: "us-gov-east-1", 230 | }, 231 | { 232 | name: "Wavelength zone", 233 | zones: []string{"us-east-1-wl1-bos-wlz-1"}, 234 | expRegion: "us-east-1", 235 | }, 236 | { 237 | name: "Local zone", 238 | zones: []string{"us-west-2-lax-1a"}, 239 | expRegion: "us-west-2", 240 | }, 241 | { 242 | name: "Invalid: empty zones", 243 | zones: []string{}, 244 | expErr: true, 245 | }, 246 | { 247 | name: "Invalid: multiple regions", 248 | zones: []string{"us-west-2a", "us-east-1a"}, 249 | expErr: true, 250 | }, 251 | { 252 | name: "Invalid: region name only", 253 | zones: []string{"us-west-2"}, 254 | expErr: true, 255 | }, 256 | { 257 | name: "Invalid: invalid suffix", 258 | zones: []string{"us-west-2ab"}, 259 | expErr: true, 260 | }, 261 | { 262 | name: "Invalid: not enough fields", 263 | zones: []string{"us-west"}, 264 | expErr: true, 265 | }, 266 | } 267 | 268 | for _, tc := range cases { 269 | t.Run(tc.name, func(t *testing.T) { 270 | t.Logf("Testing %v", tc.name) 271 | got, err := getAwsRegionFromZones(tc.zones) 272 | if err != nil && !tc.expErr { 273 | t.Fatalf("Did not expect error but got: %v", err) 274 | } 275 | 276 | if err == nil && tc.expErr { 277 | t.Fatalf("Expected error, but did not get one.") 278 | } 279 | 280 | if err == nil && !reflect.DeepEqual(got, tc.expRegion) { 281 | t.Errorf("Got PV name: %v, expected :%v", got, tc.expRegion) 282 | } 283 | }) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /plugins/azure_disk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "regexp" 22 | "strings" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | storage "k8s.io/api/storage/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/klog/v2" 28 | ) 29 | 30 | const ( 31 | // AzureDiskDriverName is the name of the CSI driver for Azure Disk 32 | AzureDiskDriverName = "disk.csi.azure.com" 33 | // AzureDiskTopologyKey is the topology key of Azure Disk CSI driver 34 | AzureDiskTopologyKey = "topology.disk.csi.azure.com/zone" 35 | // AzureDiskInTreePluginName is the name of the intree plugin for Azure Disk 36 | AzureDiskInTreePluginName = "kubernetes.io/azure-disk" 37 | 38 | // Parameter names defined in azure disk CSI driver, refer to 39 | // https://github.com/kubernetes-sigs/azuredisk-csi-driver/blob/master/docs/driver-parameters.md 40 | azureDiskKind = "kind" 41 | azureDiskCachingMode = "cachingmode" 42 | azureDiskFSType = "fstype" 43 | ) 44 | 45 | var ( 46 | managedDiskPathRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/disks/(.+)`) 47 | unmanagedDiskPathRE = regexp.MustCompile(`http(?:.*)://(?:.*)/vhds/(.+)`) 48 | managed = string(v1.AzureManagedDisk) 49 | unzonedCSIRegionRE = regexp.MustCompile(`^[0-9]+$`) 50 | ) 51 | 52 | var _ InTreePlugin = &azureDiskCSITranslator{} 53 | 54 | // azureDiskCSITranslator handles translation of PV spec from In-tree 55 | // Azure Disk to CSI Azure Disk and vice versa 56 | type azureDiskCSITranslator struct{} 57 | 58 | // NewAzureDiskCSITranslator returns a new instance of azureDiskTranslator 59 | func NewAzureDiskCSITranslator() InTreePlugin { 60 | return &azureDiskCSITranslator{} 61 | } 62 | 63 | // TranslateInTreeStorageClassToCSI translates InTree Azure Disk storage class parameters to CSI storage class 64 | func (t *azureDiskCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 65 | var ( 66 | generatedTopologies []v1.TopologySelectorTerm 67 | params = map[string]string{} 68 | ) 69 | for k, v := range sc.Parameters { 70 | switch strings.ToLower(k) { 71 | case zoneKey: 72 | generatedTopologies = generateToplogySelectors(AzureDiskTopologyKey, []string{v}) 73 | case zonesKey: 74 | generatedTopologies = generateToplogySelectors(AzureDiskTopologyKey, strings.Split(v, ",")) 75 | default: 76 | params[k] = v 77 | } 78 | } 79 | 80 | if len(generatedTopologies) > 0 && len(sc.AllowedTopologies) > 0 { 81 | return nil, fmt.Errorf("cannot simultaneously set allowed topologies and zone/zones parameters") 82 | } else if len(generatedTopologies) > 0 { 83 | sc.AllowedTopologies = generatedTopologies 84 | } else if len(sc.AllowedTopologies) > 0 { 85 | newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, AzureDiskTopologyKey) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed translating allowed topologies: %v", err) 88 | } 89 | sc.AllowedTopologies = newTopologies 90 | } 91 | sc.AllowedTopologies = t.replaceFailureDomainsToCSI(sc.AllowedTopologies) 92 | 93 | sc.Parameters = params 94 | 95 | return sc, nil 96 | } 97 | 98 | // TranslateInTreeInlineVolumeToCSI takes a Volume with AzureDisk set from in-tree 99 | // and converts the AzureDisk source to a CSIPersistentVolumeSource 100 | func (t *azureDiskCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 101 | if volume == nil || volume.AzureDisk == nil { 102 | return nil, fmt.Errorf("volume is nil or Azure Disk not defined on volume") 103 | } 104 | 105 | azureSource := volume.AzureDisk 106 | if azureSource.Kind != nil && !strings.EqualFold(string(*azureSource.Kind), managed) { 107 | return nil, fmt.Errorf("kind(%v) is not supported in csi migration", *azureSource.Kind) 108 | } 109 | pv := &v1.PersistentVolume{ 110 | ObjectMeta: metav1.ObjectMeta{ 111 | // Must be unique per disk as it is used as the unique part of the 112 | // staging path 113 | Name: azureSource.DataDiskURI, 114 | }, 115 | Spec: v1.PersistentVolumeSpec{ 116 | PersistentVolumeSource: v1.PersistentVolumeSource{ 117 | CSI: &v1.CSIPersistentVolumeSource{ 118 | Driver: AzureDiskDriverName, 119 | VolumeHandle: azureSource.DataDiskURI, 120 | VolumeAttributes: map[string]string{azureDiskKind: managed}, 121 | }, 122 | }, 123 | AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 124 | }, 125 | } 126 | if azureSource.ReadOnly != nil { 127 | pv.Spec.PersistentVolumeSource.CSI.ReadOnly = *azureSource.ReadOnly 128 | } 129 | 130 | if azureSource.CachingMode != nil && *azureSource.CachingMode != "" { 131 | pv.Spec.PersistentVolumeSource.CSI.VolumeAttributes[azureDiskCachingMode] = string(*azureSource.CachingMode) 132 | } 133 | if azureSource.FSType != nil { 134 | pv.Spec.PersistentVolumeSource.CSI.FSType = *azureSource.FSType 135 | pv.Spec.PersistentVolumeSource.CSI.VolumeAttributes[azureDiskFSType] = *azureSource.FSType 136 | } 137 | pv.Spec.PersistentVolumeSource.CSI.VolumeAttributes[azureDiskKind] = managed 138 | 139 | return pv, nil 140 | } 141 | 142 | // TranslateInTreePVToCSI takes a PV with AzureDisk set from in-tree 143 | // and converts the AzureDisk source to a CSIPersistentVolumeSource 144 | func (t *azureDiskCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 145 | if pv == nil || pv.Spec.AzureDisk == nil { 146 | return nil, fmt.Errorf("pv is nil or Azure Disk source not defined on pv") 147 | } 148 | 149 | var ( 150 | azureSource = pv.Spec.PersistentVolumeSource.AzureDisk 151 | 152 | // refer to https://github.com/kubernetes-sigs/azuredisk-csi-driver/blob/master/docs/driver-parameters.md 153 | csiSource = &v1.CSIPersistentVolumeSource{ 154 | Driver: AzureDiskDriverName, 155 | VolumeAttributes: map[string]string{azureDiskKind: managed}, 156 | VolumeHandle: azureSource.DataDiskURI, 157 | } 158 | ) 159 | 160 | if azureSource.Kind != nil && !strings.EqualFold(string(*azureSource.Kind), managed) { 161 | return nil, fmt.Errorf("kind(%v) is not supported in csi migration", *azureSource.Kind) 162 | } 163 | 164 | if azureSource.CachingMode != nil { 165 | csiSource.VolumeAttributes[azureDiskCachingMode] = string(*azureSource.CachingMode) 166 | } 167 | 168 | if azureSource.FSType != nil { 169 | csiSource.FSType = *azureSource.FSType 170 | csiSource.VolumeAttributes[azureDiskFSType] = *azureSource.FSType 171 | } 172 | csiSource.VolumeAttributes[azureDiskKind] = managed 173 | 174 | if azureSource.ReadOnly != nil { 175 | csiSource.ReadOnly = *azureSource.ReadOnly 176 | } 177 | 178 | pv.Spec.PersistentVolumeSource.AzureDisk = nil 179 | pv.Spec.PersistentVolumeSource.CSI = csiSource 180 | 181 | return pv, nil 182 | } 183 | 184 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 185 | // translates the Azure Disk CSI source to a AzureDisk source. 186 | func (t *azureDiskCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 187 | if pv == nil || pv.Spec.CSI == nil { 188 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 189 | } 190 | csiSource := pv.Spec.CSI 191 | 192 | diskURI := csiSource.VolumeHandle 193 | diskName, err := getDiskName(diskURI) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | // refer to https://github.com/kubernetes-sigs/azuredisk-csi-driver/blob/master/docs/driver-parameters.md 199 | managed := v1.AzureManagedDisk 200 | azureSource := &v1.AzureDiskVolumeSource{ 201 | DiskName: diskName, 202 | DataDiskURI: diskURI, 203 | FSType: &csiSource.FSType, 204 | ReadOnly: &csiSource.ReadOnly, 205 | Kind: &managed, 206 | } 207 | 208 | if csiSource.VolumeAttributes != nil { 209 | for k, v := range csiSource.VolumeAttributes { 210 | switch strings.ToLower(k) { 211 | case azureDiskCachingMode: 212 | if v != "" { 213 | mode := v1.AzureDataDiskCachingMode(v) 214 | azureSource.CachingMode = &mode 215 | } 216 | case azureDiskFSType: 217 | if v != "" { 218 | fsType := v 219 | azureSource.FSType = &fsType 220 | } 221 | } 222 | } 223 | azureSource.Kind = &managed 224 | } 225 | 226 | pv.Spec.CSI = nil 227 | pv.Spec.AzureDisk = azureSource 228 | 229 | return pv, nil 230 | } 231 | 232 | // CanSupport tests whether the plugin supports a given volume 233 | // specification from the API. The spec pointer should be considered 234 | // const. 235 | func (t *azureDiskCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 236 | return pv != nil && pv.Spec.AzureDisk != nil 237 | } 238 | 239 | // CanSupportInline tests whether the plugin supports a given inline volume 240 | // specification from the API. The spec pointer should be considered 241 | // const. 242 | func (t *azureDiskCSITranslator) CanSupportInline(volume *v1.Volume) bool { 243 | return volume != nil && volume.AzureDisk != nil 244 | } 245 | 246 | // GetInTreePluginName returns the name of the intree plugin driver 247 | func (t *azureDiskCSITranslator) GetInTreePluginName() string { 248 | return AzureDiskInTreePluginName 249 | } 250 | 251 | // GetCSIPluginName returns the name of the CSI plugin 252 | func (t *azureDiskCSITranslator) GetCSIPluginName() string { 253 | return AzureDiskDriverName 254 | } 255 | 256 | func (t *azureDiskCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 257 | return volumeHandle, nil 258 | } 259 | 260 | func isManagedDisk(diskURI string) bool { 261 | if len(diskURI) > 4 && strings.ToLower(diskURI[:4]) == "http" { 262 | return false 263 | } 264 | return true 265 | } 266 | 267 | func getDiskName(diskURI string) (string, error) { 268 | diskPathRE := managedDiskPathRE 269 | if !isManagedDisk(diskURI) { 270 | diskPathRE = unmanagedDiskPathRE 271 | } 272 | 273 | matches := diskPathRE.FindStringSubmatch(diskURI) 274 | if len(matches) != 2 { 275 | return "", fmt.Errorf("could not get disk name from %s, correct format: %s", diskURI, diskPathRE) 276 | } 277 | return matches[1], nil 278 | } 279 | 280 | // Replace topology values for failure domains ("") to "", 281 | // as it's the value that the CSI driver expects. 282 | func (t *azureDiskCSITranslator) replaceFailureDomainsToCSI(terms []v1.TopologySelectorTerm) []v1.TopologySelectorTerm { 283 | if terms == nil { 284 | return nil 285 | } 286 | 287 | newTopologies := []v1.TopologySelectorTerm{} 288 | for _, term := range terms { 289 | newTerm := term.DeepCopy() 290 | for i := range newTerm.MatchLabelExpressions { 291 | exp := &newTerm.MatchLabelExpressions[i] 292 | if exp.Key == AzureDiskTopologyKey { 293 | for j := range exp.Values { 294 | if unzonedCSIRegionRE.Match([]byte(exp.Values[j])) { 295 | // Topologies "0", "1" etc are used when in-tree topology is translated to CSI in Azure 296 | // regions that don't have availability zones. E.g.: 297 | // topology.kubernetes.io/region: westus 298 | // topology.kubernetes.io/zone: "0" 299 | // The CSI driver uses zone "" instead of "0" in this case. 300 | // topology.disk.csi.azure.com/zone": "" 301 | exp.Values[j] = "" 302 | } 303 | } 304 | } 305 | } 306 | newTopologies = append(newTopologies, *newTerm) 307 | } 308 | return newTopologies 309 | } 310 | -------------------------------------------------------------------------------- /plugins/azure_disk_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | corev1 "k8s.io/api/core/v1" 25 | storage "k8s.io/api/storage/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/klog/v2/ktesting" 28 | _ "k8s.io/klog/v2/ktesting/init" 29 | ) 30 | 31 | func TestIsManagedDisk(t *testing.T) { 32 | tests := []struct { 33 | options string 34 | expected bool 35 | }{ 36 | { 37 | options: "testurl/subscriptions/12/resourceGroups/23/providers/Microsoft.Compute/disks/name", 38 | expected: true, 39 | }, 40 | { 41 | options: "test.com", 42 | expected: true, 43 | }, 44 | { 45 | options: "HTTP://test.com", 46 | expected: false, 47 | }, 48 | { 49 | options: "http://test.com/vhds/name", 50 | expected: false, 51 | }, 52 | } 53 | 54 | for _, test := range tests { 55 | result := isManagedDisk(test.options) 56 | if !reflect.DeepEqual(result, test.expected) { 57 | t.Errorf("input: %q, isManagedDisk result: %t, expected: %t", test.options, result, test.expected) 58 | } 59 | } 60 | } 61 | 62 | func TestGetDiskName(t *testing.T) { 63 | mDiskPathRE := managedDiskPathRE 64 | uDiskPathRE := unmanagedDiskPathRE 65 | tests := []struct { 66 | options string 67 | expected1 string 68 | expected2 error 69 | }{ 70 | { 71 | options: "testurl/subscriptions/12/resourceGroups/23/providers/Microsoft.Compute/disks/name", 72 | expected1: "name", 73 | expected2: nil, 74 | }, 75 | { 76 | options: "testurl/subscriptions/23/providers/Microsoft.Compute/disks/name", 77 | expected1: "", 78 | expected2: fmt.Errorf("could not get disk name from testurl/subscriptions/23/providers/Microsoft.Compute/disks/name, correct format: %s", mDiskPathRE), 79 | }, 80 | { 81 | options: "http://test.com/vhds/name", 82 | expected1: "name", 83 | expected2: nil, 84 | }, 85 | { 86 | options: "http://test.io/name", 87 | expected1: "", 88 | expected2: fmt.Errorf("could not get disk name from http://test.io/name, correct format: %s", uDiskPathRE), 89 | }, 90 | } 91 | 92 | for _, test := range tests { 93 | result1, result2 := getDiskName(test.options) 94 | if !reflect.DeepEqual(result1, test.expected1) || !reflect.DeepEqual(result2, test.expected2) { 95 | t.Errorf("input: %q, getDiskName result1: %q, expected1: %q, result2: %q, expected2: %q", test.options, result1, test.expected1, 96 | result2, test.expected2) 97 | } 98 | } 99 | } 100 | 101 | func TestTranslateAzureDiskInTreeInlineVolumeToCSI(t *testing.T) { 102 | sharedBlobDiskKind := corev1.AzureDedicatedBlobDisk 103 | translator := NewAzureDiskCSITranslator() 104 | logger, _ := ktesting.NewTestContext(t) 105 | 106 | cases := []struct { 107 | name string 108 | volume *corev1.Volume 109 | expVol *corev1.PersistentVolume 110 | expErr bool 111 | }{ 112 | { 113 | name: "empty volume", 114 | expErr: true, 115 | }, 116 | { 117 | name: "no azure disk volume", 118 | volume: &corev1.Volume{}, 119 | expErr: true, 120 | }, 121 | { 122 | name: "azure disk volume", 123 | volume: &corev1.Volume{ 124 | VolumeSource: corev1.VolumeSource{ 125 | AzureDisk: &corev1.AzureDiskVolumeSource{ 126 | DiskName: "diskname", 127 | DataDiskURI: "datadiskuri", 128 | }, 129 | }, 130 | }, 131 | expVol: &corev1.PersistentVolume{ 132 | ObjectMeta: metav1.ObjectMeta{ 133 | Name: "datadiskuri", 134 | }, 135 | Spec: corev1.PersistentVolumeSpec{ 136 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 137 | CSI: &corev1.CSIPersistentVolumeSource{ 138 | Driver: "disk.csi.azure.com", 139 | VolumeHandle: "datadiskuri", 140 | VolumeAttributes: map[string]string{azureDiskKind: "Managed"}, 141 | }, 142 | }, 143 | AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, 144 | }, 145 | }, 146 | }, 147 | { 148 | name: "azure disk volume with non-managed kind", 149 | volume: &corev1.Volume{ 150 | VolumeSource: corev1.VolumeSource{ 151 | AzureDisk: &corev1.AzureDiskVolumeSource{ 152 | DiskName: "diskname", 153 | DataDiskURI: "datadiskuri", 154 | Kind: &sharedBlobDiskKind, 155 | }, 156 | }, 157 | }, 158 | expErr: true, 159 | }, 160 | } 161 | 162 | for _, tc := range cases { 163 | t.Logf("Testing %v", tc.name) 164 | got, err := translator.TranslateInTreeInlineVolumeToCSI(logger, tc.volume, "") 165 | if err != nil && !tc.expErr { 166 | t.Errorf("Did not expect error but got: %v", err) 167 | } 168 | 169 | if err == nil && tc.expErr { 170 | t.Errorf("Expected error, but did not get one.") 171 | } 172 | 173 | if !reflect.DeepEqual(got, tc.expVol) { 174 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 175 | } 176 | } 177 | } 178 | 179 | func TestTranslateAzureDiskInTreePVToCSI(t *testing.T) { 180 | translator := NewAzureDiskCSITranslator() 181 | logger, _ := ktesting.NewTestContext(t) 182 | 183 | sharedBlobDiskKind := corev1.AzureDedicatedBlobDisk 184 | cachingMode := corev1.AzureDataDiskCachingMode("cachingmode") 185 | fsType := "fstype" 186 | readOnly := true 187 | diskURI := "/subscriptions/12/resourceGroups/23/providers/Microsoft.Compute/disks/name" 188 | 189 | cases := []struct { 190 | name string 191 | volume *corev1.PersistentVolume 192 | expVol *corev1.PersistentVolume 193 | expErr bool 194 | }{ 195 | { 196 | name: "empty volume", 197 | expErr: true, 198 | }, 199 | { 200 | name: "no azure disk volume", 201 | volume: &corev1.PersistentVolume{}, 202 | expErr: true, 203 | }, 204 | { 205 | name: "azure disk volume", 206 | volume: &corev1.PersistentVolume{ 207 | Spec: corev1.PersistentVolumeSpec{ 208 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 209 | AzureDisk: &corev1.AzureDiskVolumeSource{ 210 | CachingMode: &cachingMode, 211 | DataDiskURI: diskURI, 212 | FSType: &fsType, 213 | ReadOnly: &readOnly, 214 | }, 215 | }, 216 | }, 217 | }, 218 | expVol: &corev1.PersistentVolume{ 219 | Spec: corev1.PersistentVolumeSpec{ 220 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 221 | CSI: &corev1.CSIPersistentVolumeSource{ 222 | Driver: "disk.csi.azure.com", 223 | FSType: "fstype", 224 | ReadOnly: true, 225 | VolumeAttributes: map[string]string{ 226 | "cachingmode": "cachingmode", 227 | azureDiskFSType: fsType, 228 | azureDiskKind: "Managed", 229 | }, 230 | VolumeHandle: diskURI, 231 | }, 232 | }, 233 | }, 234 | }, 235 | }, 236 | { 237 | name: "azure disk volume with non-managed kind", 238 | volume: &corev1.PersistentVolume{ 239 | Spec: corev1.PersistentVolumeSpec{ 240 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 241 | AzureDisk: &corev1.AzureDiskVolumeSource{ 242 | CachingMode: &cachingMode, 243 | DataDiskURI: diskURI, 244 | FSType: &fsType, 245 | ReadOnly: &readOnly, 246 | Kind: &sharedBlobDiskKind, 247 | }, 248 | }, 249 | }, 250 | }, 251 | expErr: true, 252 | }, 253 | } 254 | 255 | for _, tc := range cases { 256 | t.Logf("Testing %v", tc.name) 257 | got, err := translator.TranslateInTreePVToCSI(logger, tc.volume) 258 | if err != nil && !tc.expErr { 259 | t.Errorf("Did not expect error but got: %v", err) 260 | } 261 | 262 | if err == nil && tc.expErr { 263 | t.Errorf("Expected error, but did not get one.") 264 | } 265 | 266 | if !reflect.DeepEqual(got, tc.expVol) { 267 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 268 | } 269 | } 270 | } 271 | 272 | func TestTranslateTranslateCSIPVToInTree(t *testing.T) { 273 | cachingModeNone := corev1.AzureDataDiskCachingNone 274 | cachingModeReadOnly := corev1.AzureDataDiskCachingReadOnly 275 | cachingModeReadWrite := corev1.AzureDataDiskCachingReadWrite 276 | fsType := "fstype" 277 | readOnly := true 278 | diskURI := "/subscriptions/12/resourceGroups/23/providers/Microsoft.Compute/disks/name" 279 | managed := corev1.AzureManagedDisk 280 | 281 | translator := NewAzureDiskCSITranslator() 282 | cases := []struct { 283 | name string 284 | cachingMode corev1.AzureDataDiskCachingMode 285 | volume *corev1.PersistentVolume 286 | expVol *corev1.PersistentVolume 287 | expErr bool 288 | }{ 289 | { 290 | name: "azure disk volume with ReadOnly cachingMode", 291 | cachingMode: corev1.AzureDataDiskCachingReadOnly, 292 | volume: &corev1.PersistentVolume{ 293 | Spec: corev1.PersistentVolumeSpec{ 294 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 295 | CSI: &corev1.CSIPersistentVolumeSource{ 296 | Driver: "disk.csi.azure.com", 297 | FSType: "fstype", 298 | ReadOnly: true, 299 | VolumeAttributes: map[string]string{ 300 | "cachingmode": "ReadOnly", 301 | azureDiskFSType: fsType, 302 | azureDiskKind: "managed", 303 | }, 304 | VolumeHandle: diskURI, 305 | }, 306 | }, 307 | }, 308 | }, 309 | expVol: &corev1.PersistentVolume{ 310 | Spec: corev1.PersistentVolumeSpec{ 311 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 312 | AzureDisk: &corev1.AzureDiskVolumeSource{ 313 | CachingMode: &cachingModeReadOnly, 314 | DataDiskURI: diskURI, 315 | FSType: &fsType, 316 | ReadOnly: &readOnly, 317 | Kind: &managed, 318 | DiskName: "name", 319 | }, 320 | }, 321 | }, 322 | }, 323 | expErr: false, 324 | }, 325 | { 326 | name: "azure disk volume with ReadOnly cachingMode", 327 | cachingMode: corev1.AzureDataDiskCachingReadOnly, 328 | volume: &corev1.PersistentVolume{ 329 | Spec: corev1.PersistentVolumeSpec{ 330 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 331 | CSI: &corev1.CSIPersistentVolumeSource{ 332 | Driver: "disk.csi.azure.com", 333 | FSType: "fstype", 334 | ReadOnly: true, 335 | VolumeAttributes: map[string]string{ 336 | "cachingmode": "ReadOnly", 337 | "fstype": fsType, 338 | azureDiskKind: "managed", 339 | }, 340 | VolumeHandle: diskURI, 341 | }, 342 | }, 343 | }, 344 | }, 345 | expVol: &corev1.PersistentVolume{ 346 | Spec: corev1.PersistentVolumeSpec{ 347 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 348 | AzureDisk: &corev1.AzureDiskVolumeSource{ 349 | CachingMode: &cachingModeReadOnly, 350 | DataDiskURI: diskURI, 351 | FSType: &fsType, 352 | ReadOnly: &readOnly, 353 | Kind: &managed, 354 | DiskName: "name", 355 | }, 356 | }, 357 | }, 358 | }, 359 | expErr: false, 360 | }, 361 | { 362 | name: "azure disk volume with None cachingMode", 363 | cachingMode: corev1.AzureDataDiskCachingReadOnly, 364 | volume: &corev1.PersistentVolume{ 365 | Spec: corev1.PersistentVolumeSpec{ 366 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 367 | CSI: &corev1.CSIPersistentVolumeSource{ 368 | Driver: "disk.csi.azure.com", 369 | FSType: "fstype", 370 | ReadOnly: true, 371 | VolumeAttributes: map[string]string{ 372 | "cachingMode": "None", 373 | "fsType": fsType, 374 | azureDiskKind: "managed", 375 | }, 376 | VolumeHandle: diskURI, 377 | }, 378 | }, 379 | }, 380 | }, 381 | expVol: &corev1.PersistentVolume{ 382 | Spec: corev1.PersistentVolumeSpec{ 383 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 384 | AzureDisk: &corev1.AzureDiskVolumeSource{ 385 | CachingMode: &cachingModeNone, 386 | DataDiskURI: diskURI, 387 | FSType: &fsType, 388 | ReadOnly: &readOnly, 389 | Kind: &managed, 390 | DiskName: "name", 391 | }, 392 | }, 393 | }, 394 | }, 395 | expErr: false, 396 | }, 397 | { 398 | name: "azure disk volume with ReadWrite cachingMode", 399 | cachingMode: corev1.AzureDataDiskCachingReadOnly, 400 | volume: &corev1.PersistentVolume{ 401 | Spec: corev1.PersistentVolumeSpec{ 402 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 403 | CSI: &corev1.CSIPersistentVolumeSource{ 404 | Driver: "disk.csi.azure.com", 405 | FSType: "fstype", 406 | ReadOnly: true, 407 | VolumeAttributes: map[string]string{ 408 | "cachingMode": "ReadWrite", 409 | "fsType": fsType, 410 | azureDiskKind: "managed", 411 | }, 412 | VolumeHandle: diskURI, 413 | }, 414 | }, 415 | }, 416 | }, 417 | expVol: &corev1.PersistentVolume{ 418 | Spec: corev1.PersistentVolumeSpec{ 419 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 420 | AzureDisk: &corev1.AzureDiskVolumeSource{ 421 | CachingMode: &cachingModeReadWrite, 422 | DataDiskURI: diskURI, 423 | FSType: &fsType, 424 | ReadOnly: &readOnly, 425 | Kind: &managed, 426 | DiskName: "name", 427 | }, 428 | }, 429 | }, 430 | }, 431 | expErr: false, 432 | }, 433 | } 434 | 435 | for _, tc := range cases { 436 | t.Logf("Testing %v", tc.name) 437 | got, err := translator.TranslateCSIPVToInTree(tc.volume) 438 | if err != nil && !tc.expErr { 439 | t.Errorf("Did not expect error but got: %v", err) 440 | } 441 | 442 | if err == nil && tc.expErr { 443 | t.Errorf("Expected error, but did not get one.") 444 | } 445 | 446 | if !reflect.DeepEqual(got, tc.expVol) { 447 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 448 | } 449 | } 450 | } 451 | 452 | func TestTranslateInTreeStorageClassToCSI(t *testing.T) { 453 | translator := NewAzureDiskCSITranslator() 454 | logger, _ := ktesting.NewTestContext(t) 455 | 456 | tcs := []struct { 457 | name string 458 | options *storage.StorageClass 459 | expOptions *storage.StorageClass 460 | expErr bool 461 | }{ 462 | { 463 | name: "nothing special", 464 | options: NewStorageClass(map[string]string{"foo": "bar"}, nil), 465 | expOptions: NewStorageClass(map[string]string{"foo": "bar"}, nil), 466 | }, 467 | { 468 | name: "empty params", 469 | options: NewStorageClass(map[string]string{}, nil), 470 | expOptions: NewStorageClass(map[string]string{}, nil), 471 | }, 472 | { 473 | name: "zone", 474 | options: NewStorageClass(map[string]string{"zone": "foo"}, nil), 475 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 476 | }, 477 | { 478 | name: "zones", 479 | options: NewStorageClass(map[string]string{"zones": "foo,bar,baz"}, nil), 480 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo", "bar", "baz"})), 481 | }, 482 | { 483 | name: "some normal topology", 484 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 485 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 486 | }, 487 | { 488 | name: "some translated topology", 489 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(corev1.LabelTopologyZone, []string{"foo"})), 490 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 491 | }, 492 | { 493 | name: "some translated topology with beta labels", 494 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(corev1.LabelFailureDomainBetaZone, []string{"foo"})), 495 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 496 | }, 497 | { 498 | name: "zone and topology", 499 | options: NewStorageClass(map[string]string{"zone": "foo"}, generateToplogySelectors(AzureDiskTopologyKey, []string{"foo"})), 500 | expErr: true, 501 | }, 502 | { 503 | name: "topology in regions without zones", 504 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(corev1.LabelTopologyZone, []string{"0"})), 505 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{""})), 506 | }, 507 | { 508 | name: "longer topology in regions without zones", 509 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(corev1.LabelTopologyZone, []string{"1234"})), 510 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{""})), 511 | }, 512 | { 513 | name: "topology in regions with zones", 514 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(corev1.LabelTopologyZone, []string{"centralus-1"})), 515 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(AzureDiskTopologyKey, []string{"centralus-1"})), 516 | }, 517 | } 518 | 519 | for _, tc := range tcs { 520 | t.Logf("Testing %v", tc.name) 521 | gotOptions, err := translator.TranslateInTreeStorageClassToCSI(logger, tc.options) 522 | if err != nil && !tc.expErr { 523 | t.Errorf("Did not expect error but got: %v", err) 524 | } 525 | if err == nil && tc.expErr { 526 | t.Errorf("Expected error, but did not get one.") 527 | } 528 | if !reflect.DeepEqual(gotOptions, tc.expOptions) { 529 | t.Errorf("Got parameters: %v, expected :%v", gotOptions, tc.expOptions) 530 | } 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /plugins/azure_file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "regexp" 22 | "strings" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | storage "k8s.io/api/storage/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/klog/v2" 28 | ) 29 | 30 | const ( 31 | // AzureFileDriverName is the name of the CSI driver for Azure File 32 | AzureFileDriverName = "file.csi.azure.com" 33 | // AzureFileInTreePluginName is the name of the intree plugin for Azure file 34 | AzureFileInTreePluginName = "kubernetes.io/azure-file" 35 | 36 | separator = "#" 37 | volumeIDTemplate = "%s#%s#%s#%s#%s" 38 | // Parameter names defined in azure file CSI driver, refer to 39 | // https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md 40 | shareNameField = "sharename" 41 | secretNameField = "secretname" 42 | secretNamespaceField = "secretnamespace" 43 | secretNameTemplate = "azure-storage-account-%s-secret" 44 | defaultSecretNamespace = "default" 45 | resourceGroupAnnotation = "kubernetes.io/azure-file-resource-group" 46 | ) 47 | 48 | var _ InTreePlugin = &azureFileCSITranslator{} 49 | 50 | var secretNameFormatRE = regexp.MustCompile(`azure-storage-account-(.+)-secret`) 51 | 52 | // azureFileCSITranslator handles translation of PV spec from In-tree 53 | // Azure File to CSI Azure File and vice versa 54 | type azureFileCSITranslator struct{} 55 | 56 | // NewAzureFileCSITranslator returns a new instance of azureFileTranslator 57 | func NewAzureFileCSITranslator() InTreePlugin { 58 | return &azureFileCSITranslator{} 59 | } 60 | 61 | // TranslateInTreeStorageClassToCSI translates InTree Azure File storage class parameters to CSI storage class 62 | func (t *azureFileCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 63 | return sc, nil 64 | } 65 | 66 | // TranslateInTreeInlineVolumeToCSI takes a Volume with AzureFile set from in-tree 67 | // and converts the AzureFile source to a CSIPersistentVolumeSource 68 | func (t *azureFileCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 69 | if volume == nil || volume.AzureFile == nil { 70 | return nil, fmt.Errorf("volume is nil or Azure File not defined on volume") 71 | } 72 | 73 | azureSource := volume.AzureFile 74 | accountName, err := getStorageAccountName(azureSource.SecretName) 75 | if err != nil { 76 | logger.V(5).Info("getStorageAccountName returned with error", "secretName", azureSource.SecretName, "err", err) 77 | accountName = azureSource.SecretName 78 | } 79 | 80 | secretNamespace := defaultSecretNamespace 81 | if podNamespace != "" { 82 | secretNamespace = podNamespace 83 | } 84 | volumeID := fmt.Sprintf(volumeIDTemplate, "", accountName, azureSource.ShareName, volume.Name, secretNamespace) 85 | 86 | var ( 87 | pv = &v1.PersistentVolume{ 88 | ObjectMeta: metav1.ObjectMeta{ 89 | // Must be unique as it is used as the unique part of the staging path 90 | Name: volumeID, 91 | }, 92 | Spec: v1.PersistentVolumeSpec{ 93 | PersistentVolumeSource: v1.PersistentVolumeSource{ 94 | CSI: &v1.CSIPersistentVolumeSource{ 95 | Driver: AzureFileDriverName, 96 | VolumeHandle: volumeID, 97 | ReadOnly: azureSource.ReadOnly, 98 | VolumeAttributes: map[string]string{shareNameField: azureSource.ShareName}, 99 | NodeStageSecretRef: &v1.SecretReference{ 100 | Name: azureSource.SecretName, 101 | Namespace: secretNamespace, 102 | }, 103 | }, 104 | }, 105 | AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}, 106 | }, 107 | } 108 | ) 109 | 110 | return pv, nil 111 | } 112 | 113 | // TranslateInTreePVToCSI takes a PV with AzureFile set from in-tree 114 | // and converts the AzureFile source to a CSIPersistentVolumeSource 115 | func (t *azureFileCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 116 | if pv == nil || pv.Spec.AzureFile == nil { 117 | return nil, fmt.Errorf("pv is nil or Azure File source not defined on pv") 118 | } 119 | 120 | azureSource := pv.Spec.PersistentVolumeSource.AzureFile 121 | accountName, err := getStorageAccountName(azureSource.SecretName) 122 | if err != nil { 123 | logger.V(5).Info("getStorageAccountName returned with error", "secretName", azureSource.SecretName, "err", err) 124 | accountName = azureSource.SecretName 125 | } 126 | resourceGroup := "" 127 | if pv.ObjectMeta.Annotations != nil { 128 | if v, ok := pv.ObjectMeta.Annotations[resourceGroupAnnotation]; ok { 129 | resourceGroup = v 130 | } 131 | } 132 | 133 | // Secret is required when mounting a volume but pod presence cannot be assumed - we should not try to read pod now. 134 | namespace := "" 135 | // Try to read SecretNamespace from source pv. 136 | if azureSource.SecretNamespace != nil { 137 | namespace = *azureSource.SecretNamespace 138 | } else { 139 | // Try to read namespace from ClaimRef which should be always present. 140 | if pv.Spec.ClaimRef != nil { 141 | namespace = pv.Spec.ClaimRef.Namespace 142 | } 143 | } 144 | 145 | if len(namespace) == 0 { 146 | return nil, fmt.Errorf("could not find a secret namespace in PersistentVolumeSource or ClaimRef") 147 | } 148 | 149 | volumeID := fmt.Sprintf(volumeIDTemplate, resourceGroup, accountName, azureSource.ShareName, pv.ObjectMeta.Name, namespace) 150 | 151 | var ( 152 | // refer to https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md 153 | csiSource = &v1.CSIPersistentVolumeSource{ 154 | Driver: AzureFileDriverName, 155 | NodeStageSecretRef: &v1.SecretReference{ 156 | Name: azureSource.SecretName, 157 | Namespace: namespace, 158 | }, 159 | ReadOnly: azureSource.ReadOnly, 160 | VolumeAttributes: map[string]string{shareNameField: azureSource.ShareName}, 161 | VolumeHandle: volumeID, 162 | } 163 | ) 164 | 165 | pv.Spec.PersistentVolumeSource.AzureFile = nil 166 | pv.Spec.PersistentVolumeSource.CSI = csiSource 167 | 168 | return pv, nil 169 | } 170 | 171 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 172 | // translates the Azure File CSI source to a AzureFile source. 173 | func (t *azureFileCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 174 | if pv == nil || pv.Spec.CSI == nil { 175 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 176 | } 177 | csiSource := pv.Spec.CSI 178 | 179 | // refer to https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md 180 | azureSource := &v1.AzureFilePersistentVolumeSource{ 181 | ReadOnly: csiSource.ReadOnly, 182 | } 183 | 184 | for k, v := range csiSource.VolumeAttributes { 185 | switch strings.ToLower(k) { 186 | case shareNameField: 187 | azureSource.ShareName = v 188 | case secretNameField: 189 | azureSource.SecretName = v 190 | case secretNamespaceField: 191 | ns := v 192 | azureSource.SecretNamespace = &ns 193 | } 194 | } 195 | 196 | resourceGroup := "" 197 | if csiSource.NodeStageSecretRef != nil && csiSource.NodeStageSecretRef.Name != "" { 198 | azureSource.SecretName = csiSource.NodeStageSecretRef.Name 199 | azureSource.SecretNamespace = &csiSource.NodeStageSecretRef.Namespace 200 | } 201 | if azureSource.ShareName == "" || azureSource.SecretName == "" { 202 | rg, storageAccount, fileShareName, _, err := getFileShareInfo(csiSource.VolumeHandle) 203 | if err != nil { 204 | return nil, err 205 | } 206 | if azureSource.ShareName == "" { 207 | azureSource.ShareName = fileShareName 208 | } 209 | if azureSource.SecretName == "" { 210 | azureSource.SecretName = fmt.Sprintf(secretNameTemplate, storageAccount) 211 | } 212 | resourceGroup = rg 213 | } 214 | 215 | if azureSource.SecretNamespace == nil { 216 | ns := defaultSecretNamespace 217 | azureSource.SecretNamespace = &ns 218 | } 219 | 220 | pv.Spec.CSI = nil 221 | pv.Spec.AzureFile = azureSource 222 | if pv.ObjectMeta.Annotations == nil { 223 | pv.ObjectMeta.Annotations = map[string]string{} 224 | } 225 | if resourceGroup != "" { 226 | pv.ObjectMeta.Annotations[resourceGroupAnnotation] = resourceGroup 227 | } 228 | 229 | return pv, nil 230 | } 231 | 232 | // CanSupport tests whether the plugin supports a given volume 233 | // specification from the API. The spec pointer should be considered 234 | // const. 235 | func (t *azureFileCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 236 | return pv != nil && pv.Spec.AzureFile != nil 237 | } 238 | 239 | // CanSupportInline tests whether the plugin supports a given inline volume 240 | // specification from the API. The spec pointer should be considered 241 | // const. 242 | func (t *azureFileCSITranslator) CanSupportInline(volume *v1.Volume) bool { 243 | return volume != nil && volume.AzureFile != nil 244 | } 245 | 246 | // GetInTreePluginName returns the name of the intree plugin driver 247 | func (t *azureFileCSITranslator) GetInTreePluginName() string { 248 | return AzureFileInTreePluginName 249 | } 250 | 251 | // GetCSIPluginName returns the name of the CSI plugin 252 | func (t *azureFileCSITranslator) GetCSIPluginName() string { 253 | return AzureFileDriverName 254 | } 255 | 256 | func (t *azureFileCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 257 | return volumeHandle, nil 258 | } 259 | 260 | // get file share info according to volume id, e.g. 261 | // input: "rg#f5713de20cde511e8ba4900#pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41#diskname.vhd" 262 | // output: rg, f5713de20cde511e8ba4900, pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41, diskname.vhd 263 | func getFileShareInfo(id string) (string, string, string, string, error) { 264 | segments := strings.Split(id, separator) 265 | if len(segments) < 3 { 266 | return "", "", "", "", fmt.Errorf("error parsing volume id: %q, should at least contain two #", id) 267 | } 268 | var diskName string 269 | if len(segments) > 3 { 270 | diskName = segments[3] 271 | } 272 | return segments[0], segments[1], segments[2], diskName, nil 273 | } 274 | 275 | // get storage account name from secret name 276 | func getStorageAccountName(secretName string) (string, error) { 277 | matches := secretNameFormatRE.FindStringSubmatch(secretName) 278 | if len(matches) != 2 { 279 | return "", fmt.Errorf("could not get account name from %s, correct format: %s", secretName, secretNameFormatRE) 280 | } 281 | return matches[1], nil 282 | } 283 | -------------------------------------------------------------------------------- /plugins/azure_file_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/klog/v2/ktesting" 27 | _ "k8s.io/klog/v2/ktesting/init" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestGetFileShareInfo(t *testing.T) { 33 | tests := []struct { 34 | id string 35 | resourceGroupName string 36 | accountName string 37 | fileShareName string 38 | diskName string 39 | expectedError error 40 | }{ 41 | { 42 | id: "rg#f5713de20cde511e8ba4900#pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41#diskname.vhd", 43 | resourceGroupName: "rg", 44 | accountName: "f5713de20cde511e8ba4900", 45 | fileShareName: "pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41", 46 | diskName: "diskname.vhd", 47 | expectedError: nil, 48 | }, 49 | { 50 | id: "rg#f5713de20cde511e8ba4900#pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41", 51 | resourceGroupName: "rg", 52 | accountName: "f5713de20cde511e8ba4900", 53 | fileShareName: "pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41", 54 | diskName: "", 55 | expectedError: nil, 56 | }, 57 | { 58 | id: "rg#f5713de20cde511e8ba4900", 59 | resourceGroupName: "", 60 | accountName: "", 61 | fileShareName: "", 62 | diskName: "", 63 | expectedError: fmt.Errorf("error parsing volume id: \"rg#f5713de20cde511e8ba4900\", should at least contain two #"), 64 | }, 65 | { 66 | id: "rg", 67 | resourceGroupName: "", 68 | accountName: "", 69 | fileShareName: "", 70 | diskName: "", 71 | expectedError: fmt.Errorf("error parsing volume id: \"rg\", should at least contain two #"), 72 | }, 73 | { 74 | id: "", 75 | resourceGroupName: "", 76 | accountName: "", 77 | fileShareName: "", 78 | diskName: "", 79 | expectedError: fmt.Errorf("error parsing volume id: \"\", should at least contain two #"), 80 | }, 81 | } 82 | 83 | for _, test := range tests { 84 | resourceGroupName, accountName, fileShareName, diskName, expectedError := getFileShareInfo(test.id) 85 | if resourceGroupName != test.resourceGroupName { 86 | t.Errorf("getFileShareInfo(%q) returned with: %q, expected: %q", test.id, resourceGroupName, test.resourceGroupName) 87 | } 88 | if accountName != test.accountName { 89 | t.Errorf("getFileShareInfo(%q) returned with: %q, expected: %q", test.id, accountName, test.accountName) 90 | } 91 | if fileShareName != test.fileShareName { 92 | t.Errorf("getFileShareInfo(%q) returned with: %q, expected: %q", test.id, fileShareName, test.fileShareName) 93 | } 94 | if diskName != test.diskName { 95 | t.Errorf("getFileShareInfo(%q) returned with: %q, expected: %q", test.id, diskName, test.diskName) 96 | } 97 | if !reflect.DeepEqual(expectedError, test.expectedError) { 98 | t.Errorf("getFileShareInfo(%q) returned with: %v, expected: %v", test.id, expectedError, test.expectedError) 99 | } 100 | } 101 | } 102 | 103 | func TestTranslateAzureFileInTreeStorageClassToCSI(t *testing.T) { 104 | translator := NewAzureFileCSITranslator() 105 | logger, _ := ktesting.NewTestContext(t) 106 | 107 | cases := []struct { 108 | name string 109 | volume *corev1.Volume 110 | podNamespace string 111 | expVol *corev1.PersistentVolume 112 | expErr bool 113 | }{ 114 | { 115 | name: "empty volume", 116 | expErr: true, 117 | }, 118 | { 119 | name: "no azure file volume", 120 | volume: &corev1.Volume{}, 121 | expErr: true, 122 | }, 123 | { 124 | name: "azure file volume", 125 | volume: &corev1.Volume{ 126 | Name: "name", 127 | VolumeSource: corev1.VolumeSource{ 128 | AzureFile: &corev1.AzureFileVolumeSource{ 129 | ReadOnly: true, 130 | SecretName: "secretname", 131 | ShareName: "sharename", 132 | }, 133 | }, 134 | }, 135 | expVol: &corev1.PersistentVolume{ 136 | ObjectMeta: metav1.ObjectMeta{ 137 | Name: "#secretname#sharename#name#default", 138 | }, 139 | Spec: corev1.PersistentVolumeSpec{ 140 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 141 | CSI: &corev1.CSIPersistentVolumeSource{ 142 | Driver: "file.csi.azure.com", 143 | NodeStageSecretRef: &corev1.SecretReference{ 144 | Name: "secretname", 145 | Namespace: "default", 146 | }, 147 | ReadOnly: true, 148 | VolumeAttributes: map[string]string{shareNameField: "sharename"}, 149 | VolumeHandle: "#secretname#sharename#name#default", 150 | }, 151 | }, 152 | AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, 153 | }, 154 | }, 155 | }, 156 | { 157 | name: "azure file volume with a pod namespace", 158 | volume: &corev1.Volume{ 159 | Name: "name", 160 | VolumeSource: corev1.VolumeSource{ 161 | AzureFile: &corev1.AzureFileVolumeSource{ 162 | ReadOnly: true, 163 | SecretName: "secretname", 164 | ShareName: "sharename", 165 | }, 166 | }, 167 | }, 168 | podNamespace: "test", 169 | expVol: &corev1.PersistentVolume{ 170 | ObjectMeta: metav1.ObjectMeta{ 171 | Name: "#secretname#sharename#name#test", 172 | }, 173 | Spec: corev1.PersistentVolumeSpec{ 174 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 175 | CSI: &corev1.CSIPersistentVolumeSource{ 176 | Driver: "file.csi.azure.com", 177 | NodeStageSecretRef: &corev1.SecretReference{ 178 | Name: "secretname", 179 | Namespace: "test", 180 | }, 181 | ReadOnly: true, 182 | VolumeAttributes: map[string]string{shareNameField: "sharename"}, 183 | VolumeHandle: "#secretname#sharename#name#test", 184 | }, 185 | }, 186 | AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, 187 | }, 188 | }, 189 | }, 190 | } 191 | 192 | for _, tc := range cases { 193 | t.Logf("Testing %v", tc.name) 194 | got, err := translator.TranslateInTreeInlineVolumeToCSI(logger, tc.volume, tc.podNamespace) 195 | if err != nil && !tc.expErr { 196 | t.Errorf("Did not expect error but got: %v", err) 197 | } 198 | 199 | if err == nil && tc.expErr { 200 | t.Errorf("Expected error, but did not get one.") 201 | } 202 | 203 | if !reflect.DeepEqual(got, tc.expVol) { 204 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 205 | } 206 | } 207 | } 208 | 209 | func TestTranslateAzureFileInTreePVToCSI(t *testing.T) { 210 | translator := NewAzureFileCSITranslator() 211 | logger, _ := ktesting.NewTestContext(t) 212 | 213 | secretNamespace := "secretnamespace" 214 | 215 | cases := []struct { 216 | name string 217 | volume *corev1.PersistentVolume 218 | expVol *corev1.PersistentVolume 219 | expErr bool 220 | }{ 221 | { 222 | name: "empty volume", 223 | expErr: true, 224 | }, 225 | { 226 | name: "no azure file volume", 227 | volume: &corev1.PersistentVolume{}, 228 | expErr: true, 229 | }, 230 | { 231 | name: "return error if secret namespace could not be found", 232 | volume: &corev1.PersistentVolume{ 233 | ObjectMeta: metav1.ObjectMeta{ 234 | Name: "uuid", 235 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 236 | }, 237 | Spec: corev1.PersistentVolumeSpec{ 238 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 239 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 240 | ShareName: "sharename", 241 | SecretName: "secretname", 242 | ReadOnly: true, 243 | }, 244 | }, 245 | }, 246 | }, 247 | expErr: true, 248 | }, 249 | { 250 | name: "azure file volume", 251 | volume: &corev1.PersistentVolume{ 252 | ObjectMeta: metav1.ObjectMeta{ 253 | Name: "uuid", 254 | }, 255 | Spec: corev1.PersistentVolumeSpec{ 256 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 257 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 258 | ShareName: "sharename", 259 | SecretName: "secretname", 260 | SecretNamespace: &secretNamespace, 261 | ReadOnly: true, 262 | }, 263 | }, 264 | }, 265 | }, 266 | expVol: &corev1.PersistentVolume{ 267 | ObjectMeta: metav1.ObjectMeta{ 268 | Name: "uuid", 269 | }, 270 | Spec: corev1.PersistentVolumeSpec{ 271 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 272 | CSI: &corev1.CSIPersistentVolumeSource{ 273 | Driver: "file.csi.azure.com", 274 | ReadOnly: true, 275 | NodeStageSecretRef: &corev1.SecretReference{ 276 | Name: "secretname", 277 | Namespace: secretNamespace, 278 | }, 279 | VolumeAttributes: map[string]string{shareNameField: "sharename"}, 280 | VolumeHandle: "#secretname#sharename#uuid#secretnamespace", 281 | }, 282 | }, 283 | }, 284 | }, 285 | }, 286 | { 287 | name: "azure file volume with rg annotation", 288 | volume: &corev1.PersistentVolume{ 289 | ObjectMeta: metav1.ObjectMeta{ 290 | Name: "uuid", 291 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 292 | }, 293 | Spec: corev1.PersistentVolumeSpec{ 294 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 295 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 296 | ShareName: "sharename", 297 | SecretName: "secretname", 298 | SecretNamespace: &secretNamespace, 299 | ReadOnly: true, 300 | }, 301 | }, 302 | }, 303 | }, 304 | expVol: &corev1.PersistentVolume{ 305 | ObjectMeta: metav1.ObjectMeta{ 306 | Name: "uuid", 307 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 308 | }, 309 | Spec: corev1.PersistentVolumeSpec{ 310 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 311 | CSI: &corev1.CSIPersistentVolumeSource{ 312 | Driver: "file.csi.azure.com", 313 | ReadOnly: true, 314 | NodeStageSecretRef: &corev1.SecretReference{ 315 | Name: "secretname", 316 | Namespace: secretNamespace, 317 | }, 318 | VolumeAttributes: map[string]string{shareNameField: "sharename"}, 319 | VolumeHandle: "rg#secretname#sharename#uuid#secretnamespace", 320 | }, 321 | }, 322 | }, 323 | }, 324 | }, 325 | { 326 | name: "get secret namespace from ClaimRef when it's missing in pv spec source", 327 | volume: &corev1.PersistentVolume{ 328 | ObjectMeta: metav1.ObjectMeta{ 329 | Name: "uuid", 330 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 331 | }, 332 | Spec: corev1.PersistentVolumeSpec{ 333 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 334 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 335 | ShareName: "sharename", 336 | SecretName: "secretname", 337 | //SecretNamespace: &secretNamespace, 338 | ReadOnly: true, 339 | }, 340 | }, 341 | ClaimRef: &corev1.ObjectReference{ 342 | Namespace: secretNamespace, 343 | }, 344 | }, 345 | }, 346 | expVol: &corev1.PersistentVolume{ 347 | ObjectMeta: metav1.ObjectMeta{ 348 | Name: "uuid", 349 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 350 | }, 351 | Spec: corev1.PersistentVolumeSpec{ 352 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 353 | CSI: &corev1.CSIPersistentVolumeSource{ 354 | Driver: "file.csi.azure.com", 355 | ReadOnly: true, 356 | NodeStageSecretRef: &corev1.SecretReference{ 357 | Name: "secretname", 358 | Namespace: secretNamespace, 359 | }, 360 | VolumeAttributes: map[string]string{shareNameField: "sharename"}, 361 | VolumeHandle: "rg#secretname#sharename#uuid#secretnamespace", 362 | }, 363 | }, 364 | ClaimRef: &corev1.ObjectReference{ 365 | Namespace: secretNamespace, 366 | }, 367 | }, 368 | }, 369 | }, 370 | } 371 | 372 | for _, tc := range cases { 373 | t.Logf("Testing %v", tc.name) 374 | got, err := translator.TranslateInTreePVToCSI(logger, tc.volume) 375 | if err != nil && !tc.expErr { 376 | t.Errorf("Did not expect error but got: %v", err) 377 | } 378 | 379 | if err == nil && tc.expErr { 380 | t.Errorf("Expected error, but did not get one.") 381 | } 382 | 383 | if !reflect.DeepEqual(got, tc.expVol) { 384 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 385 | } 386 | } 387 | } 388 | 389 | func TestTranslateCSIPVToInTree(t *testing.T) { 390 | translator := NewAzureFileCSITranslator() 391 | 392 | secretName := "secretname" 393 | secretNamespace := "secretnamespace" 394 | shareName := "sharename" 395 | defaultNS := "default" 396 | mp := make(map[string]string) 397 | mp["shareName"] = shareName 398 | 399 | secretMap := make(map[string]string) 400 | secretMap["shareName"] = shareName 401 | secretMap["secretName"] = secretName 402 | secretMap["secretNamespace"] = secretNamespace 403 | 404 | cases := []struct { 405 | name string 406 | volume *corev1.PersistentVolume 407 | expVol *corev1.PersistentVolume 408 | expErr bool 409 | }{ 410 | { 411 | name: "empty volume", 412 | expErr: true, 413 | }, 414 | { 415 | name: "resource group empty", 416 | volume: &corev1.PersistentVolume{ 417 | Spec: corev1.PersistentVolumeSpec{ 418 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 419 | CSI: &corev1.CSIPersistentVolumeSource{ 420 | NodeStageSecretRef: &corev1.SecretReference{ 421 | Name: "ut", 422 | Namespace: secretNamespace, 423 | }, 424 | ReadOnly: true, 425 | VolumeAttributes: mp, 426 | }, 427 | }, 428 | }, 429 | }, 430 | expVol: &corev1.PersistentVolume{ 431 | ObjectMeta: metav1.ObjectMeta{ 432 | Annotations: map[string]string{}, 433 | }, 434 | Spec: corev1.PersistentVolumeSpec{ 435 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 436 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 437 | SecretName: "ut", 438 | SecretNamespace: &secretNamespace, 439 | ReadOnly: true, 440 | ShareName: shareName, 441 | }, 442 | }, 443 | }, 444 | }, 445 | expErr: false, 446 | }, 447 | { 448 | name: "translate from volume handle error", 449 | volume: &corev1.PersistentVolume{ 450 | Spec: corev1.PersistentVolumeSpec{ 451 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 452 | CSI: &corev1.CSIPersistentVolumeSource{ 453 | VolumeHandle: shareName, 454 | ReadOnly: true, 455 | VolumeAttributes: mp, 456 | }, 457 | }, 458 | }, 459 | }, 460 | expErr: true, 461 | }, 462 | { 463 | name: "translate from VolumeAttributes", 464 | volume: &corev1.PersistentVolume{ 465 | ObjectMeta: metav1.ObjectMeta{ 466 | Name: "file.csi.azure.com-sharename", 467 | }, 468 | Spec: corev1.PersistentVolumeSpec{ 469 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 470 | CSI: &corev1.CSIPersistentVolumeSource{ 471 | VolumeHandle: "rg#st#pvc-file-dynamic#diskname.vhd", 472 | ReadOnly: true, 473 | VolumeAttributes: mp, 474 | }, 475 | }, 476 | }, 477 | }, 478 | expVol: &corev1.PersistentVolume{ 479 | ObjectMeta: metav1.ObjectMeta{ 480 | Name: "file.csi.azure.com-sharename", 481 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 482 | }, 483 | Spec: corev1.PersistentVolumeSpec{ 484 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 485 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 486 | SecretName: "azure-storage-account-st-secret", 487 | ShareName: shareName, 488 | SecretNamespace: &defaultNS, 489 | ReadOnly: true, 490 | }, 491 | }, 492 | }, 493 | }, 494 | expErr: false, 495 | }, 496 | { 497 | name: "translate from SecretMap VolumeAttributes", 498 | volume: &corev1.PersistentVolume{ 499 | ObjectMeta: metav1.ObjectMeta{ 500 | Name: "file.csi.azure.com-sharename", 501 | Annotations: map[string]string{}, 502 | }, 503 | Spec: corev1.PersistentVolumeSpec{ 504 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 505 | CSI: &corev1.CSIPersistentVolumeSource{ 506 | VolumeHandle: "rg#st#pvc-file-dynamic#diskname.vhd", 507 | ReadOnly: true, 508 | VolumeAttributes: secretMap, 509 | }, 510 | }, 511 | }, 512 | }, 513 | expVol: &corev1.PersistentVolume{ 514 | ObjectMeta: metav1.ObjectMeta{ 515 | Name: "file.csi.azure.com-sharename", 516 | Annotations: map[string]string{}, 517 | }, 518 | Spec: corev1.PersistentVolumeSpec{ 519 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 520 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 521 | SecretName: secretName, 522 | SecretNamespace: &secretNamespace, 523 | ShareName: shareName, 524 | ReadOnly: true, 525 | }, 526 | }, 527 | }, 528 | }, 529 | expErr: false, 530 | }, 531 | { 532 | name: "translate from NodeStageSecretRef", 533 | volume: &corev1.PersistentVolume{ 534 | ObjectMeta: metav1.ObjectMeta{ 535 | Name: "file.csi.azure.com-sharename", 536 | }, 537 | Spec: corev1.PersistentVolumeSpec{ 538 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 539 | CSI: &corev1.CSIPersistentVolumeSource{ 540 | VolumeHandle: "rg#st#pvc-file-dynamic#diskname.vhd", 541 | ReadOnly: true, 542 | VolumeAttributes: mp, 543 | NodeStageSecretRef: &corev1.SecretReference{ 544 | Name: secretName, 545 | Namespace: secretNamespace, 546 | }, 547 | }, 548 | }, 549 | }, 550 | }, 551 | expVol: &corev1.PersistentVolume{ 552 | ObjectMeta: metav1.ObjectMeta{ 553 | Name: "file.csi.azure.com-sharename", 554 | Annotations: map[string]string{}, 555 | }, 556 | Spec: corev1.PersistentVolumeSpec{ 557 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 558 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 559 | SecretName: secretName, 560 | ShareName: shareName, 561 | SecretNamespace: &secretNamespace, 562 | ReadOnly: true, 563 | }, 564 | }, 565 | }, 566 | }, 567 | expErr: false, 568 | }, 569 | { 570 | name: "translate from VolumeHandle", 571 | volume: &corev1.PersistentVolume{ 572 | ObjectMeta: metav1.ObjectMeta{ 573 | Name: "file.csi.azure.com-sharename", 574 | }, 575 | Spec: corev1.PersistentVolumeSpec{ 576 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 577 | CSI: &corev1.CSIPersistentVolumeSource{ 578 | VolumeHandle: "rg#st#pvc-file-dynamic#diskname.vhd", 579 | ReadOnly: true, 580 | }, 581 | }, 582 | }, 583 | }, 584 | expVol: &corev1.PersistentVolume{ 585 | ObjectMeta: metav1.ObjectMeta{ 586 | Name: "file.csi.azure.com-sharename", 587 | Annotations: map[string]string{resourceGroupAnnotation: "rg"}, 588 | }, 589 | Spec: corev1.PersistentVolumeSpec{ 590 | PersistentVolumeSource: corev1.PersistentVolumeSource{ 591 | AzureFile: &corev1.AzureFilePersistentVolumeSource{ 592 | SecretName: "azure-storage-account-st-secret", 593 | ShareName: "pvc-file-dynamic", 594 | SecretNamespace: &defaultNS, 595 | ReadOnly: true, 596 | }, 597 | }, 598 | }, 599 | }, 600 | expErr: false, 601 | }, 602 | } 603 | 604 | for _, tc := range cases { 605 | t.Logf("Testing %v", tc.name) 606 | got, err := translator.TranslateCSIPVToInTree(tc.volume) 607 | if err != nil && !tc.expErr { 608 | t.Errorf("Did not expect error but got: %v", err) 609 | } 610 | 611 | if err == nil && tc.expErr { 612 | t.Errorf("Expected error, but did not get one.") 613 | } 614 | 615 | if !reflect.DeepEqual(got, tc.expVol) { 616 | t.Errorf("Got parameters: %v, expected :%v", got, tc.expVol) 617 | } 618 | } 619 | 620 | } 621 | 622 | func TestGetStorageAccount(t *testing.T) { 623 | tests := []struct { 624 | secretName string 625 | expectedError bool 626 | expectedResult string 627 | }{ 628 | { 629 | secretName: "azure-storage-account-accountname-secret", 630 | expectedError: false, 631 | expectedResult: "accountname", 632 | }, 633 | { 634 | secretName: "azure-storage-account-accountname-dup-secret", 635 | expectedError: false, 636 | expectedResult: "accountname-dup", 637 | }, 638 | { 639 | secretName: "invalid", 640 | expectedError: true, 641 | expectedResult: "", 642 | }, 643 | } 644 | 645 | for i, test := range tests { 646 | accountName, err := getStorageAccountName(test.secretName) 647 | assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]", i) 648 | assert.Equal(t, test.expectedResult, accountName, "TestCase[%d]", i) 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /plugins/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | // Matches the delimiter LabelMultiZoneDelimiter used by k8s.io/cloud-provider/volume and is mirrored here to avoid a large dependency 20 | // labelMultiZoneDelimiter separates zones for volumes 21 | const labelMultiZoneDelimiter = "__" 22 | -------------------------------------------------------------------------------- /plugins/gce_pd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "strings" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | storage "k8s.io/api/storage/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/util/sets" 28 | "k8s.io/klog/v2" 29 | ) 30 | 31 | const ( 32 | // GCEPDDriverName is the name of the CSI driver for GCE PD 33 | GCEPDDriverName = "pd.csi.storage.gke.io" 34 | // GCEPDInTreePluginName is the name of the intree plugin for GCE PD 35 | GCEPDInTreePluginName = "kubernetes.io/gce-pd" 36 | 37 | // GCEPDTopologyKey is the zonal topology key for GCE PD CSI Driver 38 | GCEPDTopologyKey = "topology.gke.io/zone" 39 | 40 | // Volume ID Expected Format 41 | // "projects/{projectName}/zones/{zoneName}/disks/{diskName}" 42 | volIDZonalFmt = "projects/%s/zones/%s/disks/%s" 43 | // "projects/{projectName}/regions/{regionName}/disks/{diskName}" 44 | volIDRegionalFmt = "projects/%s/regions/%s/disks/%s" 45 | volIDProjectValue = 1 46 | volIDRegionalityValue = 2 47 | volIDZoneValue = 3 48 | volIDDiskNameValue = 5 49 | volIDTotalElements = 6 50 | 51 | nodeIDFmt = "projects/%s/zones/%s/instances/%s" 52 | 53 | // UnspecifiedValue is used for an unknown zone string 54 | UnspecifiedValue = "UNSPECIFIED" 55 | ) 56 | 57 | var _ InTreePlugin = &gcePersistentDiskCSITranslator{} 58 | 59 | // gcePersistentDiskCSITranslator handles translation of PV spec from In-tree 60 | // GCE PD to CSI GCE PD and vice versa 61 | type gcePersistentDiskCSITranslator struct{} 62 | 63 | // NewGCEPersistentDiskCSITranslator returns a new instance of gcePersistentDiskTranslator 64 | func NewGCEPersistentDiskCSITranslator() InTreePlugin { 65 | return &gcePersistentDiskCSITranslator{} 66 | } 67 | 68 | func generateToplogySelectors(key string, values []string) []v1.TopologySelectorTerm { 69 | return []v1.TopologySelectorTerm{ 70 | { 71 | MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{ 72 | { 73 | Key: key, 74 | Values: values, 75 | }, 76 | }, 77 | }, 78 | } 79 | } 80 | 81 | // TranslateInTreeStorageClassToCSI translates InTree GCE storage class parameters to CSI storage class 82 | func (g *gcePersistentDiskCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 83 | var generatedTopologies []v1.TopologySelectorTerm 84 | 85 | np := map[string]string{} 86 | for k, v := range sc.Parameters { 87 | switch strings.ToLower(k) { 88 | case fsTypeKey: 89 | // prefixed fstype parameter is stripped out by external provisioner 90 | np[csiFsTypeKey] = v 91 | // Strip out zone and zones parameters and translate them into topologies instead 92 | case zoneKey: 93 | generatedTopologies = generateToplogySelectors(GCEPDTopologyKey, []string{v}) 94 | case zonesKey: 95 | generatedTopologies = generateToplogySelectors(GCEPDTopologyKey, strings.Split(v, ",")) 96 | default: 97 | np[k] = v 98 | } 99 | } 100 | 101 | if len(generatedTopologies) > 0 && len(sc.AllowedTopologies) > 0 { 102 | return nil, fmt.Errorf("cannot simultaneously set allowed topologies and zone/zones parameters") 103 | } else if len(generatedTopologies) > 0 { 104 | sc.AllowedTopologies = generatedTopologies 105 | } else if len(sc.AllowedTopologies) > 0 { 106 | newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, GCEPDTopologyKey) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed translating allowed topologies: %v", err) 109 | } 110 | sc.AllowedTopologies = newTopologies 111 | } 112 | 113 | sc.Parameters = np 114 | 115 | return sc, nil 116 | } 117 | 118 | // backwardCompatibleAccessModes translates all instances of ReadWriteMany 119 | // access mode from the in-tree plugin to ReadWriteOnce. This is because in-tree 120 | // plugin never supported ReadWriteMany but also did not validate or enforce 121 | // this access mode for pre-provisioned volumes. The GCE PD CSI Driver validates 122 | // and enforces (fails) ReadWriteMany. Therefore we treat all in-tree 123 | // ReadWriteMany as ReadWriteOnce volumes to not break legacy volumes. It also 124 | // takes [ReadWriteOnce, ReadOnlyMany] and makes it ReadWriteOnce. This is 125 | // because the in-tree plugin does not enforce access modes and just attaches 126 | // the disk in ReadWriteOnce mode; however, the CSI external-attacher will fail 127 | // this combination because technically [ReadWriteOnce, ReadOnlyMany] is not 128 | // supportable on an attached volume 129 | // See: https://github.com/kubernetes-csi/external-attacher/issues/153 130 | func backwardCompatibleAccessModes(ams []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { 131 | if ams == nil { 132 | return nil 133 | } 134 | 135 | s := map[v1.PersistentVolumeAccessMode]bool{} 136 | var newAM []v1.PersistentVolumeAccessMode 137 | 138 | for _, am := range ams { 139 | if am == v1.ReadWriteMany { 140 | // ReadWriteMany is unsupported in CSI, but in-tree did no 141 | // validation and treated it as ReadWriteOnce 142 | s[v1.ReadWriteOnce] = true 143 | } else { 144 | s[am] = true 145 | } 146 | } 147 | 148 | switch { 149 | case s[v1.ReadOnlyMany] && s[v1.ReadWriteOnce]: 150 | // ROX,RWO is unsupported in CSI, but in-tree did not validation and 151 | // treated it as ReadWriteOnce 152 | newAM = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce} 153 | case s[v1.ReadWriteOnce]: 154 | newAM = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce} 155 | case s[v1.ReadOnlyMany]: 156 | newAM = []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany} 157 | default: 158 | newAM = []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce} 159 | } 160 | 161 | return newAM 162 | } 163 | 164 | // TranslateInTreeInlineVolumeToCSI takes a Volume with GCEPersistentDisk set from in-tree 165 | // and converts the GCEPersistentDisk source to a CSIPersistentVolumeSource 166 | func (g *gcePersistentDiskCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 167 | if volume == nil || volume.GCEPersistentDisk == nil { 168 | return nil, fmt.Errorf("volume is nil or GCE PD not defined on volume") 169 | } 170 | 171 | pdSource := volume.GCEPersistentDisk 172 | 173 | partition := "" 174 | if pdSource.Partition != 0 { 175 | partition = strconv.Itoa(int(pdSource.Partition)) 176 | } 177 | 178 | var am v1.PersistentVolumeAccessMode 179 | if pdSource.ReadOnly { 180 | am = v1.ReadOnlyMany 181 | } else { 182 | am = v1.ReadWriteOnce 183 | } 184 | 185 | fsMode := v1.PersistentVolumeFilesystem 186 | return &v1.PersistentVolume{ 187 | ObjectMeta: metav1.ObjectMeta{ 188 | // Must be unique per disk as it is used as the unique part of the 189 | // staging path 190 | Name: fmt.Sprintf("%s-%s", GCEPDDriverName, pdSource.PDName), 191 | }, 192 | Spec: v1.PersistentVolumeSpec{ 193 | PersistentVolumeSource: v1.PersistentVolumeSource{ 194 | CSI: &v1.CSIPersistentVolumeSource{ 195 | Driver: GCEPDDriverName, 196 | VolumeHandle: fmt.Sprintf(volIDZonalFmt, UnspecifiedValue, UnspecifiedValue, pdSource.PDName), 197 | ReadOnly: pdSource.ReadOnly, 198 | FSType: pdSource.FSType, 199 | VolumeAttributes: map[string]string{ 200 | "partition": partition, 201 | }, 202 | }, 203 | }, 204 | AccessModes: []v1.PersistentVolumeAccessMode{am}, 205 | VolumeMode: &fsMode, 206 | }, 207 | }, nil 208 | } 209 | 210 | // TranslateInTreePVToCSI takes a PV with GCEPersistentDisk set from in-tree 211 | // and converts the GCEPersistentDisk source to a CSIPersistentVolumeSource 212 | func (g *gcePersistentDiskCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 213 | var volID string 214 | 215 | if pv == nil || pv.Spec.GCEPersistentDisk == nil { 216 | return nil, fmt.Errorf("pv is nil or GCE Persistent Disk source not defined on pv") 217 | } 218 | 219 | // depend on which version it migrates from, the label could be failuredomain beta or topology GA version 220 | zonesLabel := pv.Labels[v1.LabelFailureDomainBetaZone] 221 | if zonesLabel == "" { 222 | zonesLabel = pv.Labels[v1.LabelTopologyZone] 223 | } 224 | 225 | zones := strings.Split(zonesLabel, labelMultiZoneDelimiter) 226 | if len(zones) == 1 && len(zones[0]) != 0 { 227 | // Zonal 228 | volID = fmt.Sprintf(volIDZonalFmt, UnspecifiedValue, zones[0], pv.Spec.GCEPersistentDisk.PDName) 229 | } else if len(zones) > 1 { 230 | // Regional 231 | region, err := gceGetRegionFromZones(zones) 232 | if err != nil { 233 | return nil, fmt.Errorf("failed to get region from zones: %v", err) 234 | } 235 | volID = fmt.Sprintf(volIDRegionalFmt, UnspecifiedValue, region, pv.Spec.GCEPersistentDisk.PDName) 236 | } else { 237 | // Unspecified 238 | volID = fmt.Sprintf(volIDZonalFmt, UnspecifiedValue, UnspecifiedValue, pv.Spec.GCEPersistentDisk.PDName) 239 | } 240 | 241 | gceSource := pv.Spec.PersistentVolumeSource.GCEPersistentDisk 242 | 243 | partition := "" 244 | if gceSource.Partition != 0 { 245 | partition = strconv.Itoa(int(gceSource.Partition)) 246 | } 247 | 248 | csiSource := &v1.CSIPersistentVolumeSource{ 249 | Driver: GCEPDDriverName, 250 | VolumeHandle: volID, 251 | ReadOnly: gceSource.ReadOnly, 252 | FSType: gceSource.FSType, 253 | VolumeAttributes: map[string]string{ 254 | "partition": partition, 255 | }, 256 | } 257 | 258 | if err := translateTopologyFromInTreeToCSI(pv, GCEPDTopologyKey); err != nil { 259 | return nil, fmt.Errorf("failed to translate topology: %v", err) 260 | } 261 | 262 | pv.Spec.PersistentVolumeSource.GCEPersistentDisk = nil 263 | pv.Spec.PersistentVolumeSource.CSI = csiSource 264 | pv.Spec.AccessModes = backwardCompatibleAccessModes(pv.Spec.AccessModes) 265 | 266 | return pv, nil 267 | } 268 | 269 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 270 | // translates the GCE PD CSI source to a GCEPersistentDisk source. 271 | func (g *gcePersistentDiskCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 272 | if pv == nil || pv.Spec.CSI == nil { 273 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 274 | } 275 | csiSource := pv.Spec.CSI 276 | 277 | pdName, err := pdNameFromVolumeID(csiSource.VolumeHandle) 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | gceSource := &v1.GCEPersistentDiskVolumeSource{ 283 | PDName: pdName, 284 | FSType: csiSource.FSType, 285 | ReadOnly: csiSource.ReadOnly, 286 | } 287 | if partition, ok := csiSource.VolumeAttributes["partition"]; ok && partition != "" { 288 | partInt, err := strconv.Atoi(partition) 289 | if err != nil { 290 | return nil, fmt.Errorf("Failed to convert partition %v to integer: %v", partition, err) 291 | } 292 | gceSource.Partition = int32(partInt) 293 | } 294 | 295 | // translate CSI topology to In-tree topology for rollback compatibility 296 | if err := translateTopologyFromCSIToInTree(pv, GCEPDTopologyKey, gceGetRegionFromZones); err != nil { 297 | return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err) 298 | } 299 | 300 | pv.Spec.CSI = nil 301 | pv.Spec.GCEPersistentDisk = gceSource 302 | 303 | return pv, nil 304 | } 305 | 306 | // CanSupport tests whether the plugin supports a given persistent volume 307 | // specification from the API. The spec pointer should be considered 308 | // const. 309 | func (g *gcePersistentDiskCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 310 | return pv != nil && pv.Spec.GCEPersistentDisk != nil 311 | } 312 | 313 | // CanSupportInline tests whether the plugin supports a given inline volume 314 | // specification from the API. The spec pointer should be considered 315 | // const. 316 | func (g *gcePersistentDiskCSITranslator) CanSupportInline(volume *v1.Volume) bool { 317 | return volume != nil && volume.GCEPersistentDisk != nil 318 | } 319 | 320 | // GetInTreePluginName returns the name of the intree plugin driver 321 | func (g *gcePersistentDiskCSITranslator) GetInTreePluginName() string { 322 | return GCEPDInTreePluginName 323 | } 324 | 325 | // GetCSIPluginName returns the name of the CSI plugin 326 | func (g *gcePersistentDiskCSITranslator) GetCSIPluginName() string { 327 | return GCEPDDriverName 328 | } 329 | 330 | // RepairVolumeHandle returns a fully specified volume handle by inferring 331 | // project, zone/region from the node ID if the volume handle has UNSPECIFIED 332 | // sections 333 | func (g *gcePersistentDiskCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 334 | var err error 335 | tok := strings.Split(volumeHandle, "/") 336 | if len(tok) < volIDTotalElements { 337 | return "", fmt.Errorf("volume handle has wrong number of elements; got %v, wanted %v or more", len(tok), volIDTotalElements) 338 | } 339 | if tok[volIDProjectValue] != UnspecifiedValue { 340 | return volumeHandle, nil 341 | } 342 | 343 | nodeTok := strings.Split(nodeID, "/") 344 | if len(nodeTok) < volIDTotalElements { 345 | return "", fmt.Errorf("node handle has wrong number of elements; got %v, wanted %v or more", len(nodeTok), volIDTotalElements) 346 | } 347 | 348 | switch tok[volIDRegionalityValue] { 349 | case "zones": 350 | zone := "" 351 | if tok[volIDZoneValue] == UnspecifiedValue { 352 | zone = nodeTok[volIDZoneValue] 353 | } else { 354 | zone = tok[volIDZoneValue] 355 | } 356 | return fmt.Sprintf(volIDZonalFmt, nodeTok[volIDProjectValue], zone, tok[volIDDiskNameValue]), nil 357 | case "regions": 358 | region := "" 359 | if tok[volIDZoneValue] == UnspecifiedValue { 360 | region, err = gceGetRegionFromZones([]string{nodeTok[volIDZoneValue]}) 361 | if err != nil { 362 | return "", fmt.Errorf("failed to get region from zone %s: %v", nodeTok[volIDZoneValue], err) 363 | } 364 | } else { 365 | region = tok[volIDZoneValue] 366 | } 367 | return fmt.Sprintf(volIDRegionalFmt, nodeTok[volIDProjectValue], region, tok[volIDDiskNameValue]), nil 368 | default: 369 | return "", fmt.Errorf("expected volume handle to have zones or regions regionality value, got: %s", tok[volIDRegionalityValue]) 370 | } 371 | } 372 | 373 | func pdNameFromVolumeID(id string) (string, error) { 374 | splitID := strings.Split(id, "/") 375 | if len(splitID) < volIDTotalElements { 376 | return "", fmt.Errorf("failed to get id components.Got: %v, wanted %v components or more. ", len(splitID), volIDTotalElements) 377 | } 378 | return splitID[volIDDiskNameValue], nil 379 | } 380 | 381 | // TODO: Replace this with the imported one from GCE PD CSI Driver when 382 | // the driver removes all k8s/k8s dependencies 383 | func gceGetRegionFromZones(zones []string) (string, error) { 384 | regions := sets.String{} 385 | if len(zones) < 1 { 386 | return "", fmt.Errorf("no zones specified") 387 | } 388 | for _, zone := range zones { 389 | // Zone expected format {locale}-{region}-{zone} 390 | splitZone := strings.Split(zone, "-") 391 | if len(splitZone) != 3 { 392 | return "", fmt.Errorf("zone in unexpected format, expected: {locale}-{region}-{zone}, got: %v", zone) 393 | } 394 | regions.Insert(strings.Join(splitZone[0:2], "-")) 395 | } 396 | if regions.Len() != 1 { 397 | return "", fmt.Errorf("multiple or no regions gotten from zones, got: %v", regions) 398 | } 399 | return regions.UnsortedList()[0], nil 400 | } 401 | -------------------------------------------------------------------------------- /plugins/gce_pd_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | storage "k8s.io/api/storage/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/klog/v2/ktesting" 28 | _ "k8s.io/klog/v2/ktesting/init" 29 | ) 30 | 31 | func NewStorageClass(params map[string]string, allowedTopologies []v1.TopologySelectorTerm) *storage.StorageClass { 32 | return &storage.StorageClass{ 33 | Parameters: params, 34 | AllowedTopologies: allowedTopologies, 35 | } 36 | } 37 | 38 | func TestTranslatePDInTreeStorageClassToCSI(t *testing.T) { 39 | g := NewGCEPersistentDiskCSITranslator() 40 | logger, _ := ktesting.NewTestContext(t) 41 | 42 | tcs := []struct { 43 | name string 44 | options *storage.StorageClass 45 | expOptions *storage.StorageClass 46 | expErr bool 47 | }{ 48 | { 49 | name: "nothing special", 50 | options: NewStorageClass(map[string]string{"foo": "bar"}, nil), 51 | expOptions: NewStorageClass(map[string]string{"foo": "bar"}, nil), 52 | }, 53 | { 54 | name: "fstype", 55 | options: NewStorageClass(map[string]string{"fstype": "myfs"}, nil), 56 | expOptions: NewStorageClass(map[string]string{"csi.storage.k8s.io/fstype": "myfs"}, nil), 57 | }, 58 | { 59 | name: "empty params", 60 | options: NewStorageClass(map[string]string{}, nil), 61 | expOptions: NewStorageClass(map[string]string{}, nil), 62 | }, 63 | { 64 | name: "zone", 65 | options: NewStorageClass(map[string]string{"zone": "foo"}, nil), 66 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo"})), 67 | }, 68 | { 69 | name: "zones", 70 | options: NewStorageClass(map[string]string{"zones": "foo,bar,baz"}, nil), 71 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo", "bar", "baz"})), 72 | }, 73 | { 74 | name: "some normal topology", 75 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo"})), 76 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo"})), 77 | }, 78 | { 79 | name: "some translated topology", 80 | options: NewStorageClass(map[string]string{}, generateToplogySelectors(v1.LabelFailureDomainBetaZone, []string{"foo"})), 81 | expOptions: NewStorageClass(map[string]string{}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo"})), 82 | }, 83 | { 84 | name: "zone and topology", 85 | options: NewStorageClass(map[string]string{"zone": "foo"}, generateToplogySelectors(GCEPDTopologyKey, []string{"foo"})), 86 | expErr: true, 87 | }, 88 | } 89 | 90 | for _, tc := range tcs { 91 | t.Logf("Testing %v", tc.name) 92 | gotOptions, err := g.TranslateInTreeStorageClassToCSI(logger, tc.options) 93 | if err != nil && !tc.expErr { 94 | t.Errorf("Did not expect error but got: %v", err) 95 | } 96 | if err == nil && tc.expErr { 97 | t.Errorf("Expected error, but did not get one.") 98 | } 99 | if !reflect.DeepEqual(gotOptions, tc.expOptions) { 100 | t.Errorf("Got parameters: %v, expected :%v", gotOptions, tc.expOptions) 101 | } 102 | } 103 | } 104 | 105 | func TestRepairVolumeHandle(t *testing.T) { 106 | testCases := []struct { 107 | name string 108 | volumeHandle string 109 | nodeID string 110 | expectedVolumeHandle string 111 | expectedErr bool 112 | }{ 113 | { 114 | name: "fully specified", 115 | volumeHandle: fmt.Sprintf(volIDZonalFmt, "foo", "bar", "baz"), 116 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "bada", "boom"), 117 | expectedVolumeHandle: fmt.Sprintf(volIDZonalFmt, "foo", "bar", "baz"), 118 | }, 119 | { 120 | name: "fully specified (regional)", 121 | volumeHandle: fmt.Sprintf(volIDRegionalFmt, "foo", "us-central1-c", "baz"), 122 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "bada", "boom"), 123 | expectedVolumeHandle: fmt.Sprintf(volIDRegionalFmt, "foo", "us-central1-c", "baz"), 124 | }, 125 | { 126 | name: "no project", 127 | volumeHandle: fmt.Sprintf(volIDZonalFmt, UnspecifiedValue, "bar", "baz"), 128 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "bada", "boom"), 129 | expectedVolumeHandle: fmt.Sprintf(volIDZonalFmt, "bing", "bar", "baz"), 130 | }, 131 | { 132 | name: "no project or zone", 133 | volumeHandle: fmt.Sprintf(volIDZonalFmt, UnspecifiedValue, UnspecifiedValue, "baz"), 134 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "bada", "boom"), 135 | expectedVolumeHandle: fmt.Sprintf(volIDZonalFmt, "bing", "bada", "baz"), 136 | }, 137 | { 138 | name: "no project or region", 139 | volumeHandle: fmt.Sprintf(volIDRegionalFmt, UnspecifiedValue, UnspecifiedValue, "baz"), 140 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "us-central1-c", "boom"), 141 | expectedVolumeHandle: fmt.Sprintf(volIDRegionalFmt, "bing", "us-central1", "baz"), 142 | }, 143 | { 144 | name: "no project (regional)", 145 | volumeHandle: fmt.Sprintf(volIDRegionalFmt, UnspecifiedValue, "us-west1", "baz"), 146 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "us-central1-c", "boom"), 147 | expectedVolumeHandle: fmt.Sprintf(volIDRegionalFmt, "bing", "us-west1", "baz"), 148 | }, 149 | { 150 | name: "invalid handle", 151 | volumeHandle: "foo", 152 | nodeID: fmt.Sprintf(nodeIDFmt, "bing", "us-central1-c", "boom"), 153 | expectedErr: true, 154 | }, 155 | { 156 | name: "invalid node ID", 157 | volumeHandle: fmt.Sprintf(volIDRegionalFmt, UnspecifiedValue, "us-west1", "baz"), 158 | nodeID: "foo", 159 | expectedErr: true, 160 | }, 161 | } 162 | g := NewGCEPersistentDiskCSITranslator() 163 | for _, tc := range testCases { 164 | t.Run(tc.name, func(t *testing.T) { 165 | gotVolumeHandle, err := g.RepairVolumeHandle(tc.volumeHandle, tc.nodeID) 166 | if err != nil && !tc.expectedErr { 167 | if !tc.expectedErr { 168 | t.Fatalf("Got error: %v, but expected none", err) 169 | } 170 | return 171 | } 172 | if err == nil && tc.expectedErr { 173 | t.Fatal("Got no error, but expected one") 174 | } 175 | 176 | if gotVolumeHandle != tc.expectedVolumeHandle { 177 | t.Fatalf("Got volume handle %s, but expected %s", gotVolumeHandle, tc.expectedVolumeHandle) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestBackwardCompatibleAccessModes(t *testing.T) { 184 | testCases := []struct { 185 | name string 186 | accessModes []v1.PersistentVolumeAccessMode 187 | expAccessModes []v1.PersistentVolumeAccessMode 188 | }{ 189 | { 190 | name: "ROX", 191 | accessModes: []v1.PersistentVolumeAccessMode{ 192 | v1.ReadOnlyMany, 193 | }, 194 | expAccessModes: []v1.PersistentVolumeAccessMode{ 195 | v1.ReadOnlyMany, 196 | }, 197 | }, 198 | { 199 | name: "RWO", 200 | accessModes: []v1.PersistentVolumeAccessMode{ 201 | v1.ReadWriteOnce, 202 | }, 203 | expAccessModes: []v1.PersistentVolumeAccessMode{ 204 | v1.ReadWriteOnce, 205 | }, 206 | }, 207 | { 208 | name: "RWX", 209 | accessModes: []v1.PersistentVolumeAccessMode{ 210 | v1.ReadWriteMany, 211 | }, 212 | expAccessModes: []v1.PersistentVolumeAccessMode{ 213 | v1.ReadWriteOnce, 214 | }, 215 | }, 216 | { 217 | name: "RWO, ROX", 218 | accessModes: []v1.PersistentVolumeAccessMode{ 219 | v1.ReadOnlyMany, 220 | v1.ReadWriteOnce, 221 | }, 222 | expAccessModes: []v1.PersistentVolumeAccessMode{ 223 | v1.ReadWriteOnce, 224 | }, 225 | }, 226 | { 227 | name: "RWO, RWX", 228 | accessModes: []v1.PersistentVolumeAccessMode{ 229 | v1.ReadWriteOnce, 230 | v1.ReadWriteMany, 231 | }, 232 | expAccessModes: []v1.PersistentVolumeAccessMode{ 233 | v1.ReadWriteOnce, 234 | }, 235 | }, 236 | { 237 | name: "RWX, ROX", 238 | accessModes: []v1.PersistentVolumeAccessMode{ 239 | v1.ReadWriteMany, 240 | v1.ReadOnlyMany, 241 | }, 242 | expAccessModes: []v1.PersistentVolumeAccessMode{ 243 | v1.ReadWriteOnce, 244 | }, 245 | }, 246 | { 247 | name: "RWX, ROX, RWO", 248 | accessModes: []v1.PersistentVolumeAccessMode{ 249 | v1.ReadWriteMany, 250 | v1.ReadWriteOnce, 251 | v1.ReadOnlyMany, 252 | }, 253 | expAccessModes: []v1.PersistentVolumeAccessMode{ 254 | v1.ReadWriteOnce, 255 | }, 256 | }, 257 | } 258 | 259 | for _, tc := range testCases { 260 | t.Logf("running test: %v", tc.name) 261 | 262 | got := backwardCompatibleAccessModes(tc.accessModes) 263 | 264 | if !reflect.DeepEqual(tc.expAccessModes, got) { 265 | t.Fatalf("Expected access modes: %v, instead got: %v", tc.expAccessModes, got) 266 | } 267 | } 268 | } 269 | 270 | func TestInlineReadOnly(t *testing.T) { 271 | g := NewGCEPersistentDiskCSITranslator() 272 | logger, _ := ktesting.NewTestContext(t) 273 | pv, err := g.TranslateInTreeInlineVolumeToCSI(logger, &v1.Volume{ 274 | VolumeSource: v1.VolumeSource{ 275 | GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 276 | PDName: "foo", 277 | ReadOnly: true, 278 | }, 279 | }, 280 | }, "") 281 | if err != nil { 282 | t.Fatalf("Failed to translate in tree inline volume to CSI: %v", err) 283 | } 284 | 285 | if pv == nil || pv.Spec.PersistentVolumeSource.CSI == nil { 286 | t.Fatal("PV or volume source unexpectedly nil") 287 | } 288 | 289 | if !pv.Spec.PersistentVolumeSource.CSI.ReadOnly { 290 | t.Error("PV readonly value not true") 291 | } 292 | 293 | ams := pv.Spec.AccessModes 294 | if len(ams) != 1 { 295 | t.Errorf("got am %v, expected length of 1", ams) 296 | } 297 | 298 | if ams[0] != v1.ReadOnlyMany { 299 | t.Errorf("got am %v, expected access mode of ReadOnlyMany", ams[0]) 300 | } 301 | } 302 | 303 | func TestTranslateInTreePVToCSIVolIDFmt(t *testing.T) { 304 | g := NewGCEPersistentDiskCSITranslator() 305 | logger, _ := ktesting.NewTestContext(t) 306 | pdName := "pd-name" 307 | tests := []struct { 308 | desc string 309 | topologyLabelKey string 310 | topologyLabelValue string 311 | wantVolId string 312 | }{ 313 | { 314 | desc: "beta topology key zonal", 315 | topologyLabelKey: v1.LabelFailureDomainBetaZone, 316 | topologyLabelValue: "us-east1-a", 317 | wantVolId: "projects/UNSPECIFIED/zones/us-east1-a/disks/pd-name", 318 | }, 319 | { 320 | desc: "v1 topology key zonal", 321 | topologyLabelKey: v1.LabelTopologyZone, 322 | topologyLabelValue: "us-east1-a", 323 | wantVolId: "projects/UNSPECIFIED/zones/us-east1-a/disks/pd-name", 324 | }, 325 | { 326 | desc: "beta topology key regional", 327 | topologyLabelKey: v1.LabelFailureDomainBetaZone, 328 | topologyLabelValue: "us-central1-a__us-central1-c", 329 | wantVolId: "projects/UNSPECIFIED/regions/us-central1/disks/pd-name", 330 | }, 331 | { 332 | desc: "v1 topology key regional", 333 | topologyLabelKey: v1.LabelTopologyZone, 334 | topologyLabelValue: "us-central1-a__us-central1-c", 335 | wantVolId: "projects/UNSPECIFIED/regions/us-central1/disks/pd-name", 336 | }, 337 | } 338 | for _, tc := range tests { 339 | t.Run(tc.desc, func(t *testing.T) { 340 | translatedPV, err := g.TranslateInTreePVToCSI(logger, &v1.PersistentVolume{ 341 | ObjectMeta: metav1.ObjectMeta{ 342 | Labels: map[string]string{tc.topologyLabelKey: tc.topologyLabelValue}, 343 | }, 344 | Spec: v1.PersistentVolumeSpec{ 345 | PersistentVolumeSource: v1.PersistentVolumeSource{ 346 | GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 347 | PDName: pdName, 348 | }, 349 | }, 350 | }, 351 | }) 352 | if err != nil { 353 | t.Errorf("got error translating in-tree PV to CSI: %v", err) 354 | } 355 | if got := translatedPV.Spec.PersistentVolumeSource.CSI.VolumeHandle; got != tc.wantVolId { 356 | t.Errorf("got translated volume handle: %q, want %q", got, tc.wantVolId) 357 | } 358 | }) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /plugins/in_tree_volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "sort" 23 | "strings" 24 | 25 | v1 "k8s.io/api/core/v1" 26 | storage "k8s.io/api/storage/v1" 27 | "k8s.io/apimachinery/pkg/util/sets" 28 | "k8s.io/klog/v2" 29 | ) 30 | 31 | // InTreePlugin handles translations between CSI and in-tree sources in a PV 32 | type InTreePlugin interface { 33 | 34 | // TranslateInTreeStorageClassToCSI takes in-tree volume options 35 | // and translates them to a volume options consumable by CSI plugin 36 | TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) 37 | 38 | // TranslateInTreeInlineVolumeToCSI takes a inline volume and will translate 39 | // the in-tree inline volume source to a CSIPersistentVolumeSource 40 | // A PV object containing the CSIPersistentVolumeSource in it's spec is returned 41 | // podNamespace is only needed for azurefile to fetch secret namespace, no need to be set for other plugins. 42 | TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) 43 | 44 | // TranslateInTreePVToCSI takes a persistent volume and will translate 45 | // the in-tree pv source to a CSI Source. The input persistent volume can be modified 46 | TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) 47 | 48 | // TranslateCSIPVToInTree takes a PV with a CSI PersistentVolume Source and will translate 49 | // it to a in-tree Persistent Volume Source for the in-tree volume 50 | // by the `Driver` field in the CSI Source. The input PV object can be modified 51 | TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) 52 | 53 | // CanSupport tests whether the plugin supports a given persistent volume 54 | // specification from the API. 55 | CanSupport(pv *v1.PersistentVolume) bool 56 | 57 | // CanSupportInline tests whether the plugin supports a given inline volume 58 | // specification from the API. 59 | CanSupportInline(vol *v1.Volume) bool 60 | 61 | // GetInTreePluginName returns the in-tree plugin name this migrates 62 | GetInTreePluginName() string 63 | 64 | // GetCSIPluginName returns the name of the CSI plugin that supersedes the in-tree plugin 65 | GetCSIPluginName() string 66 | 67 | // RepairVolumeHandle generates a correct volume handle based on node ID information. 68 | RepairVolumeHandle(volumeHandle, nodeID string) (string, error) 69 | } 70 | 71 | const ( 72 | // fsTypeKey is the deprecated storage class parameter key for fstype 73 | fsTypeKey = "fstype" 74 | // csiFsTypeKey is the storage class parameter key for CSI fstype 75 | csiFsTypeKey = "csi.storage.k8s.io/fstype" 76 | // zoneKey is the deprecated storage class parameter key for zone 77 | zoneKey = "zone" 78 | // zonesKey is the deprecated storage class parameter key for zones 79 | zonesKey = "zones" 80 | ) 81 | 82 | // replaceTopology overwrites an existing key in NodeAffinity by a new one. 83 | // If there are any newKey already exist in an expression of a term, we will 84 | // not combine the replaced key Values with the existing ones. 85 | // So there might be duplication if there is any newKey expression 86 | // already in the terms. 87 | func replaceTopology(pv *v1.PersistentVolume, oldKey, newKey string) error { 88 | // Make sure the necessary fields exist 89 | if pv == nil || pv.Spec.NodeAffinity == nil || pv.Spec.NodeAffinity.Required == nil || 90 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms == nil || len(pv.Spec.NodeAffinity.Required.NodeSelectorTerms) == 0 { 91 | return nil 92 | } 93 | for i := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms { 94 | for j, r := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms[i].MatchExpressions { 95 | if r.Key == oldKey { 96 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms[i].MatchExpressions[j].Key = newKey 97 | } 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | // getTopologyValues returns all unique topology values with the given key found in 105 | // the PV NodeAffinity. Sort by alphabetical order. 106 | // This function collapses multiple zones into a list that is ORed. This assumes that 107 | // the plugin does not support a constraint like key in "zone1" AND "zone2" 108 | func getTopologyValues(pv *v1.PersistentVolume, key string) []string { 109 | if pv.Spec.NodeAffinity == nil || 110 | pv.Spec.NodeAffinity.Required == nil || 111 | len(pv.Spec.NodeAffinity.Required.NodeSelectorTerms) < 1 { 112 | return nil 113 | } 114 | 115 | values := make(map[string]bool) 116 | for i := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms { 117 | for _, r := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms[i].MatchExpressions { 118 | if r.Key == key { 119 | for _, v := range r.Values { 120 | values[v] = true 121 | } 122 | } 123 | } 124 | } 125 | // remove duplication and sort them in order for better usage 126 | var re []string 127 | for k := range values { 128 | re = append(re, k) 129 | } 130 | sort.Strings(re) 131 | return re 132 | } 133 | 134 | // addTopology appends the topology to the given PV to all Terms. 135 | func addTopology(pv *v1.PersistentVolume, topologyKey string, zones []string) error { 136 | // Make sure there are no duplicate or empty strings 137 | filteredZones := sets.String{} 138 | for i := range zones { 139 | zone := strings.TrimSpace(zones[i]) 140 | if len(zone) > 0 { 141 | filteredZones.Insert(zone) 142 | } 143 | } 144 | 145 | zones = filteredZones.List() 146 | if len(zones) < 1 { 147 | return errors.New("there are no valid zones to add to pv") 148 | } 149 | 150 | // Make sure the necessary fields exist 151 | if pv.Spec.NodeAffinity == nil { 152 | pv.Spec.NodeAffinity = new(v1.VolumeNodeAffinity) 153 | } 154 | 155 | if pv.Spec.NodeAffinity.Required == nil { 156 | pv.Spec.NodeAffinity.Required = new(v1.NodeSelector) 157 | } 158 | 159 | if len(pv.Spec.NodeAffinity.Required.NodeSelectorTerms) == 0 { 160 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms = make([]v1.NodeSelectorTerm, 1) 161 | } 162 | 163 | topology := v1.NodeSelectorRequirement{ 164 | Key: topologyKey, 165 | Operator: v1.NodeSelectorOpIn, 166 | Values: zones, 167 | } 168 | 169 | // add the CSI topology to each term 170 | for i := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms { 171 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms[i].MatchExpressions = append( 172 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms[i].MatchExpressions, 173 | topology, 174 | ) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // translateTopologyFromInTreeToCSI converts existing zone labels or in-tree topology to CSI topology. 181 | // In-tree topology has precedence over zone labels. When both in-tree topology and zone labels exist 182 | // for a particular CSI topology, in-tree topology will be used. 183 | // This function will remove the Beta version Kubernetes topology label in case the node upgrade to a 184 | // newer version where it does not have any Beta topology label anymore 185 | func translateTopologyFromInTreeToCSI(pv *v1.PersistentVolume, csiTopologyKey string) error { 186 | 187 | zoneLabel, regionLabel := getTopologyLabel(pv) 188 | 189 | // If Zone kubernetes topology exist, replace it to use csiTopologyKey 190 | zones := getTopologyValues(pv, zoneLabel) 191 | if len(zones) > 0 { 192 | replaceTopology(pv, zoneLabel, csiTopologyKey) 193 | } else { 194 | // if nothing is in the NodeAffinity, try to fetch the topology from PV labels 195 | if label, ok := pv.Labels[zoneLabel]; ok { 196 | zones = strings.Split(label, labelMultiZoneDelimiter) 197 | if len(zones) > 0 { 198 | addTopology(pv, csiTopologyKey, zones) 199 | } 200 | } 201 | } 202 | 203 | // if the in-tree PV has beta region label, replace it with GA label to ensure 204 | // the scheduler is able to schedule it on new nodes with only GA kubernetes label 205 | // No need to check it for zone label because it has already been replaced if exist 206 | if regionLabel == v1.LabelFailureDomainBetaRegion { 207 | regions := getTopologyValues(pv, regionLabel) 208 | if len(regions) > 0 { 209 | replaceTopology(pv, regionLabel, v1.LabelTopologyRegion) 210 | } 211 | } 212 | 213 | return nil 214 | } 215 | 216 | // getTopologyLabel checks if the kubernetes topology label used in this 217 | // PV is GA and return the zone/region label used. 218 | // The version checking follows the following orders 219 | // 1. Check NodeAffinity 220 | // 1.1 Check if zoneGA exists, if yes return GA labels 221 | // 1.2 Check if zoneBeta exists, if yes return Beta labels 222 | // 2. Check PV labels 223 | // 2.1 Check if zoneGA exists, if yes return GA labels 224 | // 2.2 Check if zoneBeta exists, if yes return Beta labels 225 | func getTopologyLabel(pv *v1.PersistentVolume) (zoneLabel string, regionLabel string) { 226 | 227 | if zoneGA := TopologyKeyExist(v1.LabelTopologyZone, pv.Spec.NodeAffinity); zoneGA { 228 | return v1.LabelTopologyZone, v1.LabelTopologyRegion 229 | } 230 | if zoneBeta := TopologyKeyExist(v1.LabelFailureDomainBetaZone, pv.Spec.NodeAffinity); zoneBeta { 231 | return v1.LabelFailureDomainBetaZone, v1.LabelFailureDomainBetaRegion 232 | } 233 | if _, zoneGA := pv.Labels[v1.LabelTopologyZone]; zoneGA { 234 | return v1.LabelTopologyZone, v1.LabelTopologyRegion 235 | } 236 | if _, zoneBeta := pv.Labels[v1.LabelFailureDomainBetaZone]; zoneBeta { 237 | return v1.LabelFailureDomainBetaZone, v1.LabelFailureDomainBetaRegion 238 | } 239 | // No labels or NodeAffinity exist, default to GA version 240 | return v1.LabelTopologyZone, v1.LabelTopologyRegion 241 | } 242 | 243 | // TopologyKeyExist checks if a certain key exists in a VolumeNodeAffinity 244 | func TopologyKeyExist(key string, vna *v1.VolumeNodeAffinity) bool { 245 | if vna == nil || vna.Required == nil || vna.Required.NodeSelectorTerms == nil || len(vna.Required.NodeSelectorTerms) == 0 { 246 | return false 247 | } 248 | 249 | for _, nodeSelectorTerms := range vna.Required.NodeSelectorTerms { 250 | nsrequirements := nodeSelectorTerms.MatchExpressions 251 | for _, nodeSelectorRequirement := range nsrequirements { 252 | if nodeSelectorRequirement.Key == key { 253 | return true 254 | } 255 | } 256 | } 257 | return false 258 | } 259 | 260 | type regionParserFn func([]string) (string, error) 261 | 262 | // translateTopologyFromCSIToInTree translate a CSI topology to 263 | // Kubernetes topology and add topology labels to it. Note that this function 264 | // will only work for plugin with a single topologyKey that translates to 265 | // Kubernetes zone(and region if regionParser is passed in). 266 | // If a plugin has more than one topologyKey, it will need to be processed 267 | // separately by the plugin. 268 | // If regionParser is nil, no region NodeAffinity will be added. If not nil, 269 | // it'll be passed to regionTopologyHandler, which will add region topology NodeAffinity 270 | // and labels for the given PV. It assumes the Zone NodeAffinity already exists. 271 | // In short this function will, 272 | // 1. Replace all CSI topology to Kubernetes Zone topology label 273 | // 2. Process and generate region topology if a regionParser is passed 274 | // 3. Add Kubernetes Topology labels(zone) if they do not exist 275 | func translateTopologyFromCSIToInTree(pv *v1.PersistentVolume, csiTopologyKey string, regionParser regionParserFn) error { 276 | 277 | zoneLabel, _ := getTopologyLabel(pv) 278 | 279 | // 1. Replace all CSI topology to Kubernetes Zone label 280 | err := replaceTopology(pv, csiTopologyKey, zoneLabel) 281 | if err != nil { 282 | return fmt.Errorf("Failed to replace CSI topology to Kubernetes topology, error: %v", err) 283 | } 284 | 285 | // 2. Take care of region topology if a regionParser is passed 286 | if regionParser != nil { 287 | // let's make less strict on this one. Even if there is an error in the region processing, just ignore it 288 | err = regionTopologyHandler(pv, regionParser) 289 | if err != nil { 290 | return fmt.Errorf("Failed to handle region topology. error: %v", err) 291 | } 292 | } 293 | 294 | // 3. Add labels about Kubernetes Topology 295 | zoneVals := getTopologyValues(pv, zoneLabel) 296 | if len(zoneVals) > 0 { 297 | if pv.Labels == nil { 298 | pv.Labels = make(map[string]string) 299 | } 300 | _, zoneOK := pv.Labels[zoneLabel] 301 | if !zoneOK { 302 | zoneValStr := strings.Join(zoneVals, labelMultiZoneDelimiter) 303 | pv.Labels[zoneLabel] = zoneValStr 304 | } 305 | } 306 | 307 | return nil 308 | } 309 | 310 | // translateAllowedTopologies translates allowed topologies within storage class or PV 311 | // from legacy failure domain to given CSI topology key 312 | func translateAllowedTopologies(terms []v1.TopologySelectorTerm, key string) ([]v1.TopologySelectorTerm, error) { 313 | if terms == nil { 314 | return nil, nil 315 | } 316 | 317 | newTopologies := []v1.TopologySelectorTerm{} 318 | for _, term := range terms { 319 | newTerm := v1.TopologySelectorTerm{} 320 | for _, exp := range term.MatchLabelExpressions { 321 | var newExp v1.TopologySelectorLabelRequirement 322 | if exp.Key == v1.LabelFailureDomainBetaZone || exp.Key == v1.LabelTopologyZone { 323 | newExp = v1.TopologySelectorLabelRequirement{ 324 | Key: key, 325 | Values: exp.Values, 326 | } 327 | } else { 328 | // Other topologies are passed through unchanged. 329 | newExp = exp 330 | } 331 | newTerm.MatchLabelExpressions = append(newTerm.MatchLabelExpressions, newExp) 332 | } 333 | newTopologies = append(newTopologies, newTerm) 334 | } 335 | return newTopologies, nil 336 | } 337 | 338 | // regionTopologyHandler will process the PV and add region 339 | // kubernetes topology label to its NodeAffinity and labels 340 | // It assumes the Zone NodeAffinity already exists 341 | // Each provider is responsible for providing their own regionParser 342 | func regionTopologyHandler(pv *v1.PersistentVolume, regionParser regionParserFn) error { 343 | 344 | // Make sure the necessary fields exist 345 | if pv == nil || pv.Spec.NodeAffinity == nil || pv.Spec.NodeAffinity.Required == nil || 346 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms == nil || len(pv.Spec.NodeAffinity.Required.NodeSelectorTerms) == 0 { 347 | return nil 348 | } 349 | 350 | zoneLabel, regionLabel := getTopologyLabel(pv) 351 | 352 | // process each term 353 | for index, nodeSelectorTerm := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms { 354 | // In the first loop, see if regionLabel already exist 355 | regionExist := false 356 | var zoneVals []string 357 | for _, nsRequirement := range nodeSelectorTerm.MatchExpressions { 358 | if nsRequirement.Key == regionLabel { 359 | regionExist = true 360 | break 361 | } else if nsRequirement.Key == zoneLabel { 362 | zoneVals = append(zoneVals, nsRequirement.Values...) 363 | } 364 | } 365 | if regionExist { 366 | // Regionlabel already exist in this term, skip it 367 | continue 368 | } 369 | // If no regionLabel found, generate region label from the zoneLabel we collect from this term 370 | regionVal, err := regionParser(zoneVals) 371 | if err != nil { 372 | return err 373 | } 374 | // Add the regionVal to this term 375 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms[index].MatchExpressions = 376 | append(pv.Spec.NodeAffinity.Required.NodeSelectorTerms[index].MatchExpressions, v1.NodeSelectorRequirement{ 377 | Key: regionLabel, 378 | Operator: v1.NodeSelectorOpIn, 379 | Values: []string{regionVal}, 380 | }) 381 | 382 | } 383 | 384 | // Add region label 385 | regionVals := getTopologyValues(pv, regionLabel) 386 | if len(regionVals) == 1 { 387 | // We should only have exactly 1 region value 388 | if pv.Labels == nil { 389 | pv.Labels = make(map[string]string) 390 | } 391 | _, regionOK := pv.Labels[regionLabel] 392 | if !regionOK { 393 | pv.Labels[regionLabel] = regionVals[0] 394 | } 395 | } 396 | 397 | return nil 398 | } 399 | -------------------------------------------------------------------------------- /plugins/openstack_cinder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storage "k8s.io/api/storage/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | const ( 30 | // CinderDriverName is the name of the CSI driver for Cinder 31 | CinderDriverName = "cinder.csi.openstack.org" 32 | // CinderTopologyKey is the zonal topology key for Cinder CSI Driver 33 | CinderTopologyKey = "topology.cinder.csi.openstack.org/zone" 34 | // CinderInTreePluginName is the name of the intree plugin for Cinder 35 | CinderInTreePluginName = "kubernetes.io/cinder" 36 | ) 37 | 38 | var _ InTreePlugin = (*osCinderCSITranslator)(nil) 39 | 40 | // osCinderCSITranslator handles translation of PV spec from In-tree Cinder to CSI Cinder and vice versa 41 | type osCinderCSITranslator struct{} 42 | 43 | // NewOpenStackCinderCSITranslator returns a new instance of osCinderCSITranslator 44 | func NewOpenStackCinderCSITranslator() InTreePlugin { 45 | return &osCinderCSITranslator{} 46 | } 47 | 48 | // TranslateInTreeStorageClassToCSI translates InTree Cinder storage class parameters to CSI storage class 49 | func (t *osCinderCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 50 | var ( 51 | params = map[string]string{} 52 | ) 53 | for k, v := range sc.Parameters { 54 | switch strings.ToLower(k) { 55 | case fsTypeKey: 56 | params[csiFsTypeKey] = v 57 | default: 58 | // All other parameters are supported by the CSI driver. 59 | // This includes also "availability", therefore do not translate it to sc.AllowedTopologies 60 | params[k] = v 61 | } 62 | } 63 | 64 | if len(sc.AllowedTopologies) > 0 { 65 | newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, CinderTopologyKey) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed translating allowed topologies: %v", err) 68 | } 69 | sc.AllowedTopologies = newTopologies 70 | } 71 | 72 | sc.Parameters = params 73 | 74 | return sc, nil 75 | } 76 | 77 | // TranslateInTreeInlineVolumeToCSI takes a Volume with Cinder set from in-tree 78 | // and converts the Cinder source to a CSIPersistentVolumeSource 79 | func (t *osCinderCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 80 | if volume == nil || volume.Cinder == nil { 81 | return nil, fmt.Errorf("volume is nil or Cinder not defined on volume") 82 | } 83 | 84 | cinderSource := volume.Cinder 85 | pv := &v1.PersistentVolume{ 86 | ObjectMeta: metav1.ObjectMeta{ 87 | // Must be unique per disk as it is used as the unique part of the 88 | // staging path 89 | Name: fmt.Sprintf("%s-%s", CinderDriverName, cinderSource.VolumeID), 90 | }, 91 | Spec: v1.PersistentVolumeSpec{ 92 | PersistentVolumeSource: v1.PersistentVolumeSource{ 93 | CSI: &v1.CSIPersistentVolumeSource{ 94 | Driver: CinderDriverName, 95 | VolumeHandle: cinderSource.VolumeID, 96 | ReadOnly: cinderSource.ReadOnly, 97 | FSType: cinderSource.FSType, 98 | VolumeAttributes: map[string]string{}, 99 | }, 100 | }, 101 | AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 102 | }, 103 | } 104 | return pv, nil 105 | } 106 | 107 | // TranslateInTreePVToCSI takes a PV with Cinder set from in-tree 108 | // and converts the Cinder source to a CSIPersistentVolumeSource 109 | func (t *osCinderCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 110 | if pv == nil || pv.Spec.Cinder == nil { 111 | return nil, fmt.Errorf("pv is nil or Cinder not defined on pv") 112 | } 113 | 114 | cinderSource := pv.Spec.Cinder 115 | 116 | csiSource := &v1.CSIPersistentVolumeSource{ 117 | Driver: CinderDriverName, 118 | VolumeHandle: cinderSource.VolumeID, 119 | ReadOnly: cinderSource.ReadOnly, 120 | FSType: cinderSource.FSType, 121 | VolumeAttributes: map[string]string{}, 122 | } 123 | 124 | if err := translateTopologyFromInTreeToCSI(pv, CinderTopologyKey); err != nil { 125 | return nil, fmt.Errorf("failed to translate topology: %v", err) 126 | } 127 | 128 | pv.Spec.Cinder = nil 129 | pv.Spec.CSI = csiSource 130 | return pv, nil 131 | } 132 | 133 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 134 | // translates the Cinder CSI source to a Cinder In-tree source. 135 | func (t *osCinderCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 136 | if pv == nil || pv.Spec.CSI == nil { 137 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 138 | } 139 | 140 | csiSource := pv.Spec.CSI 141 | 142 | cinderSource := &v1.CinderPersistentVolumeSource{ 143 | VolumeID: csiSource.VolumeHandle, 144 | FSType: csiSource.FSType, 145 | ReadOnly: csiSource.ReadOnly, 146 | } 147 | 148 | // translate CSI topology to In-tree topology for rollback compatibility. 149 | // It is not possible to guess Cinder Region from the Zone, therefore leave it empty. 150 | if err := translateTopologyFromCSIToInTree(pv, CinderTopologyKey, nil); err != nil { 151 | return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err) 152 | } 153 | 154 | pv.Spec.CSI = nil 155 | pv.Spec.Cinder = cinderSource 156 | return pv, nil 157 | } 158 | 159 | // CanSupport tests whether the plugin supports a given persistent volume 160 | // specification from the API. The spec pointer should be considered 161 | // const. 162 | func (t *osCinderCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 163 | return pv != nil && pv.Spec.Cinder != nil 164 | } 165 | 166 | // CanSupportInline tests whether the plugin supports a given inline volume 167 | // specification from the API. The spec pointer should be considered 168 | // const. 169 | func (t *osCinderCSITranslator) CanSupportInline(volume *v1.Volume) bool { 170 | return volume != nil && volume.Cinder != nil 171 | } 172 | 173 | // GetInTreePluginName returns the name of the intree plugin driver 174 | func (t *osCinderCSITranslator) GetInTreePluginName() string { 175 | return CinderInTreePluginName 176 | } 177 | 178 | // GetCSIPluginName returns the name of the CSI plugin 179 | func (t *osCinderCSITranslator) GetCSIPluginName() string { 180 | return CinderDriverName 181 | } 182 | 183 | func (t *osCinderCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 184 | return volumeHandle, nil 185 | } 186 | -------------------------------------------------------------------------------- /plugins/openstack_cinder_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storage "k8s.io/api/storage/v1" 25 | "k8s.io/klog/v2/ktesting" 26 | _ "k8s.io/klog/v2/ktesting/init" 27 | ) 28 | 29 | func TestTranslateCinderInTreeStorageClassToCSI(t *testing.T) { 30 | translator := NewOpenStackCinderCSITranslator() 31 | logger, _ := ktesting.NewTestContext(t) 32 | 33 | cases := []struct { 34 | name string 35 | sc *storage.StorageClass 36 | expSc *storage.StorageClass 37 | expErr bool 38 | }{ 39 | { 40 | name: "translate normal", 41 | sc: NewStorageClass(map[string]string{"foo": "bar"}, nil), 42 | expSc: NewStorageClass(map[string]string{"foo": "bar"}, nil), 43 | }, 44 | { 45 | name: "translate empty map", 46 | sc: NewStorageClass(map[string]string{}, nil), 47 | expSc: NewStorageClass(map[string]string{}, nil), 48 | }, 49 | 50 | { 51 | name: "translate with fstype", 52 | sc: NewStorageClass(map[string]string{"fstype": "ext3"}, nil), 53 | expSc: NewStorageClass(map[string]string{"csi.storage.k8s.io/fstype": "ext3"}, nil), 54 | }, 55 | { 56 | name: "translate with topology in parameters (no translation expected)", 57 | sc: NewStorageClass(map[string]string{"availability": "nova"}, nil), 58 | expSc: NewStorageClass(map[string]string{"availability": "nova"}, nil), 59 | }, 60 | { 61 | name: "translate with topology", 62 | sc: NewStorageClass(map[string]string{}, generateToplogySelectors(v1.LabelFailureDomainBetaZone, []string{"nova"})), 63 | expSc: NewStorageClass(map[string]string{}, generateToplogySelectors(CinderTopologyKey, []string{"nova"})), 64 | }, 65 | } 66 | 67 | for _, tc := range cases { 68 | t.Logf("Testing %v", tc.name) 69 | got, err := translator.TranslateInTreeStorageClassToCSI(logger, tc.sc) 70 | if err != nil && !tc.expErr { 71 | t.Errorf("Did not expect error but got: %v", err) 72 | } 73 | 74 | if err == nil && tc.expErr { 75 | t.Errorf("Expected error, but did not get one.") 76 | } 77 | 78 | if !reflect.DeepEqual(got, tc.expSc) { 79 | t.Errorf("Got parameters: %v, expected: %v", got, tc.expSc) 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugins/portworx.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storagev1 "k8s.io/api/storage/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | const ( 30 | PortworxVolumePluginName = "kubernetes.io/portworx-volume" 31 | PortworxDriverName = "pxd.portworx.com" 32 | 33 | OpenStorageAuthSecretNameKey = "openstorage.io/auth-secret-name" 34 | OpenStorageAuthSecretNamespaceKey = "openstorage.io/auth-secret-namespace" 35 | 36 | csiParameterPrefix = "csi.storage.k8s.io/" 37 | 38 | prefixedProvisionerSecretNameKey = csiParameterPrefix + "provisioner-secret-name" 39 | prefixedProvisionerSecretNamespaceKey = csiParameterPrefix + "provisioner-secret-namespace" 40 | 41 | prefixedControllerPublishSecretNameKey = csiParameterPrefix + "controller-publish-secret-name" 42 | prefixedControllerPublishSecretNamespaceKey = csiParameterPrefix + "controller-publish-secret-namespace" 43 | 44 | prefixedNodeStageSecretNameKey = csiParameterPrefix + "node-stage-secret-name" 45 | prefixedNodeStageSecretNamespaceKey = csiParameterPrefix + "node-stage-secret-namespace" 46 | 47 | prefixedNodePublishSecretNameKey = csiParameterPrefix + "node-publish-secret-name" 48 | prefixedNodePublishSecretNamespaceKey = csiParameterPrefix + "node-publish-secret-namespace" 49 | 50 | prefixedControllerExpandSecretNameKey = csiParameterPrefix + "controller-expand-secret-name" 51 | prefixedControllerExpandSecretNamespaceKey = csiParameterPrefix + "controller-expand-secret-namespace" 52 | 53 | prefixedNodeExpandSecretNameKey = csiParameterPrefix + "node-expand-secret-name" 54 | prefixedNodeExpandSecretNamespaceKey = csiParameterPrefix + "node-expand-secret-namespace" 55 | ) 56 | 57 | var _ InTreePlugin = &portworxCSITranslator{} 58 | 59 | type portworxCSITranslator struct{} 60 | 61 | func NewPortworxCSITranslator() InTreePlugin { 62 | return &portworxCSITranslator{} 63 | } 64 | 65 | // TranslateInTreeStorageClassToCSI takes in-tree storage class used by in-tree plugin 66 | // and translates them to a storageclass consumable by CSI plugin 67 | func (p portworxCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storagev1.StorageClass) (*storagev1.StorageClass, error) { 68 | if sc == nil { 69 | return nil, fmt.Errorf("sc is nil") 70 | } 71 | 72 | var params = map[string]string{} 73 | for k, v := range sc.Parameters { 74 | switch strings.ToLower(k) { 75 | case OpenStorageAuthSecretNameKey: 76 | params[prefixedProvisionerSecretNameKey] = v 77 | params[prefixedControllerPublishSecretNameKey] = v 78 | params[prefixedNodePublishSecretNameKey] = v 79 | params[prefixedNodeStageSecretNameKey] = v 80 | params[prefixedControllerExpandSecretNameKey] = v 81 | params[prefixedNodeExpandSecretNameKey] = v 82 | case OpenStorageAuthSecretNamespaceKey: 83 | params[prefixedProvisionerSecretNamespaceKey] = v 84 | params[prefixedControllerPublishSecretNamespaceKey] = v 85 | params[prefixedNodePublishSecretNamespaceKey] = v 86 | params[prefixedNodeStageSecretNamespaceKey] = v 87 | params[prefixedControllerExpandSecretNamespaceKey] = v 88 | params[prefixedNodeExpandSecretNamespaceKey] = v 89 | default: 90 | // All other parameters can be copied as is 91 | params[k] = v 92 | } 93 | } 94 | if len(params) > 0 { 95 | sc.Parameters = params 96 | } 97 | sc.Provisioner = PortworxDriverName 98 | 99 | return sc, nil 100 | } 101 | 102 | // TranslateInTreeInlineVolumeToCSI takes a inline volume and will translate 103 | // the in-tree inline volume source to a CSIPersistentVolumeSource 104 | func (p portworxCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 105 | if volume == nil || volume.PortworxVolume == nil { 106 | return nil, fmt.Errorf("volume is nil or PortworxVolume not defined on volume") 107 | } 108 | 109 | var am v1.PersistentVolumeAccessMode 110 | if volume.PortworxVolume.ReadOnly { 111 | am = v1.ReadOnlyMany 112 | } else { 113 | am = v1.ReadWriteOnce 114 | } 115 | 116 | pv := &v1.PersistentVolume{ 117 | ObjectMeta: metav1.ObjectMeta{ 118 | Name: fmt.Sprintf("%s-%s", PortworxDriverName, volume.PortworxVolume.VolumeID), 119 | }, 120 | Spec: v1.PersistentVolumeSpec{ 121 | PersistentVolumeSource: v1.PersistentVolumeSource{ 122 | CSI: &v1.CSIPersistentVolumeSource{ 123 | Driver: PortworxDriverName, 124 | VolumeHandle: volume.PortworxVolume.VolumeID, 125 | FSType: volume.PortworxVolume.FSType, 126 | VolumeAttributes: make(map[string]string), 127 | }, 128 | }, 129 | AccessModes: []v1.PersistentVolumeAccessMode{am}, 130 | }, 131 | } 132 | return pv, nil 133 | } 134 | 135 | // TranslateInTreePVToCSI takes a Portworx persistent volume and will translate 136 | // the in-tree pv source to a CSI Source 137 | func (p portworxCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 138 | if pv == nil || pv.Spec.PortworxVolume == nil { 139 | return nil, fmt.Errorf("pv is nil or PortworxVolume not defined on pv") 140 | } 141 | var secretRef *v1.SecretReference 142 | 143 | if metav1.HasAnnotation(pv.ObjectMeta, OpenStorageAuthSecretNameKey) && 144 | metav1.HasAnnotation(pv.ObjectMeta, OpenStorageAuthSecretNamespaceKey) { 145 | secretRef = &v1.SecretReference{ 146 | Name: pv.Annotations[OpenStorageAuthSecretNameKey], 147 | Namespace: pv.Annotations[OpenStorageAuthSecretNamespaceKey], 148 | } 149 | } 150 | 151 | csiSource := &v1.CSIPersistentVolumeSource{ 152 | Driver: PortworxDriverName, 153 | VolumeHandle: pv.Spec.PortworxVolume.VolumeID, 154 | FSType: pv.Spec.PortworxVolume.FSType, 155 | VolumeAttributes: make(map[string]string), // copy access mode 156 | ControllerPublishSecretRef: secretRef, 157 | NodeStageSecretRef: secretRef, 158 | NodePublishSecretRef: secretRef, 159 | ControllerExpandSecretRef: secretRef, 160 | NodeExpandSecretRef: secretRef, 161 | } 162 | pv.Spec.PortworxVolume = nil 163 | pv.Spec.CSI = csiSource 164 | 165 | return pv, nil 166 | } 167 | 168 | // TranslateCSIPVToInTree takes a PV with a CSI PersistentVolume Source and will translate 169 | // it to a in-tree Persistent Volume Source for the in-tree volume 170 | func (p portworxCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 171 | if pv == nil || pv.Spec.CSI == nil { 172 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 173 | } 174 | csiSource := pv.Spec.CSI 175 | 176 | portworxSource := &v1.PortworxVolumeSource{ 177 | VolumeID: csiSource.VolumeHandle, 178 | FSType: csiSource.FSType, 179 | ReadOnly: csiSource.ReadOnly, 180 | } 181 | pv.Spec.CSI = nil 182 | pv.Spec.PortworxVolume = portworxSource 183 | 184 | return pv, nil 185 | } 186 | 187 | // CanSupport tests whether the plugin supports a given persistent volume 188 | // specification from the API. 189 | func (p portworxCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 190 | return pv != nil && pv.Spec.PortworxVolume != nil 191 | } 192 | 193 | // CanSupportInline tests whether the plugin supports a given inline volume 194 | // specification from the API. 195 | func (p portworxCSITranslator) CanSupportInline(volume *v1.Volume) bool { 196 | return volume != nil && volume.PortworxVolume != nil 197 | } 198 | 199 | // GetInTreePluginName returns the in-tree plugin name this migrates 200 | func (p portworxCSITranslator) GetInTreePluginName() string { 201 | return PortworxVolumePluginName 202 | } 203 | 204 | // GetCSIPluginName returns the name of the CSI plugin that supersedes the in-tree plugin 205 | func (p portworxCSITranslator) GetCSIPluginName() string { 206 | return PortworxDriverName 207 | } 208 | 209 | // RepairVolumeHandle generates a correct volume handle based on node ID information. 210 | func (p portworxCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 211 | return volumeHandle, nil 212 | } 213 | -------------------------------------------------------------------------------- /plugins/portworx_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storage "k8s.io/api/storage/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/klog/v2/ktesting" 27 | _ "k8s.io/klog/v2/ktesting/init" 28 | ) 29 | 30 | func TestTranslatePortworxInTreeStorageClassToCSI(t *testing.T) { 31 | translator := NewPortworxCSITranslator() 32 | logger, _ := ktesting.NewTestContext(t) 33 | testCases := []struct { 34 | name string 35 | inTreeSC *storage.StorageClass 36 | csiSC *storage.StorageClass 37 | errorExp bool 38 | }{ 39 | { 40 | name: "correct", 41 | inTreeSC: &storage.StorageClass{ 42 | Provisioner: PortworxVolumePluginName, 43 | Parameters: map[string]string{ 44 | "repl": "1", 45 | "fs": "ext4", 46 | "shared": "true", 47 | "priority_io": "high", 48 | }, 49 | }, 50 | csiSC: &storage.StorageClass{ 51 | Provisioner: PortworxDriverName, 52 | Parameters: map[string]string{ 53 | "repl": "1", 54 | "fs": "ext4", 55 | "shared": "true", 56 | "priority_io": "high", 57 | }, 58 | }, 59 | errorExp: false, 60 | }, 61 | { 62 | name: "nil, err expected", 63 | inTreeSC: nil, 64 | csiSC: nil, 65 | errorExp: true, 66 | }, 67 | { 68 | name: "empty", 69 | inTreeSC: &storage.StorageClass{}, 70 | csiSC: &storage.StorageClass{ 71 | Provisioner: PortworxDriverName, 72 | }, 73 | errorExp: false, 74 | }, 75 | { 76 | name: "with secret params", 77 | inTreeSC: &storage.StorageClass{ 78 | Parameters: map[string]string{ 79 | "repl": "1", 80 | "openstorage.io/auth-secret-name": "test-secret", 81 | "openstorage.io/auth-secret-namespace": "test-namespace", 82 | }, 83 | }, 84 | csiSC: &storage.StorageClass{ 85 | Parameters: map[string]string{ 86 | "repl": "1", 87 | "csi.storage.k8s.io/provisioner-secret-name": "test-secret", 88 | "csi.storage.k8s.io/provisioner-secret-namespace": "test-namespace", 89 | "csi.storage.k8s.io/controller-publish-secret-name": "test-secret", 90 | "csi.storage.k8s.io/controller-publish-secret-namespace": "test-namespace", 91 | "csi.storage.k8s.io/node-stage-secret-name": "test-secret", 92 | "csi.storage.k8s.io/node-stage-secret-namespace": "test-namespace", 93 | "csi.storage.k8s.io/node-publish-secret-name": "test-secret", 94 | "csi.storage.k8s.io/node-publish-secret-namespace": "test-namespace", 95 | "csi.storage.k8s.io/controller-expand-secret-name": "test-secret", 96 | "csi.storage.k8s.io/controller-expand-secret-namespace": "test-namespace", 97 | "csi.storage.k8s.io/node-expand-secret-name": "test-secret", 98 | "csi.storage.k8s.io/node-expand-secret-namespace": "test-namespace", 99 | }, 100 | Provisioner: PortworxDriverName, 101 | }, 102 | errorExp: false, 103 | }, 104 | } 105 | for _, tc := range testCases { 106 | t.Logf("Testing %v", tc.name) 107 | result, err := translator.TranslateInTreeStorageClassToCSI(logger, tc.inTreeSC) 108 | if err != nil && !tc.errorExp { 109 | t.Errorf("Did not expect error but got: %v", err) 110 | } 111 | if err == nil && tc.errorExp { 112 | t.Errorf("Expected error, but did not get one.") 113 | } 114 | if !reflect.DeepEqual(result, tc.csiSC) { 115 | t.Errorf("Got parameters: %v\n, expected :%v", result, tc.csiSC) 116 | } 117 | } 118 | } 119 | 120 | func TestTranslatePortworxInTreeInlineVolumeToCSI(t *testing.T) { 121 | translator := NewPortworxCSITranslator() 122 | logger, _ := ktesting.NewTestContext(t) 123 | 124 | testCases := []struct { 125 | name string 126 | inLine *v1.Volume 127 | csiVol *v1.PersistentVolume 128 | errExpected bool 129 | }{ 130 | { 131 | name: "normal", 132 | inLine: &v1.Volume{ 133 | Name: "PortworxVol", 134 | VolumeSource: v1.VolumeSource{ 135 | PortworxVolume: &v1.PortworxVolumeSource{ 136 | VolumeID: "ID", 137 | FSType: "type", 138 | ReadOnly: false, 139 | }, 140 | }, 141 | }, 142 | csiVol: &v1.PersistentVolume{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | // Must be unique per disk as it is used as the unique part of the 145 | // staging path 146 | Name: "pxd.portworx.com-ID", 147 | }, 148 | Spec: v1.PersistentVolumeSpec{ 149 | PersistentVolumeSource: v1.PersistentVolumeSource{ 150 | CSI: &v1.CSIPersistentVolumeSource{ 151 | Driver: PortworxDriverName, 152 | VolumeHandle: "ID", 153 | FSType: "type", 154 | VolumeAttributes: make(map[string]string), 155 | }, 156 | }, 157 | AccessModes: []v1.PersistentVolumeAccessMode{ 158 | v1.ReadWriteOnce, 159 | }, 160 | }, 161 | }, 162 | errExpected: false, 163 | }, 164 | { 165 | name: "nil", 166 | inLine: nil, 167 | csiVol: nil, 168 | errExpected: true, 169 | }, 170 | } 171 | 172 | for _, tc := range testCases { 173 | t.Logf("Testing %v", tc.name) 174 | result, err := translator.TranslateInTreeInlineVolumeToCSI(logger, tc.inLine, "ns") 175 | if err != nil && !tc.errExpected { 176 | t.Errorf("Did not expect error but got: %v", err) 177 | } 178 | if err == nil && tc.errExpected { 179 | t.Errorf("Expected error, but did not get one.") 180 | } 181 | if !reflect.DeepEqual(result, tc.csiVol) { 182 | t.Errorf("Got parameters: %v\n, expected :%v", result, tc.csiVol) 183 | } 184 | } 185 | } 186 | 187 | func TestTranslatePortworxInTreePVToCSI(t *testing.T) { 188 | translator := NewPortworxCSITranslator() 189 | logger, _ := ktesting.NewTestContext(t) 190 | 191 | testCases := []struct { 192 | name string 193 | inTree *v1.PersistentVolume 194 | csi *v1.PersistentVolume 195 | errExpected bool 196 | }{ 197 | { 198 | name: "no Portworx volume", 199 | inTree: &v1.PersistentVolume{ 200 | ObjectMeta: metav1.ObjectMeta{ 201 | Name: "pxd.portworx.com", 202 | }, 203 | Spec: v1.PersistentVolumeSpec{ 204 | AccessModes: []v1.PersistentVolumeAccessMode{ 205 | v1.ReadWriteOnce, 206 | }, 207 | ClaimRef: &v1.ObjectReference{ 208 | Name: "test-pvc", 209 | Namespace: "default", 210 | }, 211 | }, 212 | }, 213 | csi: nil, 214 | errExpected: true, 215 | }, 216 | { 217 | name: "normal", 218 | inTree: &v1.PersistentVolume{ 219 | ObjectMeta: metav1.ObjectMeta{ 220 | Name: "pxd.portworx.com", 221 | }, 222 | Spec: v1.PersistentVolumeSpec{ 223 | AccessModes: []v1.PersistentVolumeAccessMode{ 224 | v1.ReadWriteOnce, 225 | }, 226 | ClaimRef: &v1.ObjectReference{ 227 | Name: "test-pvc", 228 | Namespace: "default", 229 | }, 230 | PersistentVolumeSource: v1.PersistentVolumeSource{ 231 | PortworxVolume: &v1.PortworxVolumeSource{ 232 | VolumeID: "ID1111", 233 | FSType: "type", 234 | ReadOnly: false, 235 | }, 236 | }, 237 | }, 238 | }, 239 | csi: &v1.PersistentVolume{ 240 | ObjectMeta: metav1.ObjectMeta{ 241 | Name: "pxd.portworx.com", 242 | }, 243 | Spec: v1.PersistentVolumeSpec{ 244 | AccessModes: []v1.PersistentVolumeAccessMode{ 245 | v1.ReadWriteOnce, 246 | }, 247 | ClaimRef: &v1.ObjectReference{ 248 | Name: "test-pvc", 249 | Namespace: "default", 250 | }, 251 | PersistentVolumeSource: v1.PersistentVolumeSource{ 252 | CSI: &v1.CSIPersistentVolumeSource{ 253 | Driver: PortworxDriverName, 254 | VolumeHandle: "ID1111", 255 | FSType: "type", 256 | VolumeAttributes: make(map[string]string), 257 | }, 258 | }, 259 | }, 260 | }, 261 | errExpected: false, 262 | }, 263 | { 264 | name: "with secret annotations", 265 | inTree: &v1.PersistentVolume{ 266 | ObjectMeta: metav1.ObjectMeta{ 267 | Name: "pxd.portworx.com", 268 | Annotations: map[string]string{ 269 | "openstorage.io/auth-secret-name": "test-secret", 270 | "openstorage.io/auth-secret-namespace": "test-namespace", 271 | }, 272 | }, 273 | Spec: v1.PersistentVolumeSpec{ 274 | AccessModes: []v1.PersistentVolumeAccessMode{ 275 | v1.ReadWriteOnce, 276 | }, 277 | ClaimRef: &v1.ObjectReference{ 278 | Name: "test-pvc", 279 | Namespace: "default", 280 | }, 281 | PersistentVolumeSource: v1.PersistentVolumeSource{ 282 | PortworxVolume: &v1.PortworxVolumeSource{ 283 | VolumeID: "ID1111", 284 | FSType: "type", 285 | ReadOnly: false, 286 | }, 287 | }, 288 | }, 289 | }, 290 | csi: &v1.PersistentVolume{ 291 | ObjectMeta: metav1.ObjectMeta{ 292 | Name: "pxd.portworx.com", 293 | Annotations: map[string]string{ 294 | "openstorage.io/auth-secret-name": "test-secret", 295 | "openstorage.io/auth-secret-namespace": "test-namespace", 296 | }, 297 | }, 298 | Spec: v1.PersistentVolumeSpec{ 299 | AccessModes: []v1.PersistentVolumeAccessMode{ 300 | v1.ReadWriteOnce, 301 | }, 302 | ClaimRef: &v1.ObjectReference{ 303 | Name: "test-pvc", 304 | Namespace: "default", 305 | }, 306 | PersistentVolumeSource: v1.PersistentVolumeSource{ 307 | CSI: &v1.CSIPersistentVolumeSource{ 308 | Driver: PortworxDriverName, 309 | VolumeHandle: "ID1111", 310 | FSType: "type", 311 | VolumeAttributes: make(map[string]string), 312 | ControllerPublishSecretRef: &v1.SecretReference{ 313 | Name: "test-secret", 314 | Namespace: "test-namespace", 315 | }, 316 | NodeStageSecretRef: &v1.SecretReference{ 317 | Name: "test-secret", 318 | Namespace: "test-namespace", 319 | }, 320 | NodePublishSecretRef: &v1.SecretReference{ 321 | Name: "test-secret", 322 | Namespace: "test-namespace", 323 | }, 324 | ControllerExpandSecretRef: &v1.SecretReference{ 325 | Name: "test-secret", 326 | Namespace: "test-namespace", 327 | }, 328 | NodeExpandSecretRef: &v1.SecretReference{ 329 | Name: "test-secret", 330 | Namespace: "test-namespace", 331 | }, 332 | }, 333 | }, 334 | }, 335 | }, 336 | errExpected: false, 337 | }, 338 | { 339 | name: "nil PV", 340 | inTree: nil, 341 | csi: nil, 342 | errExpected: true, 343 | }, 344 | } 345 | 346 | for _, tc := range testCases { 347 | t.Logf("Testing %v", tc.name) 348 | result, err := translator.TranslateInTreePVToCSI(logger, tc.inTree) 349 | if err != nil && !tc.errExpected { 350 | t.Errorf("Did not expect error but got: %v", err) 351 | } 352 | if err == nil && tc.errExpected { 353 | t.Errorf("Expected error, but did not get one.") 354 | } 355 | if !reflect.DeepEqual(result, tc.csi) { 356 | t.Errorf("Got parameters: %v\n, expected :%v", result, tc.csi) 357 | } 358 | } 359 | } 360 | 361 | func TestTranslatePortworxCSIPvToInTree(t *testing.T) { 362 | translator := NewPortworxCSITranslator() 363 | 364 | testCases := []struct { 365 | name string 366 | csi *v1.PersistentVolume 367 | inTree *v1.PersistentVolume 368 | errExpected bool 369 | }{ 370 | { 371 | name: "no CSI section", 372 | csi: &v1.PersistentVolume{ 373 | ObjectMeta: metav1.ObjectMeta{ 374 | // Must be unique per disk as it is used as the unique part of the 375 | // staging path 376 | Name: "pxd.portworx.com", 377 | }, 378 | Spec: v1.PersistentVolumeSpec{ 379 | AccessModes: []v1.PersistentVolumeAccessMode{ 380 | v1.ReadWriteOnce, 381 | }, 382 | ClaimRef: &v1.ObjectReference{ 383 | Name: "test-pvc", 384 | Namespace: "default", 385 | }, 386 | }, 387 | }, 388 | inTree: nil, 389 | errExpected: true, 390 | }, 391 | { 392 | name: "normal", 393 | csi: &v1.PersistentVolume{ 394 | ObjectMeta: metav1.ObjectMeta{ 395 | Name: "pxd.portworx.com", 396 | }, 397 | Spec: v1.PersistentVolumeSpec{ 398 | AccessModes: []v1.PersistentVolumeAccessMode{ 399 | v1.ReadWriteOnce, 400 | }, 401 | ClaimRef: &v1.ObjectReference{ 402 | Name: "test-pvc", 403 | Namespace: "default", 404 | }, 405 | PersistentVolumeSource: v1.PersistentVolumeSource{ 406 | CSI: &v1.CSIPersistentVolumeSource{ 407 | Driver: PortworxDriverName, 408 | VolumeHandle: "ID1111", 409 | FSType: "type", 410 | VolumeAttributes: make(map[string]string), 411 | }, 412 | }, 413 | }, 414 | }, 415 | inTree: &v1.PersistentVolume{ 416 | ObjectMeta: metav1.ObjectMeta{ 417 | Name: "pxd.portworx.com", 418 | }, 419 | Spec: v1.PersistentVolumeSpec{ 420 | AccessModes: []v1.PersistentVolumeAccessMode{ 421 | v1.ReadWriteOnce, 422 | }, 423 | ClaimRef: &v1.ObjectReference{ 424 | Name: "test-pvc", 425 | Namespace: "default", 426 | }, 427 | PersistentVolumeSource: v1.PersistentVolumeSource{ 428 | PortworxVolume: &v1.PortworxVolumeSource{ 429 | VolumeID: "ID1111", 430 | FSType: "type", 431 | ReadOnly: false, 432 | }, 433 | }, 434 | }, 435 | }, 436 | errExpected: false, 437 | }, 438 | { 439 | name: "nil PV", 440 | inTree: nil, 441 | csi: nil, 442 | errExpected: true, 443 | }, 444 | } 445 | 446 | for _, tc := range testCases { 447 | t.Logf("Testing %v", tc.name) 448 | result, err := translator.TranslateCSIPVToInTree(tc.csi) 449 | if err != nil && !tc.errExpected { 450 | t.Errorf("Did not expect error but got: %v", err) 451 | } 452 | if err == nil && tc.errExpected { 453 | t.Errorf("Expected error, but did not get one.") 454 | } 455 | if !reflect.DeepEqual(result, tc.inTree) { 456 | t.Errorf("Got parameters: %v\n, expected :%v", result, tc.inTree) 457 | } 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /plugins/vsphere_volume.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storage "k8s.io/api/storage/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | const ( 30 | // VSphereDriverName is the name of the CSI driver for vSphere Volume 31 | VSphereDriverName = "csi.vsphere.vmware.com" 32 | // VSphereInTreePluginName is the name of the in-tree plugin for vSphere Volume 33 | VSphereInTreePluginName = "kubernetes.io/vsphere-volume" 34 | 35 | // vSphereCSITopologyZoneKey is the zonal topology key for vSphere CSI Driver 36 | vSphereCSITopologyZoneKey = "topology.csi.vmware.com/zone" 37 | 38 | // vSphereCSITopologyRegionKey is the region topology key for vSphere CSI Driver 39 | vSphereCSITopologyRegionKey = "topology.csi.vmware.com/region" 40 | 41 | // paramStoragePolicyName used to supply SPBM Policy name for Volume provisioning 42 | paramStoragePolicyName = "storagepolicyname" 43 | 44 | // This param is used to tell Driver to return volumePath and not VolumeID 45 | // in-tree vSphere plugin does not understand volume id, it uses volumePath 46 | paramcsiMigration = "csimigration" 47 | 48 | // This param is used to supply datastore name for Volume provisioning 49 | paramDatastore = "datastore-migrationparam" 50 | 51 | // This param supplies disk foramt (thin, thick, zeoredthick) for Volume provisioning 52 | paramDiskFormat = "diskformat-migrationparam" 53 | 54 | // vSAN Policy Parameters 55 | paramHostFailuresToTolerate = "hostfailurestotolerate-migrationparam" 56 | paramForceProvisioning = "forceprovisioning-migrationparam" 57 | paramCacheReservation = "cachereservation-migrationparam" 58 | paramDiskstripes = "diskstripes-migrationparam" 59 | paramObjectspacereservation = "objectspacereservation-migrationparam" 60 | paramIopslimit = "iopslimit-migrationparam" 61 | 62 | // AttributeInitialVolumeFilepath represents the path of volume where volume is created 63 | AttributeInitialVolumeFilepath = "initialvolumefilepath" 64 | ) 65 | 66 | var _ InTreePlugin = &vSphereCSITranslator{} 67 | 68 | // vSphereCSITranslator handles translation of PV spec from In-tree vSphere Volume to vSphere CSI 69 | type vSphereCSITranslator struct{} 70 | 71 | // NewvSphereCSITranslator returns a new instance of vSphereCSITranslator 72 | func NewvSphereCSITranslator() InTreePlugin { 73 | return &vSphereCSITranslator{} 74 | } 75 | 76 | // TranslateInTreeStorageClassToCSI translates InTree vSphere storage class parameters to CSI storage class 77 | func (t *vSphereCSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, sc *storage.StorageClass) (*storage.StorageClass, error) { 78 | if sc == nil { 79 | return nil, fmt.Errorf("sc is nil") 80 | } 81 | var params = map[string]string{} 82 | for k, v := range sc.Parameters { 83 | switch strings.ToLower(k) { 84 | case fsTypeKey: 85 | params[csiFsTypeKey] = v 86 | case paramStoragePolicyName: 87 | params[paramStoragePolicyName] = v 88 | case "datastore": 89 | params[paramDatastore] = v 90 | case "diskformat": 91 | params[paramDiskFormat] = v 92 | case "hostfailurestotolerate": 93 | params[paramHostFailuresToTolerate] = v 94 | case "forceprovisioning": 95 | params[paramForceProvisioning] = v 96 | case "cachereservation": 97 | params[paramCacheReservation] = v 98 | case "diskstripes": 99 | params[paramDiskstripes] = v 100 | case "objectspacereservation": 101 | params[paramObjectspacereservation] = v 102 | case "iopslimit": 103 | params[paramIopslimit] = v 104 | default: 105 | logger.V(2).Info("StorageClass parameter is not supported", "name", k, "value", v) 106 | } 107 | } 108 | 109 | // This helps vSphere CSI driver to identify in-tree provisioner request vs CSI provisioner request 110 | // When this is true, Driver returns initialvolumefilepath in the VolumeContext, which is 111 | // used in TranslateCSIPVToInTree 112 | params[paramcsiMigration] = "true" 113 | // translate AllowedTopologies to vSphere CSI Driver topology 114 | if len(sc.AllowedTopologies) > 0 { 115 | newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, vSphereCSITopologyZoneKey) 116 | if err != nil { 117 | return nil, fmt.Errorf("failed translating allowed topologies: %v", err) 118 | } 119 | sc.AllowedTopologies = newTopologies 120 | } 121 | sc.Parameters = params 122 | return sc, nil 123 | } 124 | 125 | // TranslateInTreeInlineVolumeToCSI takes a Volume with VsphereVolume set from in-tree 126 | // and converts the VsphereVolume source to a CSIPersistentVolumeSource 127 | func (t *vSphereCSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 128 | if volume == nil || volume.VsphereVolume == nil { 129 | return nil, fmt.Errorf("volume is nil or VsphereVolume not defined on volume") 130 | } 131 | pv := &v1.PersistentVolume{ 132 | ObjectMeta: metav1.ObjectMeta{ 133 | // Must be unique per disk as it is used as the unique part of the 134 | // staging path 135 | Name: fmt.Sprintf("%s-%s", VSphereDriverName, volume.VsphereVolume.VolumePath), 136 | }, 137 | Spec: v1.PersistentVolumeSpec{ 138 | PersistentVolumeSource: v1.PersistentVolumeSource{ 139 | CSI: &v1.CSIPersistentVolumeSource{ 140 | Driver: VSphereDriverName, 141 | VolumeHandle: volume.VsphereVolume.VolumePath, 142 | FSType: volume.VsphereVolume.FSType, 143 | VolumeAttributes: make(map[string]string), 144 | }, 145 | }, 146 | AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 147 | }, 148 | } 149 | if volume.VsphereVolume.StoragePolicyName != "" { 150 | pv.Spec.CSI.VolumeAttributes[paramStoragePolicyName] = pv.Spec.VsphereVolume.StoragePolicyName 151 | } 152 | return pv, nil 153 | } 154 | 155 | // TranslateInTreePVToCSI takes a PV with VsphereVolume set from in-tree 156 | // and converts the VsphereVolume source to a CSIPersistentVolumeSource 157 | func (t *vSphereCSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 158 | if pv == nil || pv.Spec.VsphereVolume == nil { 159 | return nil, fmt.Errorf("pv is nil or VsphereVolume not defined on pv") 160 | } 161 | csiSource := &v1.CSIPersistentVolumeSource{ 162 | Driver: VSphereDriverName, 163 | VolumeHandle: pv.Spec.VsphereVolume.VolumePath, 164 | FSType: pv.Spec.VsphereVolume.FSType, 165 | VolumeAttributes: make(map[string]string), 166 | } 167 | if pv.Spec.VsphereVolume.StoragePolicyName != "" { 168 | csiSource.VolumeAttributes[paramStoragePolicyName] = pv.Spec.VsphereVolume.StoragePolicyName 169 | } 170 | // translate in-tree topology to CSI topology for migration 171 | if err := translateTopologyFromInTreevSphereToCSI(pv, vSphereCSITopologyZoneKey, vSphereCSITopologyRegionKey); err != nil { 172 | return nil, fmt.Errorf("failed to translate topology: %v", err) 173 | } 174 | pv.Spec.VsphereVolume = nil 175 | pv.Spec.CSI = csiSource 176 | return pv, nil 177 | } 178 | 179 | // TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and 180 | // translates the vSphere CSI source to a vSphereVolume source. 181 | func (t *vSphereCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 182 | if pv == nil || pv.Spec.CSI == nil { 183 | return nil, fmt.Errorf("pv is nil or CSI source not defined on pv") 184 | } 185 | csiSource := pv.Spec.CSI 186 | vsphereVirtualDiskVolumeSource := &v1.VsphereVirtualDiskVolumeSource{ 187 | FSType: csiSource.FSType, 188 | } 189 | volumeFilePath, ok := csiSource.VolumeAttributes[AttributeInitialVolumeFilepath] 190 | if ok { 191 | vsphereVirtualDiskVolumeSource.VolumePath = volumeFilePath 192 | } 193 | // translate CSI topology to In-tree topology for rollback compatibility. 194 | if err := translateTopologyFromCSIToInTreevSphere(pv, vSphereCSITopologyZoneKey, vSphereCSITopologyRegionKey); err != nil { 195 | return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err) 196 | } 197 | pv.Spec.CSI = nil 198 | pv.Spec.VsphereVolume = vsphereVirtualDiskVolumeSource 199 | return pv, nil 200 | } 201 | 202 | // CanSupport tests whether the plugin supports a given persistent volume 203 | // specification from the API. 204 | func (t *vSphereCSITranslator) CanSupport(pv *v1.PersistentVolume) bool { 205 | return pv != nil && pv.Spec.VsphereVolume != nil 206 | } 207 | 208 | // CanSupportInline tests whether the plugin supports a given inline volume 209 | // specification from the API. 210 | func (t *vSphereCSITranslator) CanSupportInline(volume *v1.Volume) bool { 211 | return volume != nil && volume.VsphereVolume != nil 212 | } 213 | 214 | // GetInTreePluginName returns the name of the in-tree plugin driver 215 | func (t *vSphereCSITranslator) GetInTreePluginName() string { 216 | return VSphereInTreePluginName 217 | } 218 | 219 | // GetCSIPluginName returns the name of the CSI plugin 220 | func (t *vSphereCSITranslator) GetCSIPluginName() string { 221 | return VSphereDriverName 222 | } 223 | 224 | // RepairVolumeHandle is needed in VerifyVolumesAttached on the external attacher when we need to do strict volume 225 | // handle matching to check VolumeAttachment attached status. 226 | // vSphere volume does not need patch to help verify whether that volume is attached. 227 | func (t *vSphereCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { 228 | return volumeHandle, nil 229 | } 230 | 231 | // translateTopologyFromInTreevSphereToCSI converts existing zone labels or in-tree vsphere topology to 232 | // vSphere CSI topology. 233 | func translateTopologyFromInTreevSphereToCSI(pv *v1.PersistentVolume, csiTopologyKeyZone string, csiTopologyKeyRegion string) error { 234 | zoneLabel, regionLabel := getTopologyLabel(pv) 235 | 236 | // If Zone kubernetes topology exist, replace it to use csiTopologyKeyZone 237 | zones := getTopologyValues(pv, zoneLabel) 238 | if len(zones) > 0 { 239 | replaceTopology(pv, zoneLabel, csiTopologyKeyZone) 240 | } else { 241 | // if nothing is in the NodeAffinity, try to fetch the topology from PV labels 242 | if label, ok := pv.Labels[zoneLabel]; ok { 243 | if len(label) > 0 { 244 | addTopology(pv, csiTopologyKeyZone, []string{label}) 245 | } 246 | } 247 | } 248 | 249 | // If region kubernetes topology exist, replace it to use csiTopologyKeyRegion 250 | regions := getTopologyValues(pv, regionLabel) 251 | if len(regions) > 0 { 252 | replaceTopology(pv, regionLabel, csiTopologyKeyRegion) 253 | } else { 254 | // if nothing is in the NodeAffinity, try to fetch the topology from PV labels 255 | if label, ok := pv.Labels[regionLabel]; ok { 256 | if len(label) > 0 { 257 | addTopology(pv, csiTopologyKeyRegion, []string{label}) 258 | } 259 | } 260 | } 261 | return nil 262 | } 263 | 264 | // translateTopologyFromCSIToInTreevSphere converts CSI zone/region affinity rules to in-tree vSphere zone/region labels 265 | func translateTopologyFromCSIToInTreevSphere(pv *v1.PersistentVolume, 266 | csiTopologyKeyZone string, csiTopologyKeyRegion string) error { 267 | zoneLabel, regionLabel := getTopologyLabel(pv) 268 | 269 | // Replace all CSI topology to Kubernetes Zone label 270 | err := replaceTopology(pv, csiTopologyKeyZone, zoneLabel) 271 | if err != nil { 272 | return fmt.Errorf("failed to replace CSI topology to Kubernetes topology, error: %v", err) 273 | } 274 | 275 | // Replace all CSI topology to Kubernetes Region label 276 | err = replaceTopology(pv, csiTopologyKeyRegion, regionLabel) 277 | if err != nil { 278 | return fmt.Errorf("failed to replace CSI topology to Kubernetes topology, error: %v", err) 279 | } 280 | 281 | zoneVals := getTopologyValues(pv, zoneLabel) 282 | if len(zoneVals) > 0 { 283 | if pv.Labels == nil { 284 | pv.Labels = make(map[string]string) 285 | } 286 | _, zoneOK := pv.Labels[zoneLabel] 287 | if !zoneOK { 288 | pv.Labels[zoneLabel] = zoneVals[0] 289 | } 290 | } 291 | regionVals := getTopologyValues(pv, regionLabel) 292 | if len(regionVals) > 0 { 293 | if pv.Labels == nil { 294 | pv.Labels = make(map[string]string) 295 | } 296 | _, regionOK := pv.Labels[regionLabel] 297 | if !regionOK { 298 | pv.Labels[regionLabel] = regionVals[0] 299 | } 300 | } 301 | return nil 302 | } 303 | -------------------------------------------------------------------------------- /translate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package csitranslation 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | storage "k8s.io/api/storage/v1" 25 | "k8s.io/csi-translation-lib/plugins" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | var ( 30 | inTreePlugins = map[string]plugins.InTreePlugin{ 31 | plugins.GCEPDDriverName: plugins.NewGCEPersistentDiskCSITranslator(), 32 | plugins.AWSEBSDriverName: plugins.NewAWSElasticBlockStoreCSITranslator(), 33 | plugins.CinderDriverName: plugins.NewOpenStackCinderCSITranslator(), 34 | plugins.AzureDiskDriverName: plugins.NewAzureDiskCSITranslator(), 35 | plugins.AzureFileDriverName: plugins.NewAzureFileCSITranslator(), 36 | plugins.VSphereDriverName: plugins.NewvSphereCSITranslator(), 37 | plugins.PortworxDriverName: plugins.NewPortworxCSITranslator(), 38 | } 39 | ) 40 | 41 | // CSITranslator translates in-tree storage API objects to their equivalent CSI 42 | // API objects. It also provides many helper functions to determine whether 43 | // translation logic exists and the mappings between "in-tree plugin <-> csi driver" 44 | type CSITranslator struct{} 45 | 46 | // New creates a new CSITranslator which does real translation 47 | // for "in-tree plugins <-> csi drivers" 48 | func New() CSITranslator { 49 | return CSITranslator{} 50 | } 51 | 52 | // TranslateInTreeStorageClassToCSI takes in-tree Storage Class 53 | // and translates it to a set of parameters consumable by CSI plugin 54 | func (CSITranslator) TranslateInTreeStorageClassToCSI(logger klog.Logger, inTreePluginName string, sc *storage.StorageClass) (*storage.StorageClass, error) { 55 | newSC := sc.DeepCopy() 56 | for _, curPlugin := range inTreePlugins { 57 | if inTreePluginName == curPlugin.GetInTreePluginName() { 58 | return curPlugin.TranslateInTreeStorageClassToCSI(logger, newSC) 59 | } 60 | } 61 | return nil, fmt.Errorf("could not find in-tree storage class parameter translation logic for %#v", inTreePluginName) 62 | } 63 | 64 | // TranslateInTreeInlineVolumeToCSI takes a inline volume and will translate 65 | // the in-tree volume source to a CSIPersistentVolumeSource (wrapped in a PV) 66 | // if the translation logic has been implemented. 67 | func (CSITranslator) TranslateInTreeInlineVolumeToCSI(logger klog.Logger, volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) { 68 | if volume == nil { 69 | return nil, fmt.Errorf("persistent volume was nil") 70 | } 71 | for _, curPlugin := range inTreePlugins { 72 | if curPlugin.CanSupportInline(volume) { 73 | pv, err := curPlugin.TranslateInTreeInlineVolumeToCSI(logger, volume, podNamespace) 74 | if err != nil { 75 | return nil, err 76 | } 77 | // Inline volumes only support PersistentVolumeFilesystem (and not block). 78 | // If VolumeMode has not been set explicitly by plugin-specific 79 | // translator, set it to Filesystem here. 80 | // This is only necessary for inline volumes as the default PV 81 | // initialization that populates VolumeMode does not apply to inline volumes. 82 | if pv.Spec.VolumeMode == nil { 83 | volumeMode := v1.PersistentVolumeFilesystem 84 | pv.Spec.VolumeMode = &volumeMode 85 | } 86 | return pv, nil 87 | } 88 | } 89 | return nil, fmt.Errorf("could not find in-tree plugin translation logic for %#v", volume.Name) 90 | } 91 | 92 | // TranslateInTreePVToCSI takes a persistent volume and will translate 93 | // the in-tree source to a CSI Source if the translation logic 94 | // has been implemented. The input persistent volume will not 95 | // be modified 96 | func (CSITranslator) TranslateInTreePVToCSI(logger klog.Logger, pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 97 | if pv == nil { 98 | return nil, errors.New("persistent volume was nil") 99 | } 100 | copiedPV := pv.DeepCopy() 101 | for _, curPlugin := range inTreePlugins { 102 | if curPlugin.CanSupport(copiedPV) { 103 | return curPlugin.TranslateInTreePVToCSI(logger, copiedPV) 104 | } 105 | } 106 | return nil, fmt.Errorf("could not find in-tree plugin translation logic for %#v", copiedPV.Name) 107 | } 108 | 109 | // TranslateCSIPVToInTree takes a PV with a CSI PersistentVolume Source and will translate 110 | // it to a in-tree Persistent Volume Source for the specific in-tree volume specified 111 | // by the `Driver` field in the CSI Source. The input PV object will not be modified. 112 | func (CSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) { 113 | if pv == nil || pv.Spec.CSI == nil { 114 | return nil, errors.New("CSI persistent volume was nil") 115 | } 116 | copiedPV := pv.DeepCopy() 117 | for driverName, curPlugin := range inTreePlugins { 118 | if copiedPV.Spec.CSI.Driver == driverName { 119 | return curPlugin.TranslateCSIPVToInTree(copiedPV) 120 | } 121 | } 122 | return nil, fmt.Errorf("could not find in-tree plugin translation logic for %s", copiedPV.Spec.CSI.Driver) 123 | } 124 | 125 | // IsMigratableIntreePluginByName tests whether there is migration logic for the in-tree plugin 126 | // whose name matches the given name 127 | func (CSITranslator) IsMigratableIntreePluginByName(inTreePluginName string) bool { 128 | for _, curPlugin := range inTreePlugins { 129 | if curPlugin.GetInTreePluginName() == inTreePluginName { 130 | return true 131 | } 132 | } 133 | return false 134 | } 135 | 136 | // IsMigratedCSIDriverByName tests whether there exists an in-tree plugin with logic 137 | // to migrate to the CSI driver with given name 138 | func (CSITranslator) IsMigratedCSIDriverByName(csiPluginName string) bool { 139 | if _, ok := inTreePlugins[csiPluginName]; ok { 140 | return true 141 | } 142 | return false 143 | } 144 | 145 | // GetInTreePluginNameFromSpec returns the plugin name 146 | func (CSITranslator) GetInTreePluginNameFromSpec(pv *v1.PersistentVolume, vol *v1.Volume) (string, error) { 147 | if pv != nil { 148 | for _, curPlugin := range inTreePlugins { 149 | if curPlugin.CanSupport(pv) { 150 | return curPlugin.GetInTreePluginName(), nil 151 | } 152 | } 153 | return "", fmt.Errorf("could not find in-tree plugin name from persistent volume %s", pv.Name) 154 | } else if vol != nil { 155 | for _, curPlugin := range inTreePlugins { 156 | if curPlugin.CanSupportInline(vol) { 157 | return curPlugin.GetInTreePluginName(), nil 158 | } 159 | } 160 | return "", fmt.Errorf("could not find in-tree plugin name from volume %s", vol.Name) 161 | } else { 162 | return "", errors.New("both persistent volume and volume are nil") 163 | } 164 | } 165 | 166 | // GetCSINameFromInTreeName returns the name of a CSI driver that supersedes the 167 | // in-tree plugin with the given name 168 | func (CSITranslator) GetCSINameFromInTreeName(pluginName string) (string, error) { 169 | for csiDriverName, curPlugin := range inTreePlugins { 170 | if curPlugin.GetInTreePluginName() == pluginName { 171 | return csiDriverName, nil 172 | } 173 | } 174 | return "", fmt.Errorf("could not find CSI Driver name for plugin %s", pluginName) 175 | } 176 | 177 | // GetInTreeNameFromCSIName returns the name of the in-tree plugin superseded by 178 | // a CSI driver with the given name 179 | func (CSITranslator) GetInTreeNameFromCSIName(pluginName string) (string, error) { 180 | if plugin, ok := inTreePlugins[pluginName]; ok { 181 | return plugin.GetInTreePluginName(), nil 182 | } 183 | return "", fmt.Errorf("could not find In-Tree driver name for CSI plugin %s", pluginName) 184 | } 185 | 186 | // IsPVMigratable tests whether there is migration logic for the given Persistent Volume 187 | func (CSITranslator) IsPVMigratable(pv *v1.PersistentVolume) bool { 188 | for _, curPlugin := range inTreePlugins { 189 | if curPlugin.CanSupport(pv) { 190 | return true 191 | } 192 | } 193 | return false 194 | } 195 | 196 | // IsInlineMigratable tests whether there is Migration logic for the given Inline Volume 197 | func (CSITranslator) IsInlineMigratable(vol *v1.Volume) bool { 198 | for _, curPlugin := range inTreePlugins { 199 | if curPlugin.CanSupportInline(vol) { 200 | return true 201 | } 202 | } 203 | return false 204 | } 205 | 206 | // RepairVolumeHandle generates a correct volume handle based on node ID information. 207 | func (CSITranslator) RepairVolumeHandle(driverName, volumeHandle, nodeID string) (string, error) { 208 | if plugin, ok := inTreePlugins[driverName]; ok { 209 | return plugin.RepairVolumeHandle(volumeHandle, nodeID) 210 | } 211 | return "", fmt.Errorf("could not find In-Tree driver name for CSI plugin %s", driverName) 212 | } 213 | -------------------------------------------------------------------------------- /translate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package csitranslation 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "testing" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/util/uuid" 27 | "k8s.io/csi-translation-lib/plugins" 28 | "k8s.io/klog/v2/ktesting" 29 | _ "k8s.io/klog/v2/ktesting/init" 30 | ) 31 | 32 | var ( 33 | kubernetesBetaTopologyLabels = map[string]string{ 34 | v1.LabelFailureDomainBetaZone: "us-east-1a", 35 | v1.LabelFailureDomainBetaRegion: "us-east-1", 36 | } 37 | kubernetesGATopologyLabels = map[string]string{ 38 | v1.LabelTopologyZone: "us-east-1a", 39 | v1.LabelTopologyRegion: "us-east-1", 40 | } 41 | regionalBetaPDLabels = map[string]string{ 42 | v1.LabelFailureDomainBetaZone: "europe-west1-b__europe-west1-c", 43 | } 44 | regionalGAPDLabels = map[string]string{ 45 | v1.LabelTopologyZone: "europe-west1-b__europe-west1-c", 46 | } 47 | ) 48 | 49 | func TestTranslationStability(t *testing.T) { 50 | logger, _ := ktesting.NewTestContext(t) 51 | testCases := []struct { 52 | name string 53 | pv *v1.PersistentVolume 54 | }{ 55 | 56 | { 57 | name: "GCE PD PV Source", 58 | pv: &v1.PersistentVolume{ 59 | Spec: v1.PersistentVolumeSpec{ 60 | PersistentVolumeSource: v1.PersistentVolumeSource{ 61 | GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 62 | PDName: "test-disk", 63 | FSType: "ext4", 64 | Partition: 0, 65 | ReadOnly: false, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | { 72 | name: "AWS EBS PV Source", 73 | pv: &v1.PersistentVolume{ 74 | Spec: v1.PersistentVolumeSpec{ 75 | PersistentVolumeSource: v1.PersistentVolumeSource{ 76 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 77 | VolumeID: "vol01", 78 | FSType: "ext3", 79 | Partition: 1, 80 | ReadOnly: true, 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | } 87 | for _, test := range testCases { 88 | ctl := New() 89 | t.Logf("Testing %v", test.name) 90 | csiSource, err := ctl.TranslateInTreePVToCSI(logger, test.pv) 91 | if err != nil { 92 | t.Errorf("Error when translating to CSI: %v", err) 93 | } 94 | newPV, err := ctl.TranslateCSIPVToInTree(csiSource) 95 | if err != nil { 96 | t.Errorf("Error when translating CSI Source to in tree volume: %v", err) 97 | } 98 | if !reflect.DeepEqual(newPV, test.pv) { 99 | t.Errorf("Volumes after translation and back not equal:\n\nOriginal Volume: %#v\n\nRound-trip Volume: %#v", test.pv, newPV) 100 | } 101 | } 102 | } 103 | 104 | func TestTopologyTranslation(t *testing.T) { 105 | logger, _ := ktesting.NewTestContext(t) 106 | testCases := []struct { 107 | name string 108 | key string 109 | pv *v1.PersistentVolume 110 | expectedNodeAffinity *v1.VolumeNodeAffinity 111 | }{ 112 | { 113 | name: "GCE PD with beta zone labels", 114 | key: plugins.GCEPDTopologyKey, 115 | pv: makeGCEPDPV(kubernetesBetaTopologyLabels, nil /*topology*/), 116 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "us-east-1a"), 117 | }, 118 | { 119 | name: "GCE PD with GA kubernetes zone labels", 120 | key: plugins.GCEPDTopologyKey, 121 | pv: makeGCEPDPV(kubernetesGATopologyLabels, nil /*topology*/), 122 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "us-east-1a"), 123 | }, 124 | { 125 | name: "GCE PD with existing topology (beta keys)", 126 | pv: makeGCEPDPV(nil /*labels*/, makeTopology(v1.LabelFailureDomainBetaZone, "us-east-2a")), 127 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "us-east-2a"), 128 | }, 129 | { 130 | name: "GCE PD with existing topology (CSI keys)", 131 | key: plugins.GCEPDTopologyKey, 132 | pv: makeGCEPDPV(nil /*labels*/, makeTopology(plugins.GCEPDTopologyKey, "us-east-2a")), 133 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "us-east-2a"), 134 | }, 135 | { 136 | name: "GCE PD with zone labels and topology", 137 | pv: makeGCEPDPV(kubernetesBetaTopologyLabels, makeTopology(v1.LabelFailureDomainBetaZone, "us-east-2a")), 138 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "us-east-2a"), 139 | }, 140 | { 141 | name: "GCE PD with regional zones", 142 | key: plugins.GCEPDTopologyKey, 143 | pv: makeGCEPDPV(regionalBetaPDLabels, nil /*topology*/), 144 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "europe-west1-b", "europe-west1-c"), 145 | }, 146 | { 147 | name: "GCE PD with regional topology", 148 | key: plugins.GCEPDTopologyKey, 149 | pv: makeGCEPDPV(nil /*labels*/, makeTopology(v1.LabelTopologyZone, "europe-west1-b", "europe-west1-c")), 150 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "europe-west1-b", "europe-west1-c"), 151 | }, 152 | { 153 | name: "GCE PD with Beta regional zone and topology", 154 | key: plugins.GCEPDTopologyKey, 155 | pv: makeGCEPDPV(regionalBetaPDLabels, makeTopology(v1.LabelFailureDomainBetaZone, "europe-west1-f", "europe-west1-g")), 156 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "europe-west1-f", "europe-west1-g"), 157 | }, 158 | { 159 | name: "GCE PD with GA regional zone and topology", 160 | key: plugins.GCEPDTopologyKey, 161 | pv: makeGCEPDPV(regionalGAPDLabels, makeTopology(v1.LabelTopologyZone, "europe-west1-f", "europe-west1-g")), 162 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.GCEPDTopologyKey, "europe-west1-f", "europe-west1-g"), 163 | }, 164 | { 165 | name: "GCE PD with multiple node selector terms", 166 | key: plugins.GCEPDTopologyKey, 167 | pv: makeGCEPDPVMultTerms( 168 | nil, /*labels*/ 169 | makeTopology(v1.LabelTopologyZone, "europe-west1-f"), 170 | makeTopology(v1.LabelTopologyZone, "europe-west1-g")), 171 | expectedNodeAffinity: makeNodeAffinity( 172 | true, /*multiTerms*/ 173 | plugins.GCEPDTopologyKey, "europe-west1-f", "europe-west1-g"), 174 | }, 175 | // EBS test cases: test mostly topology key, i.e., don't repeat testing done with GCE 176 | { 177 | name: "AWS EBS with beta zone labels", 178 | pv: makeAWSEBSPV(kubernetesBetaTopologyLabels, nil /*topology*/), 179 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.AWSEBSTopologyKey, "us-east-1a"), 180 | }, 181 | { 182 | name: "AWS EBS with beta zone labels and topology", 183 | pv: makeAWSEBSPV(kubernetesBetaTopologyLabels, makeTopology(v1.LabelFailureDomainBetaZone, "us-east-2a")), 184 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.AWSEBSTopologyKey, "us-east-2a"), 185 | }, 186 | { 187 | name: "AWS EBS with GA zone labels", 188 | pv: makeAWSEBSPV(kubernetesGATopologyLabels, nil /*topology*/), 189 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.AWSEBSTopologyKey, "us-east-1a"), 190 | }, 191 | { 192 | name: "AWS EBS with GA zone labels and topology", 193 | pv: makeAWSEBSPV(kubernetesGATopologyLabels, makeTopology(v1.LabelTopologyZone, "us-east-2a")), 194 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.AWSEBSTopologyKey, "us-east-2a"), 195 | }, 196 | // Cinder test cases: test mosty topology key, i.e., don't repeat testing done with GCE 197 | { 198 | name: "OpenStack Cinder with zone labels", 199 | pv: makeCinderPV(kubernetesBetaTopologyLabels, nil /*topology*/), 200 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.CinderTopologyKey, "us-east-1a"), 201 | }, 202 | { 203 | name: "OpenStack Cinder with zone labels and topology", 204 | pv: makeCinderPV(kubernetesBetaTopologyLabels, makeTopology(v1.LabelFailureDomainBetaZone, "us-east-2a")), 205 | expectedNodeAffinity: makeNodeAffinity(false /*multiTerms*/, plugins.CinderTopologyKey, "us-east-2a"), 206 | }, 207 | } 208 | 209 | for _, test := range testCases { 210 | ctl := New() 211 | t.Logf("Testing %v", test.name) 212 | 213 | // Translate to CSI PV and check translated node affinity 214 | newCSIPV, err := ctl.TranslateInTreePVToCSI(logger, test.pv) 215 | if err != nil { 216 | t.Errorf("Error when translating to CSI: %v", err) 217 | } 218 | 219 | nodeAffinity := newCSIPV.Spec.NodeAffinity 220 | if !reflect.DeepEqual(nodeAffinity, test.expectedNodeAffinity) { 221 | t.Errorf("Expected node affinity %v, got %v", *test.expectedNodeAffinity, *nodeAffinity) 222 | } 223 | 224 | // Translate back to in-tree and make sure node affinity has been removed 225 | newInTreePV, err := ctl.TranslateCSIPVToInTree(newCSIPV) 226 | if err != nil { 227 | t.Errorf("Error when translating to in-tree: %v", err) 228 | } 229 | 230 | // For now, non-pd cloud should stay the old behavior which is still have the CSI topology. 231 | if test.key != "" { 232 | nodeAffinity = newInTreePV.Spec.NodeAffinity 233 | if plugins.TopologyKeyExist(test.key, nodeAffinity) { 234 | t.Errorf("Expected node affinity key %v being removed, got %v", test.key, *nodeAffinity) 235 | } 236 | // verify that either beta or GA kubernetes topology key should exist 237 | if !(plugins.TopologyKeyExist(v1.LabelFailureDomainBetaZone, nodeAffinity) || plugins.TopologyKeyExist(v1.LabelTopologyZone, nodeAffinity)) { 238 | t.Errorf("Expected node affinity kubernetes topology label exist, got %v", *nodeAffinity) 239 | } 240 | } else { 241 | nodeAffinity := newCSIPV.Spec.NodeAffinity 242 | if !reflect.DeepEqual(nodeAffinity, test.expectedNodeAffinity) { 243 | t.Errorf("Expected node affinity %v, got %v", *test.expectedNodeAffinity, *nodeAffinity) 244 | } 245 | } 246 | } 247 | } 248 | 249 | func makePV(labels map[string]string, topology *v1.NodeSelectorRequirement) *v1.PersistentVolume { 250 | pv := &v1.PersistentVolume{ 251 | ObjectMeta: metav1.ObjectMeta{ 252 | Labels: labels, 253 | }, 254 | Spec: v1.PersistentVolumeSpec{}, 255 | } 256 | 257 | if topology != nil { 258 | pv.Spec.NodeAffinity = &v1.VolumeNodeAffinity{ 259 | Required: &v1.NodeSelector{ 260 | NodeSelectorTerms: []v1.NodeSelectorTerm{ 261 | {MatchExpressions: []v1.NodeSelectorRequirement{*topology}}, 262 | }, 263 | }, 264 | } 265 | } 266 | 267 | return pv 268 | } 269 | 270 | func makeGCEPDPV(labels map[string]string, topology *v1.NodeSelectorRequirement) *v1.PersistentVolume { 271 | pv := makePV(labels, topology) 272 | pv.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 273 | GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 274 | PDName: "test-disk", 275 | FSType: "ext4", 276 | Partition: 0, 277 | ReadOnly: false, 278 | }, 279 | } 280 | return pv 281 | } 282 | 283 | func makeGCEPDPVMultTerms(labels map[string]string, topologies ...*v1.NodeSelectorRequirement) *v1.PersistentVolume { 284 | pv := makeGCEPDPV(labels, topologies[0]) 285 | for _, topology := range topologies[1:] { 286 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms = append( 287 | pv.Spec.NodeAffinity.Required.NodeSelectorTerms, 288 | v1.NodeSelectorTerm{ 289 | MatchExpressions: []v1.NodeSelectorRequirement{*topology}, 290 | }, 291 | ) 292 | } 293 | return pv 294 | } 295 | 296 | func makeAWSEBSPV(labels map[string]string, topology *v1.NodeSelectorRequirement) *v1.PersistentVolume { 297 | pv := makePV(labels, topology) 298 | pv.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 299 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 300 | VolumeID: "vol01", 301 | FSType: "ext3", 302 | Partition: 1, 303 | ReadOnly: true, 304 | }, 305 | } 306 | return pv 307 | } 308 | 309 | func makeCinderPV(labels map[string]string, topology *v1.NodeSelectorRequirement) *v1.PersistentVolume { 310 | pv := makePV(labels, topology) 311 | pv.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 312 | Cinder: &v1.CinderPersistentVolumeSource{ 313 | VolumeID: "vol1", 314 | FSType: "ext4", 315 | ReadOnly: false, 316 | }, 317 | } 318 | return pv 319 | } 320 | 321 | func makeNodeAffinity(multiTerms bool, key string, values ...string) *v1.VolumeNodeAffinity { 322 | nodeAffinity := &v1.VolumeNodeAffinity{ 323 | Required: &v1.NodeSelector{ 324 | NodeSelectorTerms: []v1.NodeSelectorTerm{ 325 | { 326 | MatchExpressions: []v1.NodeSelectorRequirement{ 327 | { 328 | Key: key, 329 | Operator: v1.NodeSelectorOpIn, 330 | Values: values, 331 | }, 332 | }, 333 | }, 334 | }, 335 | }, 336 | } 337 | 338 | // If multiple terms is NOT requested, return a single term with all values 339 | if !multiTerms { 340 | return nodeAffinity 341 | } 342 | 343 | // Otherwise return multiple terms, each one with a single value 344 | nodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions[0].Values = values[:1] // If values=[1,2,3], overwrite with [1] 345 | for _, value := range values[1:] { 346 | term := v1.NodeSelectorTerm{ 347 | MatchExpressions: []v1.NodeSelectorRequirement{ 348 | { 349 | Key: key, 350 | Operator: v1.NodeSelectorOpIn, 351 | Values: []string{value}, 352 | }, 353 | }, 354 | } 355 | nodeAffinity.Required.NodeSelectorTerms = append(nodeAffinity.Required.NodeSelectorTerms, term) 356 | } 357 | 358 | return nodeAffinity 359 | } 360 | 361 | func makeTopology(key string, values ...string) *v1.NodeSelectorRequirement { 362 | return &v1.NodeSelectorRequirement{ 363 | Key: key, 364 | Operator: v1.NodeSelectorOpIn, 365 | Values: values, 366 | } 367 | } 368 | 369 | func TestTranslateInTreeInlineVolumeToCSINameUniqueness(t *testing.T) { 370 | for driverName := range inTreePlugins { 371 | t.Run(driverName, func(t *testing.T) { 372 | logger, _ := ktesting.NewTestContext(t) 373 | ctl := New() 374 | vs1, err := generateUniqueVolumeSource(driverName) 375 | if err != nil { 376 | t.Fatalf("Couldn't generate random source: %v", err) 377 | } 378 | pv1, err := ctl.TranslateInTreeInlineVolumeToCSI(logger, &v1.Volume{ 379 | VolumeSource: vs1, 380 | }, "") 381 | if err != nil { 382 | t.Fatalf("Error when translating to CSI: %v", err) 383 | } 384 | vs2, err := generateUniqueVolumeSource(driverName) 385 | if err != nil { 386 | t.Fatalf("Couldn't generate random source: %v", err) 387 | } 388 | pv2, err := ctl.TranslateInTreeInlineVolumeToCSI(logger, &v1.Volume{ 389 | VolumeSource: vs2, 390 | }, "") 391 | if err != nil { 392 | t.Fatalf("Error when translating to CSI: %v", err) 393 | } 394 | if pv1 == nil || pv2 == nil { 395 | t.Fatalf("Did not expect either pv1: %v or pv2: %v to be nil", pv1, pv2) 396 | } 397 | if pv1.Name == pv2.Name { 398 | t.Errorf("PV name %s not sufficiently unique for different volumes", pv1.Name) 399 | } 400 | }) 401 | 402 | } 403 | } 404 | 405 | func generateUniqueVolumeSource(driverName string) (v1.VolumeSource, error) { 406 | switch driverName { 407 | case plugins.GCEPDDriverName: 408 | return v1.VolumeSource{ 409 | GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 410 | PDName: string(uuid.NewUUID()), 411 | }, 412 | }, nil 413 | case plugins.AWSEBSDriverName: 414 | return v1.VolumeSource{ 415 | AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 416 | VolumeID: string(uuid.NewUUID()), 417 | }, 418 | }, nil 419 | 420 | case plugins.CinderDriverName: 421 | return v1.VolumeSource{ 422 | Cinder: &v1.CinderVolumeSource{ 423 | VolumeID: string(uuid.NewUUID()), 424 | }, 425 | }, nil 426 | case plugins.AzureDiskDriverName: 427 | return v1.VolumeSource{ 428 | AzureDisk: &v1.AzureDiskVolumeSource{ 429 | DiskName: string(uuid.NewUUID()), 430 | DataDiskURI: string(uuid.NewUUID()), 431 | }, 432 | }, nil 433 | case plugins.AzureFileDriverName: 434 | return v1.VolumeSource{ 435 | AzureFile: &v1.AzureFileVolumeSource{ 436 | SecretName: string(uuid.NewUUID()), 437 | ShareName: string(uuid.NewUUID()), 438 | }, 439 | }, nil 440 | case plugins.VSphereDriverName: 441 | return v1.VolumeSource{ 442 | VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{ 443 | VolumePath: " [vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-" + string(uuid.NewUUID()+".vmdk"), 444 | FSType: "ext4", 445 | }, 446 | }, nil 447 | case plugins.PortworxDriverName: 448 | return v1.VolumeSource{ 449 | PortworxVolume: &v1.PortworxVolumeSource{ 450 | VolumeID: string(uuid.NewUUID()), 451 | }, 452 | }, nil 453 | default: 454 | return v1.VolumeSource{}, fmt.Errorf("couldn't find logic for driver: %v", driverName) 455 | } 456 | } 457 | 458 | func TestPluginNameMappings(t *testing.T) { 459 | testCases := []struct { 460 | name string 461 | inTreePluginName string 462 | csiPluginName string 463 | }{ 464 | { 465 | name: "GCE PD plugin name", 466 | inTreePluginName: "kubernetes.io/gce-pd", 467 | csiPluginName: "pd.csi.storage.gke.io", 468 | }, 469 | { 470 | name: "AWS EBS plugin name", 471 | inTreePluginName: "kubernetes.io/aws-ebs", 472 | csiPluginName: "ebs.csi.aws.com", 473 | }, 474 | } 475 | for _, test := range testCases { 476 | t.Logf("Testing %v", test.name) 477 | ctl := New() 478 | csiPluginName, err := ctl.GetCSINameFromInTreeName(test.inTreePluginName) 479 | if err != nil { 480 | t.Errorf("Error when mapping In-tree plugin name to CSI plugin name %s", err) 481 | } 482 | if !ctl.IsMigratedCSIDriverByName(csiPluginName) { 483 | t.Errorf("%s expected to supersede an In-tree plugin", csiPluginName) 484 | } 485 | inTreePluginName, err := ctl.GetInTreeNameFromCSIName(csiPluginName) 486 | if err != nil { 487 | t.Errorf("Error when mapping CSI plugin name to In-tree plugin name %s", err) 488 | } 489 | if !ctl.IsMigratableIntreePluginByName(inTreePluginName) { 490 | t.Errorf("%s expected to be migratable to a CSI name", inTreePluginName) 491 | } 492 | if inTreePluginName != test.inTreePluginName || csiPluginName != test.csiPluginName { 493 | t.Errorf("CSI plugin name and In-tree plugin name do not map to each other: [%s => %s], [%s => %s]", test.csiPluginName, inTreePluginName, test.inTreePluginName, csiPluginName) 494 | } 495 | } 496 | } 497 | 498 | // TODO: test for not modifying the original PV. 499 | --------------------------------------------------------------------------------