Merge pull request #122857 from nilo19/chore/cleanup-azure
chore: Cleanup in-tree credential provider azure and cloud provider a…
This commit is contained in:
		
							
								
								
									
										25
									
								
								LICENSES/vendor/github.com/Azure/azure-sdk-for-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								LICENSES/vendor/github.com/Azure/azure-sdk-for-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| = vendor/github.com/Azure/azure-sdk-for-go licensed under: = | ||||
|  | ||||
| The MIT License (MIT) | ||||
|  | ||||
| Copyright (c) Microsoft Corporation. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|  | ||||
| = vendor/github.com/Azure/azure-sdk-for-go/LICENSE.txt 4f7454c9bcbb0acee6d9a971001befe2 | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/adal/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/adal/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest/adal licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/date/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/date/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest/date licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/mocks/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/mocks/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest/mocks licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/to/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/to/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest/to licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/validation/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/autorest/validation/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/autorest/validation licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/logger/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/logger/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/logger licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/tracing/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										195
									
								
								LICENSES/vendor/github.com/Azure/go-autorest/tracing/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,195 +0,0 @@ | ||||
| = vendor/github.com/Azure/go-autorest/tracing licensed under: = | ||||
|  | ||||
|  | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
|  | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
|  | ||||
|    1. Definitions. | ||||
|  | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
|  | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
|  | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
|  | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
|  | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
|  | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
|  | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
|  | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
|  | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
|  | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
|  | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
|  | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
|  | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
|  | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
|  | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
|  | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
|  | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
|  | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
|  | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
|  | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
|  | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
|  | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
|  | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    Copyright 2015 Microsoft Corporation | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
| = vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b | ||||
							
								
								
									
										24
									
								
								LICENSES/vendor/github.com/gofrs/uuid/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								LICENSES/vendor/github.com/gofrs/uuid/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,24 +0,0 @@ | ||||
| = vendor/github.com/gofrs/uuid licensed under: = | ||||
|  | ||||
| Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru> | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of this software and associated documentation files (the | ||||
| "Software"), to deal in the Software without restriction, including | ||||
| without limitation the rights to use, copy, modify, merge, publish, | ||||
| distribute, sublicense, and/or sell copies of the Software, and to | ||||
| permit persons to whom the Software is furnished to do so, subject to | ||||
| the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be | ||||
| included in all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
|  | ||||
| = vendor/github.com/gofrs/uuid/LICENSE ae4ba217c6e20c2d8f48f69966b9121b | ||||
							
								
								
									
										26
									
								
								LICENSES/vendor/github.com/rubiojr/go-vhd/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								LICENSES/vendor/github.com/rubiojr/go-vhd/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,26 +0,0 @@ | ||||
| = vendor/github.com/rubiojr/go-vhd licensed under: = | ||||
|  | ||||
| The MIT License (MIT) | ||||
|  | ||||
| Copyright (c) 2015 Sergio Rubio | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|  | ||||
|  | ||||
| = vendor/github.com/rubiojr/go-vhd/LICENSE 9ce5db55ba47444787183e59733e1977 | ||||
| @@ -25,7 +25,6 @@ package main | ||||
| import ( | ||||
| 	// NOTE: Importing all in-tree cloud-providers is not required when | ||||
| 	// implementing an out-of-tree cloud-provider. | ||||
| 	_ "k8s.io/legacy-cloud-providers/azure" | ||||
| 	_ "k8s.io/legacy-cloud-providers/gce" | ||||
| 	_ "k8s.io/legacy-cloud-providers/vsphere" | ||||
| ) | ||||
|   | ||||
| @@ -29,7 +29,6 @@ import ( | ||||
| 	"k8s.io/component-base/version/verflag" | ||||
|  | ||||
| 	// ensure libs have a chance to globally register their flags | ||||
| 	_ "k8s.io/kubernetes/pkg/credentialprovider/azure" | ||||
| 	_ "k8s.io/kubernetes/pkg/credentialprovider/gcp" | ||||
| ) | ||||
|  | ||||
| @@ -60,16 +59,6 @@ func register(global *flag.FlagSet, local *pflag.FlagSet, globalName string) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pflagRegister adds a flag to local that targets the Value associated with the Flag named globalName in global | ||||
| func pflagRegister(global, local *pflag.FlagSet, globalName string) { | ||||
| 	if f := global.Lookup(globalName); f != nil { | ||||
| 		f.Name = normalize(f.Name) | ||||
| 		local.AddFlag(f) | ||||
| 	} else { | ||||
| 		panic(fmt.Sprintf("failed to find flag in global flagset (pflag): %s", globalName)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // registerDeprecated registers the flag with register, and then marks it deprecated | ||||
| func registerDeprecated(global *flag.FlagSet, local *pflag.FlagSet, globalName, deprecated string) { | ||||
| 	register(global, local, globalName) | ||||
| @@ -79,10 +68,7 @@ func registerDeprecated(global *flag.FlagSet, local *pflag.FlagSet, globalName, | ||||
| // addCredentialProviderFlags adds flags from k8s.io/kubernetes/pkg/credentialprovider | ||||
| func addCredentialProviderFlags(fs *pflag.FlagSet) { | ||||
| 	// lookup flags in global flag set and re-register the values with our flagset | ||||
| 	global := pflag.CommandLine | ||||
| 	local := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) | ||||
|  | ||||
| 	addLegacyCloudProviderCredentialProviderFlags(global, local) | ||||
|  | ||||
| 	fs.AddFlagSet(local) | ||||
| } | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package options | ||||
|  | ||||
| import ( | ||||
| 	"github.com/spf13/pflag" | ||||
| ) | ||||
|  | ||||
| func addLegacyCloudProviderCredentialProviderFlags(global, local *pflag.FlagSet) { | ||||
| 	// TODO(#58034): This is not a static file, so it's not quite as straightforward as --google-json-key. | ||||
| 	// We need to figure out how ACR users can dynamically provide pull credentials before we can deprecate this. | ||||
| 	pflagRegister(global, local, "azure-container-registry-config") | ||||
| 	local.MarkDeprecated("azure-container-registry-config", "Use --image-credential-provider-config and --image-credential-provider-bin-dir to setup acr credential provider instead. Will be removed in a future release.") | ||||
| } | ||||
| @@ -21,7 +21,6 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	// Credential providers | ||||
| 	_ "k8s.io/kubernetes/pkg/credentialprovider/azure" | ||||
| 	_ "k8s.io/kubernetes/pkg/credentialprovider/gcp" | ||||
|  | ||||
| 	"k8s.io/component-base/featuregate" | ||||
|   | ||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| @@ -10,9 +10,6 @@ go 1.21 | ||||
|  | ||||
| require ( | ||||
| 	bitbucket.org/bertimus9/systemstat v0.5.0 | ||||
| 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible | ||||
| 	github.com/Azure/go-autorest/autorest v0.11.29 | ||||
| 	github.com/Azure/go-autorest/autorest/adal v0.9.23 | ||||
| 	github.com/GoogleCloudPlatform/k8s-cloud-provider v1.18.1-0.20220218231025-f11817397a1b | ||||
| 	github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab | ||||
| 	github.com/Microsoft/go-winio v0.6.0 | ||||
| @@ -135,13 +132,6 @@ require ( | ||||
| 	cloud.google.com/go/compute v1.23.0 // indirect | ||||
| 	cloud.google.com/go/compute/metadata v0.2.3 // indirect | ||||
| 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect | ||||
| 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect | ||||
| 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect | ||||
| 	github.com/Azure/go-autorest/autorest/mocks v0.4.2 // indirect | ||||
| 	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect | ||||
| 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect | ||||
| 	github.com/Azure/go-autorest/logger v0.2.1 // indirect | ||||
| 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect | ||||
| 	github.com/MakeNowJust/heredoc v1.0.0 // indirect | ||||
| 	github.com/NYTimes/gziphandler v1.1.1 // indirect | ||||
| 	github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect | ||||
| @@ -171,7 +161,6 @@ require ( | ||||
| 	github.com/go-openapi/jsonreference v0.20.2 // indirect | ||||
| 	github.com/go-openapi/swag v0.22.3 // indirect | ||||
| 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect | ||||
| 	github.com/gofrs/uuid v4.4.0+incompatible // indirect | ||||
| 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect | ||||
| 	github.com/google/btree v1.0.1 // indirect | ||||
| 	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect | ||||
| @@ -209,7 +198,6 @@ require ( | ||||
| 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect | ||||
| 	github.com/pquerna/cachecontrol v0.1.0 // indirect | ||||
| 	github.com/prometheus/procfs v0.10.1 // indirect | ||||
| 	github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 // indirect | ||||
| 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||||
| 	github.com/seccomp/libseccomp-golang v0.10.0 // indirect | ||||
| 	github.com/sirupsen/logrus v1.9.0 // indirect | ||||
|   | ||||
							
								
								
									
										45
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								go.sum
									
									
									
									
									
								
							| @@ -166,30 +166,8 @@ cloud.google.com/go/webrisk v1.9.1/go.mod h1:4GCmXKcOa2BZcZPn6DCEvE7HypmEJcJkr4m | ||||
| cloud.google.com/go/websecurityscanner v1.6.1/go.mod h1:Njgaw3rttgRHXzwCB8kgCYqv5/rGpFCsBOvPbYgszpg= | ||||
| cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= | ||||
| dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||
| github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= | ||||
| github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= | ||||
| github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= | ||||
| github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= | ||||
| github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= | ||||
| github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= | ||||
| github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= | ||||
| github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= | ||||
| github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= | ||||
| github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= | ||||
| github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= | ||||
| github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= | ||||
| github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= | ||||
| github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= | ||||
| github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= | ||||
| github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= | ||||
| github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= | ||||
| github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= | ||||
| github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= | ||||
| github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= | ||||
| github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= | ||||
| github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= | ||||
| github.com/GoogleCloudPlatform/k8s-cloud-provider v1.18.1-0.20220218231025-f11817397a1b h1:Heo1J/ttaQFgGJSVnCZquy3e5eH5j1nqxBuomztB3P0= | ||||
| @@ -317,8 +295,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm | ||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||
| github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= | ||||
| github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= | ||||
| github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= | ||||
| github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= | ||||
| github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= | ||||
| github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= | ||||
| github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= | ||||
| @@ -399,14 +375,11 @@ github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= | ||||
| github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= | ||||
| github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= | ||||
| github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||
| github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | ||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||
| github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= | ||||
| github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= | ||||
| github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= | ||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||
| @@ -708,8 +681,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L | ||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||
| github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= | ||||
| github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= | ||||
| github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 h1:if3/24+h9Sq6eDx8UUz1SO9cT9tizyIsATfB7b4D3tc= | ||||
| github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= | ||||
| github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| @@ -760,7 +731,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | ||||
| @@ -860,9 +830,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U | ||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||
| golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= | ||||
| golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= | ||||
| golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| @@ -902,7 +869,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= | ||||
| golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| @@ -946,10 +912,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v | ||||
| golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= | ||||
| golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | ||||
| golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | ||||
| golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| @@ -981,7 +944,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ | ||||
| golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= | ||||
| golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| @@ -1045,15 +1007,11 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc | ||||
| golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | ||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||
| golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= | ||||
| golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= | ||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| @@ -1064,8 +1022,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| @@ -1131,7 +1087,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= | ||||
| golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
|   | ||||
| @@ -20,7 +20,6 @@ | ||||
|       "github.com/go-openapi/validate": "use k8s.io/kube-openapi/pkg/validation/validate instead", | ||||
|       "github.com/gogo/googleapis": "depends on unmaintained github.com/gogo/protobuf", | ||||
|       "github.com/gogo/protobuf": "unmaintained", | ||||
|       "github.com/golang/mock": "unmaintained, archive mode", | ||||
|       "github.com/google/s2a-go": "cloud dependency, unstable", | ||||
|       "github.com/google/shlex": "unmaintained, archive mode", | ||||
|       "github.com/googleapis/enterprise-certificate-proxy": "references cloud dependencies", | ||||
| @@ -55,7 +54,6 @@ | ||||
|       "github.com/mndrix/tap-go": "unmaintained", | ||||
|       "github.com/onsi/ginkgo": "Ginkgo has been migrated to V2, refer to #109111", | ||||
|       "github.com/pkg/errors": "unmaintained, archive mode", | ||||
|       "github.com/rubiojr/go-vhd": "unmaintained, archive mode", | ||||
|       "github.com/smartystreets/goconvey": "MPL license not in CNCF allowlist", | ||||
|       "github.com/spf13/viper": "refer to #102598", | ||||
|       "github.com/xeipuuv/gojsonschema": "unmaintained", | ||||
| @@ -126,10 +124,6 @@ | ||||
|         "k8s.io/kubernetes", | ||||
|         "k8s.io/metrics" | ||||
|       ], | ||||
|       "github.com/golang/mock": [ | ||||
|         "k8s.io/kubernetes", | ||||
|         "k8s.io/legacy-cloud-providers" | ||||
|       ], | ||||
|       "github.com/google/s2a-go": [ | ||||
|         "cloud.google.com/go/compute", | ||||
|         "google.golang.org/api" | ||||
| @@ -187,9 +181,6 @@ | ||||
|         "sigs.k8s.io/kustomize/api", | ||||
|         "sigs.k8s.io/kustomize/kustomize/v5" | ||||
|       ], | ||||
|       "github.com/rubiojr/go-vhd": [ | ||||
|         "k8s.io/legacy-cloud-providers" | ||||
|       ], | ||||
|       "go.opencensus.io": [ | ||||
|         "cloud.google.com/go/compute", | ||||
|         "github.com/Microsoft/hcsshim", | ||||
| @@ -238,7 +229,6 @@ | ||||
|       "cloud.google.com/go/compute", | ||||
|       "github.com/GoogleCloudPlatform/k8s-cloud-provider", | ||||
|       "github.com/gogo/protobuf", | ||||
|       "github.com/golang/mock", | ||||
|       "github.com/google/s2a-go", | ||||
|       "github.com/google/shlex", | ||||
|       "github.com/googleapis/enterprise-certificate-proxy", | ||||
| @@ -249,7 +239,6 @@ | ||||
|       "github.com/json-iterator/go", | ||||
|       "github.com/mailru/easyjson", | ||||
|       "github.com/pkg/errors", | ||||
|       "github.com/rubiojr/go-vhd", | ||||
|       "go.opencensus.io", | ||||
|       "golang.org/x/exp", | ||||
|       "google.golang.org/api", | ||||
|   | ||||
| @@ -21,7 +21,6 @@ package cloudprovider | ||||
|  | ||||
| import ( | ||||
| 	// Cloud providers | ||||
| 	_ "k8s.io/legacy-cloud-providers/azure" | ||||
| 	_ "k8s.io/legacy-cloud-providers/gce" | ||||
| 	_ "k8s.io/legacy-cloud-providers/vsphere" | ||||
| ) | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| # See the OWNERS docs at https://go.k8s.io/owners | ||||
|  | ||||
| approvers: | ||||
|   - andyzhangx | ||||
|   - feiskyer | ||||
|   - khenidak | ||||
| reviewers: | ||||
|   - andyzhangx | ||||
|   - feiskyer | ||||
|   - khenidak | ||||
| emeritus_approvers: | ||||
|   - karataliu | ||||
|   - brendandburns | ||||
| @@ -1,291 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| /* | ||||
| Copyright 2017 Microsoft Corporation | ||||
|  | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) Microsoft Corporation. All rights reserved. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE | ||||
| */ | ||||
|  | ||||
| // Source: https://github.com/Azure/acr-docker-credential-helper/blob/a79b541f3ee761f6cc4511863ed41fb038c19464/src/docker-credential-acr/acr_login.go | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
|  | ||||
| 	utilnet "k8s.io/apimachinery/pkg/util/net" | ||||
| ) | ||||
|  | ||||
| type authDirective struct { | ||||
| 	service string | ||||
| 	realm   string | ||||
| } | ||||
|  | ||||
| type acrAuthResponse struct { | ||||
| 	RefreshToken string `json:"refresh_token"` | ||||
| } | ||||
|  | ||||
| // 5 minutes buffer time to allow timeshift between local machine and AAD | ||||
| const userAgentHeader = "User-Agent" | ||||
| const userAgent = "kubernetes-credentialprovider-acr" | ||||
|  | ||||
| const dockerTokenLoginUsernameGUID = "00000000-0000-0000-0000-000000000000" | ||||
|  | ||||
| var client = &http.Client{ | ||||
| 	Transport: utilnet.SetTransportDefaults(&http.Transport{}), | ||||
| 	Timeout:   time.Second * 60, | ||||
| } | ||||
|  | ||||
| func receiveChallengeFromLoginServer(serverAddress string) (*authDirective, error) { | ||||
| 	challengeURL := url.URL{ | ||||
| 		Scheme: "https", | ||||
| 		Host:   serverAddress, | ||||
| 		Path:   "v2/", | ||||
| 	} | ||||
| 	var err error | ||||
| 	var r *http.Request | ||||
| 	r, err = http.NewRequest("GET", challengeURL.String(), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to construct request, got %v", err) | ||||
| 	} | ||||
| 	r.Header.Add(userAgentHeader, userAgent) | ||||
|  | ||||
| 	var challenge *http.Response | ||||
| 	if challenge, err = client.Do(r); err != nil { | ||||
| 		return nil, fmt.Errorf("error reaching registry endpoint %s, error: %s", challengeURL.String(), err) | ||||
| 	} | ||||
| 	defer challenge.Body.Close() | ||||
|  | ||||
| 	if challenge.StatusCode != 401 { | ||||
| 		return nil, fmt.Errorf("registry did not issue a valid AAD challenge, status: %d", challenge.StatusCode) | ||||
| 	} | ||||
|  | ||||
| 	var authHeader []string | ||||
| 	var ok bool | ||||
| 	if authHeader, ok = challenge.Header["Www-Authenticate"]; !ok { | ||||
| 		return nil, fmt.Errorf("challenge response does not contain header 'Www-Authenticate'") | ||||
| 	} | ||||
|  | ||||
| 	if len(authHeader) != 1 { | ||||
| 		return nil, fmt.Errorf("registry did not issue a valid AAD challenge, authenticate header [%s]", | ||||
| 			strings.Join(authHeader, ", ")) | ||||
| 	} | ||||
|  | ||||
| 	authSections := strings.SplitN(authHeader[0], " ", 2) | ||||
| 	authType := strings.ToLower(authSections[0]) | ||||
| 	var authParams *map[string]string | ||||
| 	if authParams, err = parseAssignments(authSections[1]); err != nil { | ||||
| 		return nil, fmt.Errorf("unable to understand the contents of Www-Authenticate header %s", authSections[1]) | ||||
| 	} | ||||
|  | ||||
| 	// verify headers | ||||
| 	if !strings.EqualFold("Bearer", authType) { | ||||
| 		return nil, fmt.Errorf("Www-Authenticate: expected realm: Bearer, actual: %s", authType) | ||||
| 	} | ||||
| 	if len((*authParams)["service"]) == 0 { | ||||
| 		return nil, fmt.Errorf("Www-Authenticate: missing header \"service\"") | ||||
| 	} | ||||
| 	if len((*authParams)["realm"]) == 0 { | ||||
| 		return nil, fmt.Errorf("Www-Authenticate: missing header \"realm\"") | ||||
| 	} | ||||
|  | ||||
| 	return &authDirective{ | ||||
| 		service: (*authParams)["service"], | ||||
| 		realm:   (*authParams)["realm"], | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func performTokenExchange( | ||||
| 	serverAddress string, | ||||
| 	directive *authDirective, | ||||
| 	tenant string, | ||||
| 	accessToken string) (string, error) { | ||||
| 	var err error | ||||
| 	data := url.Values{ | ||||
| 		"service":       []string{directive.service}, | ||||
| 		"grant_type":    []string{"access_token_refresh_token"}, | ||||
| 		"access_token":  []string{accessToken}, | ||||
| 		"refresh_token": []string{accessToken}, | ||||
| 		"tenant":        []string{tenant}, | ||||
| 	} | ||||
|  | ||||
| 	var realmURL *url.URL | ||||
| 	if realmURL, err = url.Parse(directive.realm); err != nil { | ||||
| 		return "", fmt.Errorf("Www-Authenticate: invalid realm %s", directive.realm) | ||||
| 	} | ||||
| 	authEndpoint := fmt.Sprintf("%s://%s/oauth2/exchange", realmURL.Scheme, realmURL.Host) | ||||
|  | ||||
| 	datac := data.Encode() | ||||
| 	var r *http.Request | ||||
| 	r, err = http.NewRequest("POST", authEndpoint, bytes.NewBufferString(datac)) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to construct request, got %v", err) | ||||
| 	} | ||||
| 	r.Header.Add(userAgentHeader, userAgent) | ||||
| 	r.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||||
| 	r.Header.Add("Content-Length", strconv.Itoa(len(datac))) | ||||
|  | ||||
| 	var exchange *http.Response | ||||
| 	if exchange, err = client.Do(r); err != nil { | ||||
| 		return "", fmt.Errorf("Www-Authenticate: failed to reach auth url %s", authEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	defer exchange.Body.Close() | ||||
| 	if exchange.StatusCode != 200 { | ||||
| 		return "", fmt.Errorf("Www-Authenticate: auth url %s responded with status code %d", authEndpoint, exchange.StatusCode) | ||||
| 	} | ||||
|  | ||||
| 	var content []byte | ||||
| 	limitedReader := &io.LimitedReader{R: exchange.Body, N: maxReadLength} | ||||
| 	if content, err = io.ReadAll(limitedReader); err != nil { | ||||
| 		return "", fmt.Errorf("Www-Authenticate: error reading response from %s", authEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	if limitedReader.N <= 0 { | ||||
| 		return "", errors.New("the read limit is reached") | ||||
| 	} | ||||
|  | ||||
| 	var authResp acrAuthResponse | ||||
| 	if err = json.Unmarshal(content, &authResp); err != nil { | ||||
| 		return "", fmt.Errorf("Www-Authenticate: unable to read response %s", content) | ||||
| 	} | ||||
|  | ||||
| 	return authResp.RefreshToken, nil | ||||
| } | ||||
|  | ||||
| // Try and parse a string of assignments in the form of: | ||||
| // key1 = value1, key2 = "value 2", key3 = "" | ||||
| // Note: this method and handle quotes but does not handle escaping of quotes | ||||
| func parseAssignments(statements string) (*map[string]string, error) { | ||||
| 	var cursor int | ||||
| 	result := make(map[string]string) | ||||
| 	var errorMsg = fmt.Errorf("malformed header value: %s", statements) | ||||
| 	for { | ||||
| 		// parse key | ||||
| 		equalIndex := nextOccurrence(statements, cursor, "=") | ||||
| 		if equalIndex == -1 { | ||||
| 			return nil, errorMsg | ||||
| 		} | ||||
| 		key := strings.TrimSpace(statements[cursor:equalIndex]) | ||||
|  | ||||
| 		// parse value | ||||
| 		cursor = nextNoneSpace(statements, equalIndex+1) | ||||
| 		if cursor == -1 { | ||||
| 			return nil, errorMsg | ||||
| 		} | ||||
| 		// case: value is quoted | ||||
| 		if statements[cursor] == '"' { | ||||
| 			cursor = cursor + 1 | ||||
| 			// like I said, not handling escapes, but this will skip any comma that's | ||||
| 			// within the quotes which is somewhat more likely | ||||
| 			closeQuoteIndex := nextOccurrence(statements, cursor, "\"") | ||||
| 			if closeQuoteIndex == -1 { | ||||
| 				return nil, errorMsg | ||||
| 			} | ||||
| 			value := statements[cursor:closeQuoteIndex] | ||||
| 			result[key] = value | ||||
|  | ||||
| 			commaIndex := nextNoneSpace(statements, closeQuoteIndex+1) | ||||
| 			if commaIndex == -1 { | ||||
| 				// no more comma, done | ||||
| 				return &result, nil | ||||
| 			} else if statements[commaIndex] != ',' { | ||||
| 				// expect comma immediately after close quote | ||||
| 				return nil, errorMsg | ||||
| 			} else { | ||||
| 				cursor = commaIndex + 1 | ||||
| 			} | ||||
| 		} else { | ||||
| 			commaIndex := nextOccurrence(statements, cursor, ",") | ||||
| 			endStatements := commaIndex == -1 | ||||
| 			var untrimmed string | ||||
| 			if endStatements { | ||||
| 				untrimmed = statements[cursor:commaIndex] | ||||
| 			} else { | ||||
| 				untrimmed = statements[cursor:] | ||||
| 			} | ||||
| 			value := strings.TrimSpace(untrimmed) | ||||
|  | ||||
| 			if len(value) == 0 { | ||||
| 				// disallow empty value without quote | ||||
| 				return nil, errorMsg | ||||
| 			} | ||||
|  | ||||
| 			result[key] = value | ||||
|  | ||||
| 			if endStatements { | ||||
| 				return &result, nil | ||||
| 			} | ||||
| 			cursor = commaIndex + 1 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func nextOccurrence(str string, start int, sep string) int { | ||||
| 	if start >= len(str) { | ||||
| 		return -1 | ||||
| 	} | ||||
| 	offset := strings.Index(str[start:], sep) | ||||
| 	if offset == -1 { | ||||
| 		return -1 | ||||
| 	} | ||||
| 	return offset + start | ||||
| } | ||||
|  | ||||
| func nextNoneSpace(str string, start int) int { | ||||
| 	if start >= len(str) { | ||||
| 		return -1 | ||||
| 	} | ||||
| 	offset := strings.IndexFunc(str[start:], func(c rune) bool { return !unicode.IsSpace(c) }) | ||||
| 	if offset == -1 { | ||||
| 		return -1 | ||||
| 	} | ||||
| 	return offset + start | ||||
| } | ||||
| @@ -1,350 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry" | ||||
| 	"github.com/Azure/go-autorest/autorest/adal" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"github.com/spf13/pflag" | ||||
|  | ||||
| 	"k8s.io/client-go/tools/cache" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/kubernetes/pkg/credentialprovider" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/auth" | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
|  | ||||
| var flagConfigFile = pflag.String("azure-container-registry-config", "", | ||||
| 	"Path to the file containing Azure container registry configuration information.") | ||||
|  | ||||
| const ( | ||||
| 	dummyRegistryEmail = "name@contoso.com" | ||||
| 	maxReadLength      = 10 * 1 << 20 // 10MB | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	containerRegistryUrls = []string{"*.azurecr.io", "*.azurecr.cn", "*.azurecr.de", "*.azurecr.us"} | ||||
| 	acrRE                 = regexp.MustCompile(`.*\.azurecr\.io|.*\.azurecr\.cn|.*\.azurecr\.de|.*\.azurecr\.us`) | ||||
| 	warnOnce              sync.Once | ||||
| ) | ||||
|  | ||||
| // init registers the various means by which credentials may | ||||
| // be resolved on Azure. | ||||
| func init() { | ||||
| 	credentialprovider.RegisterCredentialProvider( | ||||
| 		"azure", | ||||
| 		NewACRProvider(flagConfigFile), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| type cacheEntry struct { | ||||
| 	expiresAt   time.Time | ||||
| 	credentials credentialprovider.DockerConfigEntry | ||||
| 	registry    string | ||||
| } | ||||
|  | ||||
| // acrExpirationPolicy implements ExpirationPolicy from client-go. | ||||
| type acrExpirationPolicy struct{} | ||||
|  | ||||
| // stringKeyFunc returns the cache key as a string | ||||
| func stringKeyFunc(obj interface{}) (string, error) { | ||||
| 	key := obj.(*cacheEntry).registry | ||||
| 	return key, nil | ||||
| } | ||||
|  | ||||
| // IsExpired checks if the ACR credentials are expired. | ||||
| func (p *acrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool { | ||||
| 	return time.Now().After(entry.Obj.(*cacheEntry).expiresAt) | ||||
| } | ||||
|  | ||||
| // RegistriesClient is a testable interface for the ACR client List operation. | ||||
| type RegistriesClient interface { | ||||
| 	List(ctx context.Context) ([]containerregistry.Registry, error) | ||||
| } | ||||
|  | ||||
| // NewACRProvider parses the specified configFile and returns a DockerConfigProvider | ||||
| func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider { | ||||
| 	return &acrProvider{ | ||||
| 		file:  configFile, | ||||
| 		cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type acrProvider struct { | ||||
| 	file                  *string | ||||
| 	config                *auth.AzureAuthConfig | ||||
| 	environment           *azure.Environment | ||||
| 	servicePrincipalToken *adal.ServicePrincipalToken | ||||
| 	cache                 cache.Store | ||||
| } | ||||
|  | ||||
| // ParseConfig returns a parsed configuration for an Azure cloudprovider config file | ||||
| func parseConfig(configReader io.Reader) (*auth.AzureAuthConfig, error) { | ||||
| 	var config auth.AzureAuthConfig | ||||
|  | ||||
| 	if configReader == nil { | ||||
| 		return &config, nil | ||||
| 	} | ||||
|  | ||||
| 	limitedReader := &io.LimitedReader{R: configReader, N: maxReadLength} | ||||
| 	configContents, err := io.ReadAll(limitedReader) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if limitedReader.N <= 0 { | ||||
| 		return nil, errors.New("the read limit is reached") | ||||
| 	} | ||||
| 	err = yaml.Unmarshal(configContents, &config) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &config, nil | ||||
| } | ||||
|  | ||||
| func (a *acrProvider) loadConfig(rdr io.Reader) error { | ||||
| 	var err error | ||||
| 	a.config, err = parseConfig(rdr) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("Failed to load azure credential file: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	a.environment, err = auth.ParseAzureEnvironment(a.config.Cloud, a.config.ResourceManagerEndpoint, a.config.IdentitySystem) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *acrProvider) Enabled() bool { | ||||
| 	if a.file == nil || len(*a.file) == 0 { | ||||
| 		klog.V(5).Infof("Azure config unspecified, disabling") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if credentialprovider.AreLegacyCloudCredentialProvidersDisabled() { | ||||
| 		warnOnce.Do(func() { | ||||
| 			klog.V(4).Infof("Azure credential provider is now disabled. Please refer to sig-cloud-provider for guidance on external credential provider integration for Azure") | ||||
| 		}) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	f, err := os.Open(*a.file) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("Failed to load config from file: %s", *a.file) | ||||
| 		return false | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	err = a.loadConfig(f) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("Failed to load config from file: %s", *a.file) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	a.servicePrincipalToken, err = auth.GetServicePrincipalToken(a.config, a.environment) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("Failed to create service principal token: %v", err) | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // getFromCache attempts to get credentials from the cache | ||||
| func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.DockerConfig, bool) { | ||||
| 	cfg := credentialprovider.DockerConfig{} | ||||
| 	obj, exists, err := a.cache.GetByKey(loginServer) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("error getting ACR credentials from cache: %v", err) | ||||
| 		return cfg, false | ||||
| 	} | ||||
| 	if !exists { | ||||
| 		return cfg, false | ||||
| 	} | ||||
|  | ||||
| 	entry := obj.(*cacheEntry) | ||||
| 	cfg[entry.registry] = entry.credentials | ||||
| 	return cfg, true | ||||
| } | ||||
|  | ||||
| // getFromACR gets credentials from ACR since they are not in the cache | ||||
| func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerConfig, error) { | ||||
| 	cfg := credentialprovider.DockerConfig{} | ||||
| 	cred, err := getACRDockerEntryFromARMToken(a, loginServer) | ||||
| 	if err != nil { | ||||
| 		return cfg, err | ||||
| 	} | ||||
|  | ||||
| 	entry := &cacheEntry{ | ||||
| 		expiresAt:   time.Now().Add(10 * time.Minute), | ||||
| 		credentials: *cred, | ||||
| 		registry:    loginServer, | ||||
| 	} | ||||
| 	if err := a.cache.Add(entry); err != nil { | ||||
| 		return cfg, err | ||||
| 	} | ||||
| 	cfg[loginServer] = *cred | ||||
| 	return cfg, nil | ||||
| } | ||||
|  | ||||
| func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig { | ||||
| 	loginServer := a.parseACRLoginServerFromImage(image) | ||||
| 	if loginServer == "" { | ||||
| 		klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image) | ||||
| 		return credentialprovider.DockerConfig{} | ||||
| 	} | ||||
|  | ||||
| 	cfg := credentialprovider.DockerConfig{} | ||||
| 	if a.config != nil && a.config.UseManagedIdentityExtension { | ||||
| 		var exists bool | ||||
| 		cfg, exists = a.getFromCache(loginServer) | ||||
| 		if exists { | ||||
| 			klog.V(4).Infof("Got ACR credentials from cache for %s", loginServer) | ||||
| 		} else { | ||||
| 			klog.V(2).Infof("unable to get ACR credentials from cache for %s, checking ACR API", loginServer) | ||||
| 			var err error | ||||
| 			cfg, err = a.getFromACR(loginServer) | ||||
| 			if err != nil { | ||||
| 				klog.Errorf("error getting credentials from ACR for %s %v", loginServer, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Add our entry for each of the supported container registry URLs | ||||
| 		for _, url := range containerRegistryUrls { | ||||
| 			cred := &credentialprovider.DockerConfigEntry{ | ||||
| 				Username: a.config.AADClientID, | ||||
| 				Password: a.config.AADClientSecret, | ||||
| 				Email:    dummyRegistryEmail, | ||||
| 			} | ||||
| 			cfg[url] = *cred | ||||
| 		} | ||||
|  | ||||
| 		// Handle the custom cloud case | ||||
| 		// In clouds where ACR is not yet deployed, the string will be empty | ||||
| 		if a.environment != nil && strings.Contains(a.environment.ContainerRegistryDNSSuffix, ".azurecr.") { | ||||
| 			customAcrSuffix := "*" + a.environment.ContainerRegistryDNSSuffix | ||||
| 			hasBeenAdded := false | ||||
| 			for _, url := range containerRegistryUrls { | ||||
| 				if strings.EqualFold(url, customAcrSuffix) { | ||||
| 					hasBeenAdded = true | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if !hasBeenAdded { | ||||
| 				cred := &credentialprovider.DockerConfigEntry{ | ||||
| 					Username: a.config.AADClientID, | ||||
| 					Password: a.config.AADClientSecret, | ||||
| 					Email:    dummyRegistryEmail, | ||||
| 				} | ||||
| 				cfg[customAcrSuffix] = *cred | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// add ACR anonymous repo support: use empty username and password for anonymous access | ||||
| 	defaultConfigEntry := credentialprovider.DockerConfigEntry{ | ||||
| 		Username: "", | ||||
| 		Password: "", | ||||
| 		Email:    dummyRegistryEmail, | ||||
| 	} | ||||
| 	cfg["*.azurecr.*"] = defaultConfigEntry | ||||
| 	return cfg | ||||
| } | ||||
|  | ||||
| func getLoginServer(registry containerregistry.Registry) string { | ||||
| 	return *(*registry.RegistryProperties).LoginServer | ||||
| } | ||||
|  | ||||
| func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) { | ||||
| 	if a.servicePrincipalToken == nil { | ||||
| 		token, err := auth.GetServicePrincipalToken(a.config, a.environment) | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("Failed to create service principal token: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		a.servicePrincipalToken = token | ||||
| 	} else { | ||||
| 		// Run EnsureFresh to make sure the token is valid and does not expire | ||||
| 		if err := a.servicePrincipalToken.EnsureFresh(); err != nil { | ||||
| 			klog.Errorf("Failed to ensure fresh service principal token: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	armAccessToken := a.servicePrincipalToken.OAuthToken() | ||||
|  | ||||
| 	klog.V(4).Infof("discovering auth redirects for: %s", loginServer) | ||||
| 	directive, err := receiveChallengeFromLoginServer(loginServer) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("failed to receive challenge: %s", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(4).Infof("exchanging an acr refresh_token") | ||||
| 	registryRefreshToken, err := performTokenExchange( | ||||
| 		loginServer, directive, a.config.TenantID, armAccessToken) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("failed to perform token exchange: %s", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer) | ||||
| 	return &credentialprovider.DockerConfigEntry{ | ||||
| 		Username: dockerTokenLoginUsernameGUID, | ||||
| 		Password: registryRefreshToken, | ||||
| 		Email:    dummyRegistryEmail, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // parseACRLoginServerFromImage takes image as parameter and returns login server of it. | ||||
| // Parameter `image` is expected in following format: foo.azurecr.io/bar/imageName:version | ||||
| // If the provided image is not an acr image, this function will return an empty string. | ||||
| func (a *acrProvider) parseACRLoginServerFromImage(image string) string { | ||||
| 	match := acrRE.FindAllString(image, -1) | ||||
| 	if len(match) == 1 { | ||||
| 		return match[0] | ||||
| 	} | ||||
|  | ||||
| 	// handle the custom cloud case | ||||
| 	if a != nil && a.environment != nil { | ||||
| 		cloudAcrSuffix := a.environment.ContainerRegistryDNSSuffix | ||||
| 		cloudAcrSuffixLength := len(cloudAcrSuffix) | ||||
| 		if cloudAcrSuffixLength > 0 { | ||||
| 			customAcrSuffixIndex := strings.Index(image, cloudAcrSuffix) | ||||
| 			if customAcrSuffixIndex != -1 { | ||||
| 				endIndex := customAcrSuffixIndex + cloudAcrSuffixLength | ||||
| 				return image[0:endIndex] | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
| @@ -1,182 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"k8s.io/client-go/tools/cache" | ||||
| 	"k8s.io/utils/pointer" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func Test(t *testing.T) { | ||||
| 	configStr := ` | ||||
|     { | ||||
|         "aadClientId": "foo", | ||||
|         "aadClientSecret": "bar" | ||||
|     }` | ||||
| 	result := []containerregistry.Registry{ | ||||
| 		{ | ||||
| 			Name: pointer.String("foo"), | ||||
| 			RegistryProperties: &containerregistry.RegistryProperties{ | ||||
| 				LoginServer: pointer.String("*.azurecr.io"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.String("bar"), | ||||
| 			RegistryProperties: &containerregistry.RegistryProperties{ | ||||
| 				LoginServer: pointer.String("*.azurecr.cn"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.String("baz"), | ||||
| 			RegistryProperties: &containerregistry.RegistryProperties{ | ||||
| 				LoginServer: pointer.String("*.azurecr.de"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.String("bus"), | ||||
| 			RegistryProperties: &containerregistry.RegistryProperties{ | ||||
| 				LoginServer: pointer.String("*.azurecr.us"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	provider := &acrProvider{ | ||||
| 		cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}), | ||||
| 	} | ||||
| 	provider.loadConfig(bytes.NewBufferString(configStr)) | ||||
|  | ||||
| 	creds := provider.Provide("foo.azurecr.io/nginx:v1") | ||||
|  | ||||
| 	if len(creds) != len(result)+1 { | ||||
| 		t.Errorf("Unexpected list: %v, expected length %d", creds, len(result)+1) | ||||
| 	} | ||||
| 	for _, cred := range creds { | ||||
| 		if cred.Username != "" && cred.Username != "foo" { | ||||
| 			t.Errorf("expected 'foo' for username, saw: %v", cred.Username) | ||||
| 		} | ||||
| 		if cred.Password != "" && cred.Password != "bar" { | ||||
| 			t.Errorf("expected 'bar' for password, saw: %v", cred.Username) | ||||
| 		} | ||||
| 	} | ||||
| 	for _, val := range result { | ||||
| 		registryName := getLoginServer(val) | ||||
| 		if _, found := creds[registryName]; !found { | ||||
| 			t.Errorf("Missing expected registry: %s", registryName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestProvide(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		desc                string | ||||
| 		image               string | ||||
| 		configStr           string | ||||
| 		expectedCredsLength int | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:  "return multiple credentials using Service Principal", | ||||
| 			image: "foo.azurecr.io/bar/image:v1", | ||||
| 			configStr: ` | ||||
|     { | ||||
|         "aadClientId": "foo", | ||||
|         "aadClientSecret": "bar" | ||||
|     }`, | ||||
| 			expectedCredsLength: 5, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:  "retuen 0 credential for non-ACR image using Managed Identity", | ||||
| 			image: "busybox", | ||||
| 			configStr: ` | ||||
|     { | ||||
| 	"UseManagedIdentityExtension": true | ||||
|     }`, | ||||
| 			expectedCredsLength: 0, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		provider := &acrProvider{ | ||||
| 			cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}), | ||||
| 		} | ||||
| 		provider.loadConfig(bytes.NewBufferString(test.configStr)) | ||||
|  | ||||
| 		creds := provider.Provide(test.image) | ||||
| 		assert.Equal(t, test.expectedCredsLength, len(creds), "TestCase[%d]: %s", i, test.desc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestParseACRLoginServerFromImage(t *testing.T) { | ||||
| 	configStr := ` | ||||
|     { | ||||
|         "aadClientId": "foo", | ||||
|         "aadClientSecret": "bar" | ||||
|     }` | ||||
|  | ||||
| 	provider := &acrProvider{} | ||||
| 	provider.loadConfig(bytes.NewBufferString(configStr)) | ||||
| 	provider.environment = &azure.Environment{ | ||||
| 		ContainerRegistryDNSSuffix: ".azurecr.my.cloud", | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		image    string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			image:    "invalidImage", | ||||
| 			expected: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "docker.io/library/busybox:latest", | ||||
| 			expected: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "foo.azurecr.io/bar/image:version", | ||||
| 			expected: "foo.azurecr.io", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "foo.azurecr.cn/bar/image:version", | ||||
| 			expected: "foo.azurecr.cn", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "foo.azurecr.de/bar/image:version", | ||||
| 			expected: "foo.azurecr.de", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "foo.azurecr.us/bar/image:version", | ||||
| 			expected: "foo.azurecr.us", | ||||
| 		}, | ||||
| 		{ | ||||
| 			image:    "foo.azurecr.my.cloud/bar/image:version", | ||||
| 			expected: "foo.azurecr.my.cloud", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		if loginServer := provider.parseACRLoginServerFromImage(test.image); loginServer != test.expected { | ||||
| 			t.Errorf("function parseACRLoginServerFromImage returns \"%s\" for image %s, expected \"%s\"", loginServer, test.image, test.expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
| @@ -40,7 +40,6 @@ var ( | ||||
| 		external bool | ||||
| 		detail   string | ||||
| 	}{ | ||||
| 		{"azure", false, "The Azure provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes-sigs/cloud-provider-azure"}, | ||||
| 		{"gce", false, "The GCE provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes/cloud-provider-gcp"}, | ||||
| 		{"vsphere", false, "The vSphere provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes/cloud-provider-vsphere"}, | ||||
| 	} | ||||
|   | ||||
| @@ -1,18 +0,0 @@ | ||||
| # See the OWNERS docs at https://go.k8s.io/owners | ||||
| # We are no longer accepting features into k8s.io/legacy-cloud-providers. | ||||
| # Any kind/feature PRs must be approved by SIG Cloud Provider going forward. | ||||
|  | ||||
| emeritus_approvers: | ||||
|   - andyzhangx | ||||
|   - brendandburns | ||||
|   - feiskyer | ||||
|   - karataliu | ||||
|   - khenidak | ||||
|   - nilo19 | ||||
| reviewers: | ||||
|   - andyzhangx | ||||
|   - aramase | ||||
|   - feiskyer | ||||
|   - khenidak | ||||
|   - ritazh | ||||
|   - nilo19 | ||||
| @@ -1,290 +0,0 @@ | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package auth | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest/adal" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"golang.org/x/crypto/pkcs12" | ||||
|  | ||||
| 	"k8s.io/klog/v2" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// ADFSIdentitySystem is the override value for tenantID on Azure Stack clouds. | ||||
| 	ADFSIdentitySystem = "adfs" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// ErrorNoAuth indicates that no credentials are provided. | ||||
| 	ErrorNoAuth = fmt.Errorf("no credentials provided for Azure cloud provider") | ||||
| ) | ||||
|  | ||||
| // AzureAuthConfig holds auth related part of cloud config | ||||
| type AzureAuthConfig struct { | ||||
| 	// The cloud environment identifier. Takes values from https://github.com/Azure/go-autorest/blob/ec5f4903f77ed9927ac95b19ab8e44ada64c1356/autorest/azure/environments.go#L13 | ||||
| 	Cloud string `json:"cloud,omitempty" yaml:"cloud,omitempty"` | ||||
| 	// The AAD Tenant ID for the Subscription that the cluster is deployed in | ||||
| 	TenantID string `json:"tenantId,omitempty" yaml:"tenantId,omitempty"` | ||||
| 	// The ClientID for an AAD application with RBAC access to talk to Azure RM APIs | ||||
| 	AADClientID string `json:"aadClientId,omitempty" yaml:"aadClientId,omitempty"` | ||||
| 	// The ClientSecret for an AAD application with RBAC access to talk to Azure RM APIs | ||||
| 	AADClientSecret string `json:"aadClientSecret,omitempty" yaml:"aadClientSecret,omitempty" datapolicy:"token"` | ||||
| 	// The path of a client certificate for an AAD application with RBAC access to talk to Azure RM APIs | ||||
| 	AADClientCertPath string `json:"aadClientCertPath,omitempty" yaml:"aadClientCertPath,omitempty"` | ||||
| 	// The password of the client certificate for an AAD application with RBAC access to talk to Azure RM APIs | ||||
| 	AADClientCertPassword string `json:"aadClientCertPassword,omitempty" yaml:"aadClientCertPassword,omitempty" datapolicy:"password"` | ||||
| 	// Use managed service identity for the virtual machine to access Azure ARM APIs | ||||
| 	UseManagedIdentityExtension bool `json:"useManagedIdentityExtension,omitempty" yaml:"useManagedIdentityExtension,omitempty"` | ||||
| 	// UserAssignedIdentityID contains the Client ID of the user assigned MSI which is assigned to the underlying VMs. If empty the user assigned identity is not used. | ||||
| 	// More details of the user assigned identity can be found at: https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview | ||||
| 	// For the user assigned identity specified here to be used, the UseManagedIdentityExtension has to be set to true. | ||||
| 	UserAssignedIdentityID string `json:"userAssignedIdentityID,omitempty" yaml:"userAssignedIdentityID,omitempty"` | ||||
| 	// The ID of the Azure Subscription that the cluster is deployed in | ||||
| 	SubscriptionID string `json:"subscriptionId,omitempty" yaml:"subscriptionId,omitempty"` | ||||
| 	// IdentitySystem indicates the identity provider. Relevant only to hybrid clouds (Azure Stack). | ||||
| 	// Allowed values are 'azure_ad' (default), 'adfs'. | ||||
| 	IdentitySystem string `json:"identitySystem,omitempty" yaml:"identitySystem,omitempty"` | ||||
| 	// ResourceManagerEndpoint is the cloud's resource manager endpoint. If set, cloud provider queries this endpoint | ||||
| 	// in order to generate an autorest.Environment instance instead of using one of the pre-defined Environments. | ||||
| 	ResourceManagerEndpoint string `json:"resourceManagerEndpoint,omitempty" yaml:"resourceManagerEndpoint,omitempty"` | ||||
| 	// The AAD Tenant ID for the Subscription that the network resources are deployed in | ||||
| 	NetworkResourceTenantID string `json:"networkResourceTenantID,omitempty" yaml:"networkResourceTenantID,omitempty"` | ||||
| 	// The ID of the Azure Subscription that the network resources are deployed in | ||||
| 	NetworkResourceSubscriptionID string `json:"networkResourceSubscriptionID,omitempty" yaml:"networkResourceSubscriptionID,omitempty"` | ||||
| } | ||||
|  | ||||
| // GetServicePrincipalToken creates a new service principal token based on the configuration. | ||||
| // | ||||
| // By default, the cluster and its network resources are deployed in the same AAD Tenant and Subscription, | ||||
| // and all azure clients use this method to fetch Service Principal Token. | ||||
| // | ||||
| // If NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID, network resources are deployed in different AAD Tenant and Subscription than those for the cluster, | ||||
| // than only azure clients except VM/VMSS and network resource ones use this method to fetch Token. | ||||
| // For tokens for VM/VMSS and network resource ones, please check GetMultiTenantServicePrincipalToken and GetNetworkResourceServicePrincipalToken. | ||||
| func GetServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.ServicePrincipalToken, error) { | ||||
| 	var tenantID string | ||||
| 	if strings.EqualFold(config.IdentitySystem, ADFSIdentitySystem) { | ||||
| 		tenantID = ADFSIdentitySystem | ||||
| 	} else { | ||||
| 		tenantID = config.TenantID | ||||
| 	} | ||||
|  | ||||
| 	if config.UseManagedIdentityExtension { | ||||
| 		klog.V(2).Infoln("azure: using managed identity extension to retrieve access token") | ||||
| 		msiEndpoint, err := adal.GetMSIVMEndpoint() | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("Getting the managed service identity endpoint: %v", err) | ||||
| 		} | ||||
| 		if len(config.UserAssignedIdentityID) > 0 { | ||||
| 			klog.V(4).Info("azure: using User Assigned MSI ID to retrieve access token") | ||||
| 			return adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, | ||||
| 				env.ServiceManagementEndpoint, | ||||
| 				config.UserAssignedIdentityID) | ||||
| 		} | ||||
| 		klog.V(4).Info("azure: using System Assigned MSI to retrieve access token") | ||||
| 		return adal.NewServicePrincipalTokenFromMSI( | ||||
| 			msiEndpoint, | ||||
| 			env.ServiceManagementEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, tenantID, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("creating the OAuth config: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientSecret) > 0 { | ||||
| 		klog.V(2).Infoln("azure: using client_id+client_secret to retrieve access token") | ||||
| 		return adal.NewServicePrincipalToken( | ||||
| 			*oauthConfig, | ||||
| 			config.AADClientID, | ||||
| 			config.AADClientSecret, | ||||
| 			env.ServiceManagementEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 { | ||||
| 		klog.V(2).Infoln("azure: using jwt client_assertion (client_cert+client_private_key) to retrieve access token") | ||||
| 		certData, err := ioutil.ReadFile(config.AADClientCertPath) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("reading the client certificate from file %s: %v", config.AADClientCertPath, err) | ||||
| 		} | ||||
| 		certificate, privateKey, err := decodePkcs12(certData, config.AADClientCertPassword) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("decoding the client certificate: %v", err) | ||||
| 		} | ||||
| 		return adal.NewServicePrincipalTokenFromCertificate( | ||||
| 			*oauthConfig, | ||||
| 			config.AADClientID, | ||||
| 			certificate, | ||||
| 			privateKey, | ||||
| 			env.ServiceManagementEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	return nil, ErrorNoAuth | ||||
| } | ||||
|  | ||||
| // GetMultiTenantServicePrincipalToken is used when (and only when) NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID. | ||||
| // | ||||
| // In that scenario, network resources are deployed in different AAD Tenant and Subscription than those for the cluster, | ||||
| // and this method creates a new multi-tenant service principal token based on the configuration. | ||||
| // | ||||
| // PrimaryToken of the returned multi-tenant token is for the AAD Tenant specified by TenantID, and AuxiliaryToken of the returned multi-tenant token is for the AAD Tenant specified by NetworkResourceTenantID. | ||||
| // | ||||
| // Azure VM/VMSS clients use this multi-tenant token, in order to operate those VM/VMSS in AAD Tenant specified by TenantID, and meanwhile in their payload they are referencing network resources (e.g. Load Balancer, Network Security Group, etc.) in AAD Tenant specified by NetworkResourceTenantID. | ||||
| func GetMultiTenantServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.MultiTenantServicePrincipalToken, error) { | ||||
| 	err := config.checkConfigWhenNetworkResourceInDifferentTenant() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("got error(%v) in getting multi-tenant service principal token", err) | ||||
| 	} | ||||
|  | ||||
| 	multiTenantOAuthConfig, err := adal.NewMultiTenantOAuthConfig( | ||||
| 		env.ActiveDirectoryEndpoint, config.TenantID, []string{config.NetworkResourceTenantID}, adal.OAuthOptions{}) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("creating the multi-tenant OAuth config: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientSecret) > 0 { | ||||
| 		klog.V(2).Infoln("azure: using client_id+client_secret to retrieve multi-tenant access token") | ||||
| 		return adal.NewMultiTenantServicePrincipalToken( | ||||
| 			multiTenantOAuthConfig, | ||||
| 			config.AADClientID, | ||||
| 			config.AADClientSecret, | ||||
| 			env.ServiceManagementEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 { | ||||
| 		return nil, fmt.Errorf("AAD Application client certificate authentication is not supported in getting multi-tenant service principal token") | ||||
| 	} | ||||
|  | ||||
| 	return nil, ErrorNoAuth | ||||
| } | ||||
|  | ||||
| // GetNetworkResourceServicePrincipalToken is used when (and only when) NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID. | ||||
| // | ||||
| // In that scenario, network resources are deployed in different AAD Tenant and Subscription than those for the cluster, | ||||
| // and this method creates a new service principal token for network resources tenant based on the configuration. | ||||
| // | ||||
| // Azure network resource (Load Balancer, Public IP, Route Table, Network Security Group and their sub level resources) clients use this multi-tenant token, in order to operate resources in AAD Tenant specified by NetworkResourceTenantID. | ||||
| func GetNetworkResourceServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.ServicePrincipalToken, error) { | ||||
| 	err := config.checkConfigWhenNetworkResourceInDifferentTenant() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("got error(%v) in getting network resources service principal token", err) | ||||
| 	} | ||||
|  | ||||
| 	oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.NetworkResourceTenantID, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("creating the OAuth config for network resources tenant: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientSecret) > 0 { | ||||
| 		klog.V(2).Infoln("azure: using client_id+client_secret to retrieve access token for network resources tenant") | ||||
| 		return adal.NewServicePrincipalToken( | ||||
| 			*oauthConfig, | ||||
| 			config.AADClientID, | ||||
| 			config.AADClientSecret, | ||||
| 			env.ServiceManagementEndpoint) | ||||
| 	} | ||||
|  | ||||
| 	if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 { | ||||
| 		return nil, fmt.Errorf("AAD Application client certificate authentication is not supported in getting network resources service principal token") | ||||
| 	} | ||||
|  | ||||
| 	return nil, ErrorNoAuth | ||||
| } | ||||
|  | ||||
| // ParseAzureEnvironment returns the azure environment. | ||||
| // If 'resourceManagerEndpoint' is set, the environment is computed by querying the cloud's resource manager endpoint. | ||||
| // Otherwise, a pre-defined Environment is looked up by name. | ||||
| func ParseAzureEnvironment(cloudName, resourceManagerEndpoint, identitySystem string) (*azure.Environment, error) { | ||||
| 	var env azure.Environment | ||||
| 	var err error | ||||
| 	if resourceManagerEndpoint != "" { | ||||
| 		klog.V(4).Infof("Loading environment from resource manager endpoint: %s", resourceManagerEndpoint) | ||||
| 		nameOverride := azure.OverrideProperty{Key: azure.EnvironmentName, Value: cloudName} | ||||
| 		env, err = azure.EnvironmentFromURL(resourceManagerEndpoint, nameOverride) | ||||
| 		if err == nil { | ||||
| 			azureStackOverrides(&env, resourceManagerEndpoint, identitySystem) | ||||
| 		} | ||||
| 	} else if cloudName == "" { | ||||
| 		klog.V(4).Info("Using public cloud environment") | ||||
| 		env = azure.PublicCloud | ||||
| 	} else { | ||||
| 		klog.V(4).Infof("Using %s environment", cloudName) | ||||
| 		env, err = azure.EnvironmentFromName(cloudName) | ||||
| 	} | ||||
| 	return &env, err | ||||
| } | ||||
|  | ||||
| // UsesNetworkResourceInDifferentTenant determines whether the AzureAuthConfig indicates to use network resources in different AAD Tenant and Subscription than those for the cluster | ||||
| // Return true only when both NetworkResourceTenantID and NetworkResourceSubscriptionID are specified | ||||
| // and they are not equals to TenantID and SubscriptionID | ||||
| func (config *AzureAuthConfig) UsesNetworkResourceInDifferentTenant() bool { | ||||
| 	return len(config.NetworkResourceTenantID) > 0 && | ||||
| 		len(config.NetworkResourceSubscriptionID) > 0 && | ||||
| 		!strings.EqualFold(config.NetworkResourceTenantID, config.TenantID) && | ||||
| 		!strings.EqualFold(config.NetworkResourceSubscriptionID, config.SubscriptionID) | ||||
| } | ||||
|  | ||||
| // decodePkcs12 decodes a PKCS#12 client certificate by extracting the public certificate and | ||||
| // the private RSA key | ||||
| func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) { | ||||
| 	privateKey, certificate, err := pkcs12.Decode(pkcs, password) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, fmt.Errorf("decoding the PKCS#12 client certificate: %v", err) | ||||
| 	} | ||||
| 	rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey) | ||||
| 	if !isRsaKey { | ||||
| 		return nil, nil, fmt.Errorf("PKCS#12 certificate must contain a RSA private key") | ||||
| 	} | ||||
|  | ||||
| 	return certificate, rsaPrivateKey, nil | ||||
| } | ||||
|  | ||||
| // azureStackOverrides ensures that the Environment matches what AKSe currently generates for Azure Stack | ||||
| func azureStackOverrides(env *azure.Environment, resourceManagerEndpoint, identitySystem string) { | ||||
| 	env.ManagementPortalURL = strings.Replace(resourceManagerEndpoint, "https://management.", "https://portal.", -1) | ||||
| 	env.ServiceManagementEndpoint = env.TokenAudience | ||||
| 	env.ResourceManagerVMDNSSuffix = strings.Replace(resourceManagerEndpoint, "https://management.", "cloudapp.", -1) | ||||
| 	env.ResourceManagerVMDNSSuffix = strings.TrimSuffix(env.ResourceManagerVMDNSSuffix, "/") | ||||
| 	if strings.EqualFold(identitySystem, ADFSIdentitySystem) { | ||||
| 		env.ActiveDirectoryEndpoint = strings.TrimSuffix(env.ActiveDirectoryEndpoint, "/") | ||||
| 		env.ActiveDirectoryEndpoint = strings.TrimSuffix(env.ActiveDirectoryEndpoint, "adfs") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // checkConfigWhenNetworkResourceInDifferentTenant checks configuration for the scenario of using network resource in different tenant | ||||
| func (config *AzureAuthConfig) checkConfigWhenNetworkResourceInDifferentTenant() error { | ||||
| 	if !config.UsesNetworkResourceInDifferentTenant() { | ||||
| 		return fmt.Errorf("NetworkResourceTenantID and NetworkResourceSubscriptionID must be configured") | ||||
| 	} | ||||
|  | ||||
| 	if strings.EqualFold(config.IdentitySystem, ADFSIdentitySystem) { | ||||
| 		return fmt.Errorf("ADFS identity system is not supported") | ||||
| 	} | ||||
|  | ||||
| 	if config.UseManagedIdentityExtension { | ||||
| 		return fmt.Errorf("managed identity is not supported") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,176 +0,0 @@ | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package auth | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest/adal" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	CrossTenantNetworkResourceNegativeConfig = []*AzureAuthConfig{ | ||||
| 		{ | ||||
| 			TenantID:        "TenantID", | ||||
| 			AADClientID:     "AADClientID", | ||||
| 			AADClientSecret: "AADClientSecret", | ||||
| 		}, | ||||
| 		{ | ||||
| 			TenantID:                      "TenantID", | ||||
| 			AADClientID:                   "AADClientID", | ||||
| 			AADClientSecret:               "AADClientSecret", | ||||
| 			NetworkResourceTenantID:       "NetworkResourceTenantID", | ||||
| 			NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID", | ||||
| 			IdentitySystem:                ADFSIdentitySystem, | ||||
| 		}, | ||||
| 		{ | ||||
| 			TenantID:                      "TenantID", | ||||
| 			AADClientID:                   "AADClientID", | ||||
| 			AADClientSecret:               "AADClientSecret", | ||||
| 			NetworkResourceTenantID:       "NetworkResourceTenantID", | ||||
| 			NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID", | ||||
| 			UseManagedIdentityExtension:   true, | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func TestGetServicePrincipalToken(t *testing.T) { | ||||
| 	config := &AzureAuthConfig{ | ||||
| 		TenantID:        "TenantID", | ||||
| 		AADClientID:     "AADClientID", | ||||
| 		AADClientSecret: "AADClientSecret", | ||||
| 	} | ||||
| 	env := &azure.PublicCloud | ||||
|  | ||||
| 	token, err := GetServicePrincipalToken(config, env) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.TenantID, nil) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	assert.Equal(t, token, spt) | ||||
| } | ||||
|  | ||||
| func TestGetMultiTenantServicePrincipalToken(t *testing.T) { | ||||
| 	config := &AzureAuthConfig{ | ||||
| 		TenantID:                      "TenantID", | ||||
| 		AADClientID:                   "AADClientID", | ||||
| 		AADClientSecret:               "AADClientSecret", | ||||
| 		NetworkResourceTenantID:       "NetworkResourceTenantID", | ||||
| 		NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID", | ||||
| 	} | ||||
| 	env := &azure.PublicCloud | ||||
|  | ||||
| 	multiTenantToken, err := GetMultiTenantServicePrincipalToken(config, env) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	multiTenantOAuthConfig, err := adal.NewMultiTenantOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID, []string{config.NetworkResourceTenantID}, adal.OAuthOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	spt, err := adal.NewMultiTenantServicePrincipalToken(multiTenantOAuthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	assert.Equal(t, multiTenantToken, spt) | ||||
| } | ||||
|  | ||||
| func TestGetMultiTenantServicePrincipalTokenNegative(t *testing.T) { | ||||
| 	env := &azure.PublicCloud | ||||
| 	for _, config := range CrossTenantNetworkResourceNegativeConfig { | ||||
| 		_, err := GetMultiTenantServicePrincipalToken(config, env) | ||||
| 		assert.Error(t, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetNetworkResourceServicePrincipalToken(t *testing.T) { | ||||
| 	config := &AzureAuthConfig{ | ||||
| 		TenantID:                      "TenantID", | ||||
| 		AADClientID:                   "AADClientID", | ||||
| 		AADClientSecret:               "AADClientSecret", | ||||
| 		NetworkResourceTenantID:       "NetworkResourceTenantID", | ||||
| 		NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID", | ||||
| 	} | ||||
| 	env := &azure.PublicCloud | ||||
|  | ||||
| 	token, err := GetNetworkResourceServicePrincipalToken(config, env) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.NetworkResourceTenantID, nil) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	assert.Equal(t, token, spt) | ||||
| } | ||||
|  | ||||
| func TestGetNetworkResourceServicePrincipalTokenNegative(t *testing.T) { | ||||
| 	env := &azure.PublicCloud | ||||
| 	for _, config := range CrossTenantNetworkResourceNegativeConfig { | ||||
| 		_, err := GetNetworkResourceServicePrincipalToken(config, env) | ||||
| 		assert.Error(t, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestParseAzureEnvironment(t *testing.T) { | ||||
| 	cases := []struct { | ||||
| 		cloudName               string | ||||
| 		resourceManagerEndpoint string | ||||
| 		identitySystem          string | ||||
| 		expected                *azure.Environment | ||||
| 	}{ | ||||
| 		{ | ||||
| 			cloudName:               "", | ||||
| 			resourceManagerEndpoint: "", | ||||
| 			identitySystem:          "", | ||||
| 			expected:                &azure.PublicCloud, | ||||
| 		}, | ||||
| 		{ | ||||
| 			cloudName:               "AZURECHINACLOUD", | ||||
| 			resourceManagerEndpoint: "", | ||||
| 			identitySystem:          "", | ||||
| 			expected:                &azure.ChinaCloud, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, c := range cases { | ||||
| 		env, err := ParseAzureEnvironment(c.cloudName, c.resourceManagerEndpoint, c.identitySystem) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, c.expected, env) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAzureStackOverrides(t *testing.T) { | ||||
| 	env := &azure.PublicCloud | ||||
| 	resourceManagerEndpoint := "https://management.test.com/" | ||||
|  | ||||
| 	azureStackOverrides(env, resourceManagerEndpoint, "") | ||||
| 	assert.Equal(t, env.ManagementPortalURL, "https://portal.test.com/") | ||||
| 	assert.Equal(t, env.ServiceManagementEndpoint, env.TokenAudience) | ||||
| 	assert.Equal(t, env.ResourceManagerVMDNSSuffix, "cloudapp.test.com") | ||||
| 	assert.Equal(t, env.ActiveDirectoryEndpoint, "https://login.microsoftonline.com/") | ||||
|  | ||||
| 	azureStackOverrides(env, resourceManagerEndpoint, "adfs") | ||||
| 	assert.Equal(t, env.ManagementPortalURL, "https://portal.test.com/") | ||||
| 	assert.Equal(t, env.ServiceManagementEndpoint, env.TokenAudience) | ||||
| 	assert.Equal(t, env.ResourceManagerVMDNSSuffix, "cloudapp.test.com") | ||||
| 	assert.Equal(t, env.ActiveDirectoryEndpoint, "https://login.microsoftonline.com") | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package auth provides a general libraty to authorize Azure ARM clients. | ||||
| package auth // import "k8s.io/legacy-cloud-providers/azure/auth" | ||||
| @@ -1,988 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/adal" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"k8s.io/client-go/informers" | ||||
| 	clientset "k8s.io/client-go/kubernetes" | ||||
| 	"k8s.io/client-go/kubernetes/scheme" | ||||
| 	v1core "k8s.io/client-go/kubernetes/typed/core/v1" | ||||
| 	"k8s.io/client-go/tools/cache" | ||||
| 	"k8s.io/client-go/tools/record" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/auth" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/diskclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/fileclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/publicipclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routeclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routetableclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/snapshotclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/subnetclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmsizeclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
|  | ||||
| 	// ensure the newly added package from azure-sdk-for-go is in vendor/ | ||||
| 	_ "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient" | ||||
| 	// ensure the newly added package from azure-sdk-for-go is in vendor/ | ||||
| 	_ "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient" | ||||
|  | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// CloudProviderName is the value used for the --cloud-provider flag | ||||
| 	CloudProviderName = "azure" | ||||
| 	// AzureStackCloudName is the cloud name of Azure Stack | ||||
| 	AzureStackCloudName    = "AZURESTACKCLOUD" | ||||
| 	rateLimitQPSDefault    = 1.0 | ||||
| 	rateLimitBucketDefault = 5 | ||||
| 	backoffRetriesDefault  = 6 | ||||
| 	backoffExponentDefault = 1.5 | ||||
| 	backoffDurationDefault = 5 // in seconds | ||||
| 	backoffJitterDefault   = 1.0 | ||||
| 	// According to https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#load-balancer. | ||||
| 	maximumLoadBalancerRuleCount = 250 | ||||
|  | ||||
| 	vmTypeVMSS     = "vmss" | ||||
| 	vmTypeStandard = "standard" | ||||
|  | ||||
| 	loadBalancerSkuBasic    = "basic" | ||||
| 	loadBalancerSkuStandard = "standard" | ||||
|  | ||||
| 	externalResourceGroupLabel = "kubernetes.azure.com/resource-group" | ||||
| 	managedByAzureLabel        = "kubernetes.azure.com/managed" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// PreConfiguredBackendPoolLoadBalancerTypesNone means that the load balancers are not pre-configured | ||||
| 	PreConfiguredBackendPoolLoadBalancerTypesNone = "" | ||||
| 	// PreConfiguredBackendPoolLoadBalancerTypesInternal means that the `internal` load balancers are pre-configured | ||||
| 	PreConfiguredBackendPoolLoadBalancerTypesInternal = "internal" | ||||
| 	// PreConfiguredBackendPoolLoadBalancerTypesExternal means that the `external` load balancers are pre-configured | ||||
| 	PreConfiguredBackendPoolLoadBalancerTypesExternal = "external" | ||||
| 	// PreConfiguredBackendPoolLoadBalancerTypesAll means that all load balancers are pre-configured | ||||
| 	PreConfiguredBackendPoolLoadBalancerTypesAll = "all" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// Master nodes are not added to standard load balancer by default. | ||||
| 	defaultExcludeMasterFromStandardLB = true | ||||
| 	// Outbound SNAT is enabled by default. | ||||
| 	defaultDisableOutboundSNAT = false | ||||
| ) | ||||
|  | ||||
| // Config holds the configuration parsed from the --cloud-config flag | ||||
| // All fields are required unless otherwise specified | ||||
| // NOTE: Cloud config files should follow the same Kubernetes deprecation policy as | ||||
| // flags or CLIs. Config fields should not change behavior in incompatible ways and | ||||
| // should be deprecated for at least 2 release prior to removing. | ||||
| // See https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-a-flag-or-cli | ||||
| // for more details. | ||||
| type Config struct { | ||||
| 	auth.AzureAuthConfig | ||||
| 	CloudProviderRateLimitConfig | ||||
|  | ||||
| 	// The name of the resource group that the cluster is deployed in | ||||
| 	ResourceGroup string `json:"resourceGroup,omitempty" yaml:"resourceGroup,omitempty"` | ||||
| 	// The location of the resource group that the cluster is deployed in | ||||
| 	Location string `json:"location,omitempty" yaml:"location,omitempty"` | ||||
| 	// The name of the VNet that the cluster is deployed in | ||||
| 	VnetName string `json:"vnetName,omitempty" yaml:"vnetName,omitempty"` | ||||
| 	// The name of the resource group that the Vnet is deployed in | ||||
| 	VnetResourceGroup string `json:"vnetResourceGroup,omitempty" yaml:"vnetResourceGroup,omitempty"` | ||||
| 	// The name of the subnet that the cluster is deployed in | ||||
| 	SubnetName string `json:"subnetName,omitempty" yaml:"subnetName,omitempty"` | ||||
| 	// The name of the security group attached to the cluster's subnet | ||||
| 	SecurityGroupName string `json:"securityGroupName,omitempty" yaml:"securityGroupName,omitempty"` | ||||
| 	// The name of the resource group that the security group is deployed in | ||||
| 	SecurityGroupResourceGroup string `json:"securityGroupResourceGroup,omitempty" yaml:"securityGroupResourceGroup,omitempty"` | ||||
| 	// (Optional in 1.6) The name of the route table attached to the subnet that the cluster is deployed in | ||||
| 	RouteTableName string `json:"routeTableName,omitempty" yaml:"routeTableName,omitempty"` | ||||
| 	// The name of the resource group that the RouteTable is deployed in | ||||
| 	RouteTableResourceGroup string `json:"routeTableResourceGroup,omitempty" yaml:"routeTableResourceGroup,omitempty"` | ||||
| 	// (Optional) The name of the availability set that should be used as the load balancer backend | ||||
| 	// If this is set, the Azure cloudprovider will only add nodes from that availability set to the load | ||||
| 	// balancer backend pool. If this is not set, and multiple agent pools (availability sets) are used, then | ||||
| 	// the cloudprovider will try to add all nodes to a single backend pool which is forbidden. | ||||
| 	// In other words, if you use multiple agent pools (availability sets), you MUST set this field. | ||||
| 	PrimaryAvailabilitySetName string `json:"primaryAvailabilitySetName,omitempty" yaml:"primaryAvailabilitySetName,omitempty"` | ||||
| 	// The type of azure nodes. Candidate values are: vmss and standard. | ||||
| 	// If not set, it will be default to standard. | ||||
| 	VMType string `json:"vmType,omitempty" yaml:"vmType,omitempty"` | ||||
| 	// The name of the scale set that should be used as the load balancer backend. | ||||
| 	// If this is set, the Azure cloudprovider will only add nodes from that scale set to the load | ||||
| 	// balancer backend pool. If this is not set, and multiple agent pools (scale sets) are used, then | ||||
| 	// the cloudprovider will try to add all nodes to a single backend pool which is forbidden. | ||||
| 	// In other words, if you use multiple agent pools (scale sets), you MUST set this field. | ||||
| 	PrimaryScaleSetName string `json:"primaryScaleSetName,omitempty" yaml:"primaryScaleSetName,omitempty"` | ||||
| 	// Enable exponential backoff to manage resource request retries | ||||
| 	CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"` | ||||
| 	// Backoff retry limit | ||||
| 	CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries,omitempty" yaml:"cloudProviderBackoffRetries,omitempty"` | ||||
| 	// Backoff exponent | ||||
| 	CloudProviderBackoffExponent float64 `json:"cloudProviderBackoffExponent,omitempty" yaml:"cloudProviderBackoffExponent,omitempty"` | ||||
| 	// Backoff duration | ||||
| 	CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration,omitempty" yaml:"cloudProviderBackoffDuration,omitempty"` | ||||
| 	// Backoff jitter | ||||
| 	CloudProviderBackoffJitter float64 `json:"cloudProviderBackoffJitter,omitempty" yaml:"cloudProviderBackoffJitter,omitempty"` | ||||
| 	// Use instance metadata service where possible | ||||
| 	UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty" yaml:"useInstanceMetadata,omitempty"` | ||||
|  | ||||
| 	// Sku of Load Balancer and Public IP. Candidate values are: basic and standard. | ||||
| 	// If not set, it will be default to basic. | ||||
| 	LoadBalancerSku string `json:"loadBalancerSku,omitempty" yaml:"loadBalancerSku,omitempty"` | ||||
| 	// ExcludeMasterFromStandardLB excludes master nodes from standard load balancer. | ||||
| 	// If not set, it will be default to true. | ||||
| 	ExcludeMasterFromStandardLB *bool `json:"excludeMasterFromStandardLB,omitempty" yaml:"excludeMasterFromStandardLB,omitempty"` | ||||
| 	// DisableOutboundSNAT disables the outbound SNAT for public load balancer rules. | ||||
| 	// It should only be set when loadBalancerSku is standard. If not set, it will be default to false. | ||||
| 	DisableOutboundSNAT *bool `json:"disableOutboundSNAT,omitempty" yaml:"disableOutboundSNAT,omitempty"` | ||||
|  | ||||
| 	// Maximum allowed LoadBalancer Rule Count is the limit enforced by Azure Load balancer | ||||
| 	MaximumLoadBalancerRuleCount int `json:"maximumLoadBalancerRuleCount,omitempty" yaml:"maximumLoadBalancerRuleCount,omitempty"` | ||||
|  | ||||
| 	// The cloud configure type for Azure cloud provider. Supported values are file, secret and merge. | ||||
| 	CloudConfigType cloudConfigType `json:"cloudConfigType,omitempty" yaml:"cloudConfigType,omitempty"` | ||||
|  | ||||
| 	// LoadBalancerName determines the specific name of the load balancer user want to use, working with | ||||
| 	// LoadBalancerResourceGroup | ||||
| 	LoadBalancerName string `json:"loadBalancerName,omitempty" yaml:"loadBalancerName,omitempty"` | ||||
| 	// LoadBalancerResourceGroup determines the specific resource group of the load balancer user want to use, working | ||||
| 	// with LoadBalancerName | ||||
| 	LoadBalancerResourceGroup string `json:"loadBalancerResourceGroup,omitempty" yaml:"loadBalancerResourceGroup,omitempty"` | ||||
| 	// PreConfiguredBackendPoolLoadBalancerTypes determines whether the LoadBalancer BackendPool has been preconfigured. | ||||
| 	// Candidate values are: | ||||
| 	//   "": exactly with today (not pre-configured for any LBs) | ||||
| 	//   "internal": for internal LoadBalancer | ||||
| 	//   "external": for external LoadBalancer | ||||
| 	//   "all": for both internal and external LoadBalancer | ||||
| 	PreConfiguredBackendPoolLoadBalancerTypes string `json:"preConfiguredBackendPoolLoadBalancerTypes,omitempty" yaml:"preConfiguredBackendPoolLoadBalancerTypes,omitempty"` | ||||
| 	// EnableMultipleStandardLoadBalancers determines the behavior of the standard load balancer. If set to true | ||||
| 	// there would be one standard load balancer per VMAS or VMSS, which is similar with the behavior of the basic | ||||
| 	// load balancer. Users could select the specific standard load balancer for their service by the service | ||||
| 	// annotation `service.beta.kubernetes.io/azure-load-balancer-mode`, If set to false, the same standard load balancer | ||||
| 	// would be shared by all services in the cluster. In this case, the mode selection annotation would be ignored. | ||||
| 	EnableMultipleStandardLoadBalancers bool `json:"enableMultipleStandardLoadBalancers,omitempty" yaml:"enableMultipleStandardLoadBalancers,omitempty"` | ||||
|  | ||||
| 	// AvailabilitySetNodesCacheTTLInSeconds sets the Cache TTL for availabilitySetNodesCache | ||||
| 	// if not set, will use default value | ||||
| 	AvailabilitySetNodesCacheTTLInSeconds int `json:"availabilitySetNodesCacheTTLInSeconds,omitempty" yaml:"availabilitySetNodesCacheTTLInSeconds,omitempty"` | ||||
| 	// VmssCacheTTLInSeconds sets the cache TTL for VMSS | ||||
| 	VmssCacheTTLInSeconds int `json:"vmssCacheTTLInSeconds,omitempty" yaml:"vmssCacheTTLInSeconds,omitempty"` | ||||
| 	// VmssVirtualMachinesCacheTTLInSeconds sets the cache TTL for vmssVirtualMachines | ||||
| 	VmssVirtualMachinesCacheTTLInSeconds int `json:"vmssVirtualMachinesCacheTTLInSeconds,omitempty" yaml:"vmssVirtualMachinesCacheTTLInSeconds,omitempty"` | ||||
| 	// VmCacheTTLInSeconds sets the cache TTL for vm | ||||
| 	VMCacheTTLInSeconds int `json:"vmCacheTTLInSeconds,omitempty" yaml:"vmCacheTTLInSeconds,omitempty"` | ||||
| 	// LoadBalancerCacheTTLInSeconds sets the cache TTL for load balancer | ||||
| 	LoadBalancerCacheTTLInSeconds int `json:"loadBalancerCacheTTLInSeconds,omitempty" yaml:"loadBalancerCacheTTLInSeconds,omitempty"` | ||||
| 	// NsgCacheTTLInSeconds sets the cache TTL for network security group | ||||
| 	NsgCacheTTLInSeconds int `json:"nsgCacheTTLInSeconds,omitempty" yaml:"nsgCacheTTLInSeconds,omitempty"` | ||||
| 	// RouteTableCacheTTLInSeconds sets the cache TTL for route table | ||||
| 	RouteTableCacheTTLInSeconds int `json:"routeTableCacheTTLInSeconds,omitempty" yaml:"routeTableCacheTTLInSeconds,omitempty"` | ||||
|  | ||||
| 	// DisableAvailabilitySetNodes disables VMAS nodes support when "VMType" is set to "vmss". | ||||
| 	DisableAvailabilitySetNodes bool `json:"disableAvailabilitySetNodes,omitempty" yaml:"disableAvailabilitySetNodes,omitempty"` | ||||
|  | ||||
| 	// Tags determines what tags shall be applied to the shared resources managed by controller manager, which | ||||
| 	// includes load balancer, security group and route table. The supported format is `a=b,c=d,...`. After updated | ||||
| 	// this config, the old tags would be replaced by the new ones. | ||||
| 	Tags string `json:"tags,omitempty" yaml:"tags,omitempty"` | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	_ cloudprovider.Interface    = (*Cloud)(nil) | ||||
| 	_ cloudprovider.Instances    = (*Cloud)(nil) | ||||
| 	_ cloudprovider.LoadBalancer = (*Cloud)(nil) | ||||
| 	_ cloudprovider.Routes       = (*Cloud)(nil) | ||||
| 	_ cloudprovider.Zones        = (*Cloud)(nil) | ||||
| 	_ cloudprovider.PVLabeler    = (*Cloud)(nil) | ||||
| ) | ||||
|  | ||||
| // Cloud holds the config and clients | ||||
| type Cloud struct { | ||||
| 	Config | ||||
| 	Environment azure.Environment | ||||
|  | ||||
| 	RoutesClient                    routeclient.Interface | ||||
| 	SubnetsClient                   subnetclient.Interface | ||||
| 	InterfacesClient                interfaceclient.Interface | ||||
| 	RouteTablesClient               routetableclient.Interface | ||||
| 	LoadBalancerClient              loadbalancerclient.Interface | ||||
| 	PublicIPAddressesClient         publicipclient.Interface | ||||
| 	SecurityGroupsClient            securitygroupclient.Interface | ||||
| 	VirtualMachinesClient           vmclient.Interface | ||||
| 	StorageAccountClient            storageaccountclient.Interface | ||||
| 	DisksClient                     diskclient.Interface | ||||
| 	SnapshotsClient                 snapshotclient.Interface | ||||
| 	FileClient                      fileclient.Interface | ||||
| 	VirtualMachineScaleSetsClient   vmssclient.Interface | ||||
| 	VirtualMachineScaleSetVMsClient vmssvmclient.Interface | ||||
| 	VirtualMachineSizesClient       vmsizeclient.Interface | ||||
|  | ||||
| 	ResourceRequestBackoff wait.Backoff | ||||
| 	metadata               *InstanceMetadataService | ||||
| 	VMSet                  VMSet | ||||
|  | ||||
| 	// ipv6DualStack allows overriding for unit testing.  It's normally initialized from featuregates | ||||
| 	ipv6DualStackEnabled bool | ||||
| 	// Lock for access to node caches, includes nodeZones, nodeResourceGroups, and unmanagedNodes. | ||||
| 	nodeCachesLock sync.RWMutex | ||||
| 	// nodeNames holds current nodes for tracking added nodes in VM caches. | ||||
| 	nodeNames sets.String | ||||
| 	// nodeZones is a mapping from Zone to a sets.String of Node's names in the Zone | ||||
| 	// it is updated by the nodeInformer | ||||
| 	nodeZones map[string]sets.String | ||||
| 	// nodeResourceGroups holds nodes external resource groups | ||||
| 	nodeResourceGroups map[string]string | ||||
| 	// unmanagedNodes holds a list of nodes not managed by Azure cloud provider. | ||||
| 	unmanagedNodes sets.String | ||||
| 	// excludeLoadBalancerNodes holds a list of nodes that should be excluded from LoadBalancer. | ||||
| 	excludeLoadBalancerNodes sets.String | ||||
| 	// nodeInformerSynced is for determining if the informer has synced. | ||||
| 	nodeInformerSynced cache.InformerSynced | ||||
|  | ||||
| 	// routeCIDRsLock holds lock for routeCIDRs cache. | ||||
| 	routeCIDRsLock sync.Mutex | ||||
| 	// routeCIDRs holds cache for route CIDRs. | ||||
| 	routeCIDRs map[string]string | ||||
|  | ||||
| 	KubeClient       clientset.Interface | ||||
| 	eventBroadcaster record.EventBroadcaster | ||||
| 	eventRecorder    record.EventRecorder | ||||
| 	routeUpdater     *delayedRouteUpdater | ||||
|  | ||||
| 	vmCache  *azcache.TimedCache | ||||
| 	lbCache  *azcache.TimedCache | ||||
| 	nsgCache *azcache.TimedCache | ||||
| 	rtCache  *azcache.TimedCache | ||||
|  | ||||
| 	*BlobDiskController | ||||
| 	*ManagedDiskController | ||||
| 	*controllerCommon | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	// In go-autorest SDK https://github.com/Azure/go-autorest/blob/master/autorest/sender.go#L258-L287, | ||||
| 	// if ARM returns http.StatusTooManyRequests, the sender doesn't increase the retry attempt count, | ||||
| 	// hence the Azure clients will keep retrying forever until it get a status code other than 429. | ||||
| 	// So we explicitly removes http.StatusTooManyRequests from autorest.StatusCodesForRetry. | ||||
| 	// Refer https://github.com/Azure/go-autorest/issues/398. | ||||
| 	// TODO(feiskyer): Use autorest.SendDecorator to customize the retry policy when new Azure SDK is available. | ||||
| 	statusCodesForRetry := make([]int, 0) | ||||
| 	for _, code := range autorest.StatusCodesForRetry { | ||||
| 		if code != http.StatusTooManyRequests { | ||||
| 			statusCodesForRetry = append(statusCodesForRetry, code) | ||||
| 		} | ||||
| 	} | ||||
| 	autorest.StatusCodesForRetry = statusCodesForRetry | ||||
|  | ||||
| 	cloudprovider.RegisterCloudProvider(CloudProviderName, NewCloud) | ||||
| } | ||||
|  | ||||
| // NewCloud returns a Cloud with initialized clients | ||||
| func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { | ||||
| 	az, err := NewCloudWithoutFeatureGates(configReader) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	az.ipv6DualStackEnabled = true | ||||
|  | ||||
| 	return az, nil | ||||
| } | ||||
|  | ||||
| // NewCloudWithoutFeatureGates returns a Cloud without trying to wire the feature gates.  This is used by the unit tests | ||||
| // that don't load the actual features being used in the cluster. | ||||
| func NewCloudWithoutFeatureGates(configReader io.Reader) (*Cloud, error) { | ||||
| 	config, err := parseConfig(configReader) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	az := &Cloud{ | ||||
| 		nodeNames:                sets.NewString(), | ||||
| 		nodeZones:                map[string]sets.String{}, | ||||
| 		nodeResourceGroups:       map[string]string{}, | ||||
| 		unmanagedNodes:           sets.NewString(), | ||||
| 		excludeLoadBalancerNodes: sets.NewString(), | ||||
| 		routeCIDRs:               map[string]string{}, | ||||
| 	} | ||||
|  | ||||
| 	err = az.InitializeCloudFromConfig(config, false) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return az, nil | ||||
| } | ||||
|  | ||||
| // InitializeCloudFromConfig initializes the Cloud from config. | ||||
| func (az *Cloud) InitializeCloudFromConfig(config *Config, fromSecret bool) error { | ||||
| 	// cloud-config not set, return nil so that it would be initialized from secret. | ||||
| 	if config == nil { | ||||
| 		klog.Warning("cloud-config is not provided, Azure cloud provider would be initialized from secret") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if config.RouteTableResourceGroup == "" { | ||||
| 		config.RouteTableResourceGroup = config.ResourceGroup | ||||
| 	} | ||||
|  | ||||
| 	if config.SecurityGroupResourceGroup == "" { | ||||
| 		config.SecurityGroupResourceGroup = config.ResourceGroup | ||||
| 	} | ||||
|  | ||||
| 	if config.VMType == "" { | ||||
| 		// default to standard vmType if not set. | ||||
| 		config.VMType = vmTypeStandard | ||||
| 	} | ||||
|  | ||||
| 	if config.DisableAvailabilitySetNodes && config.VMType != vmTypeVMSS { | ||||
| 		return fmt.Errorf("disableAvailabilitySetNodes %v is only supported when vmType is 'vmss'", config.DisableAvailabilitySetNodes) | ||||
| 	} | ||||
|  | ||||
| 	if config.CloudConfigType == "" { | ||||
| 		// The default cloud config type is cloudConfigTypeMerge. | ||||
| 		config.CloudConfigType = cloudConfigTypeMerge | ||||
| 	} else { | ||||
| 		supportedCloudConfigTypes := sets.NewString( | ||||
| 			string(cloudConfigTypeMerge), | ||||
| 			string(cloudConfigTypeFile), | ||||
| 			string(cloudConfigTypeSecret)) | ||||
| 		if !supportedCloudConfigTypes.Has(string(config.CloudConfigType)) { | ||||
| 			return fmt.Errorf("cloudConfigType %v is not supported, supported values are %v", config.CloudConfigType, supportedCloudConfigTypes.List()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	env, err := auth.ParseAzureEnvironment(config.Cloud, config.ResourceManagerEndpoint, config.IdentitySystem) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	servicePrincipalToken, err := auth.GetServicePrincipalToken(&config.AzureAuthConfig, env) | ||||
| 	if err == auth.ErrorNoAuth { | ||||
| 		// Only controller-manager would lazy-initialize from secret, and credentials are required for such case. | ||||
| 		if fromSecret { | ||||
| 			err := fmt.Errorf("no credentials provided for Azure cloud provider") | ||||
| 			klog.Fatalf("%v", err) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// No credentials provided, useInstanceMetadata should be enabled for Kubelet. | ||||
| 		// TODO(feiskyer): print different error message for Kubelet and controller-manager, as they're | ||||
| 		// requiring different credential settings. | ||||
| 		if !config.UseInstanceMetadata && config.CloudConfigType == cloudConfigTypeFile { | ||||
| 			return fmt.Errorf("useInstanceMetadata must be enabled without Azure credentials") | ||||
| 		} | ||||
|  | ||||
| 		klog.V(2).Infof("Azure cloud provider is starting without credentials") | ||||
| 	} else if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Initialize rate limiting config options. | ||||
| 	InitializeCloudProviderRateLimitConfig(&config.CloudProviderRateLimitConfig) | ||||
|  | ||||
| 	// Conditionally configure resource request backoff | ||||
| 	resourceRequestBackoff := wait.Backoff{ | ||||
| 		Steps: 1, | ||||
| 	} | ||||
| 	if config.CloudProviderBackoff { | ||||
| 		// Assign backoff defaults if no configuration was passed in | ||||
| 		if config.CloudProviderBackoffRetries == 0 { | ||||
| 			config.CloudProviderBackoffRetries = backoffRetriesDefault | ||||
| 		} | ||||
| 		if config.CloudProviderBackoffDuration == 0 { | ||||
| 			config.CloudProviderBackoffDuration = backoffDurationDefault | ||||
| 		} | ||||
| 		if config.CloudProviderBackoffExponent == 0 { | ||||
| 			config.CloudProviderBackoffExponent = backoffExponentDefault | ||||
| 		} | ||||
|  | ||||
| 		if config.CloudProviderBackoffJitter == 0 { | ||||
| 			config.CloudProviderBackoffJitter = backoffJitterDefault | ||||
| 		} | ||||
|  | ||||
| 		resourceRequestBackoff = wait.Backoff{ | ||||
| 			Steps:    config.CloudProviderBackoffRetries, | ||||
| 			Factor:   config.CloudProviderBackoffExponent, | ||||
| 			Duration: time.Duration(config.CloudProviderBackoffDuration) * time.Second, | ||||
| 			Jitter:   config.CloudProviderBackoffJitter, | ||||
| 		} | ||||
| 		klog.V(2).Infof("Azure cloudprovider using try backoff: retries=%d, exponent=%f, duration=%d, jitter=%f", | ||||
| 			config.CloudProviderBackoffRetries, | ||||
| 			config.CloudProviderBackoffExponent, | ||||
| 			config.CloudProviderBackoffDuration, | ||||
| 			config.CloudProviderBackoffJitter) | ||||
| 	} else { | ||||
| 		// CloudProviderBackoffRetries will be set to 1 by default as the requirements of Azure SDK. | ||||
| 		config.CloudProviderBackoffRetries = 1 | ||||
| 		config.CloudProviderBackoffDuration = backoffDurationDefault | ||||
| 	} | ||||
|  | ||||
| 	if strings.EqualFold(config.LoadBalancerSku, loadBalancerSkuStandard) { | ||||
| 		// Do not add master nodes to standard LB by default. | ||||
| 		if config.ExcludeMasterFromStandardLB == nil { | ||||
| 			config.ExcludeMasterFromStandardLB = &defaultExcludeMasterFromStandardLB | ||||
| 		} | ||||
|  | ||||
| 		// Enable outbound SNAT by default. | ||||
| 		if config.DisableOutboundSNAT == nil { | ||||
| 			config.DisableOutboundSNAT = &defaultDisableOutboundSNAT | ||||
| 		} | ||||
| 	} else { | ||||
| 		if config.DisableOutboundSNAT != nil && *config.DisableOutboundSNAT { | ||||
| 			return fmt.Errorf("disableOutboundSNAT should only set when loadBalancerSku is standard") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	az.Config = *config | ||||
| 	az.Environment = *env | ||||
| 	az.ResourceRequestBackoff = resourceRequestBackoff | ||||
| 	az.metadata, err = NewInstanceMetadataService(imdsServer) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// No credentials provided, InstanceMetadataService would be used for getting Azure resources. | ||||
| 	// Note that this only applies to Kubelet, controller-manager should configure credentials for managing Azure resources. | ||||
| 	if servicePrincipalToken == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// If uses network resources in different AAD Tenant, then prepare corresponding Service Principal Token for VM/VMSS client and network resources client | ||||
| 	var multiTenantServicePrincipalToken *adal.MultiTenantServicePrincipalToken | ||||
| 	var networkResourceServicePrincipalToken *adal.ServicePrincipalToken | ||||
| 	if az.Config.UsesNetworkResourceInDifferentTenant() { | ||||
| 		multiTenantServicePrincipalToken, err = auth.GetMultiTenantServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		networkResourceServicePrincipalToken, err = auth.GetNetworkResourceServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	az.configAzureClients(servicePrincipalToken, multiTenantServicePrincipalToken, networkResourceServicePrincipalToken) | ||||
|  | ||||
| 	if az.MaximumLoadBalancerRuleCount == 0 { | ||||
| 		az.MaximumLoadBalancerRuleCount = maximumLoadBalancerRuleCount | ||||
| 	} | ||||
|  | ||||
| 	if strings.EqualFold(vmTypeVMSS, az.Config.VMType) { | ||||
| 		az.VMSet, err = newScaleSet(az) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
| 		az.VMSet = newAvailabilitySet(az) | ||||
| 	} | ||||
|  | ||||
| 	az.vmCache, err = az.newVMCache() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	az.lbCache, err = az.newLBCache() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	az.nsgCache, err = az.newNSGCache() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	az.rtCache, err = az.newRouteTableCache() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := initDiskControllers(az); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// start delayed route updater. | ||||
| 	az.routeUpdater = newDelayedRouteUpdater(az, routeUpdateInterval) | ||||
| 	go az.routeUpdater.run() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) configAzureClients( | ||||
| 	servicePrincipalToken *adal.ServicePrincipalToken, | ||||
| 	multiTenantServicePrincipalToken *adal.MultiTenantServicePrincipalToken, | ||||
| 	networkResourceServicePrincipalToken *adal.ServicePrincipalToken) { | ||||
| 	azClientConfig := az.getAzureClientConfig(servicePrincipalToken) | ||||
|  | ||||
| 	// Prepare AzureClientConfig for all azure clients | ||||
| 	interfaceClientConfig := azClientConfig.WithRateLimiter(az.Config.InterfaceRateLimit) | ||||
| 	vmSizeClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineSizeRateLimit) | ||||
| 	snapshotClientConfig := azClientConfig.WithRateLimiter(az.Config.SnapshotRateLimit) | ||||
| 	storageAccountClientConfig := azClientConfig.WithRateLimiter(az.Config.StorageAccountRateLimit) | ||||
| 	diskClientConfig := azClientConfig.WithRateLimiter(az.Config.DiskRateLimit) | ||||
| 	vmClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineRateLimit) | ||||
| 	vmssClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineScaleSetRateLimit) | ||||
| 	// Error "not an active Virtual Machine Scale Set VM" is not retriable for VMSS VM. | ||||
| 	// But http.StatusNotFound is retriable because of ARM replication latency. | ||||
| 	vmssVMClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineScaleSetRateLimit) | ||||
| 	vmssVMClientConfig.Backoff = vmssVMClientConfig.Backoff.WithNonRetriableErrors([]string{vmssVMNotActiveErrorMessage}).WithRetriableHTTPStatusCodes([]int{http.StatusNotFound}) | ||||
| 	routeClientConfig := azClientConfig.WithRateLimiter(az.Config.RouteRateLimit) | ||||
| 	subnetClientConfig := azClientConfig.WithRateLimiter(az.Config.SubnetsRateLimit) | ||||
| 	routeTableClientConfig := azClientConfig.WithRateLimiter(az.Config.RouteTableRateLimit) | ||||
| 	loadBalancerClientConfig := azClientConfig.WithRateLimiter(az.Config.LoadBalancerRateLimit) | ||||
| 	securityGroupClientConfig := azClientConfig.WithRateLimiter(az.Config.SecurityGroupRateLimit) | ||||
| 	publicIPClientConfig := azClientConfig.WithRateLimiter(az.Config.PublicIPAddressRateLimit) | ||||
| 	// TODO(ZeroMagic): add azurefileRateLimit | ||||
| 	fileClientConfig := azClientConfig.WithRateLimiter(nil) | ||||
|  | ||||
| 	// If uses network resources in different AAD Tenant, update Authorizer for VM/VMSS client config | ||||
| 	if multiTenantServicePrincipalToken != nil { | ||||
| 		multiTenantServicePrincipalTokenAuthorizer := autorest.NewMultiTenantServicePrincipalTokenAuthorizer(multiTenantServicePrincipalToken) | ||||
| 		vmClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer | ||||
| 		vmssClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer | ||||
| 		vmssVMClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer | ||||
| 	} | ||||
|  | ||||
| 	// If uses network resources in different AAD Tenant, update SubscriptionID and Authorizer for network resources client config | ||||
| 	if networkResourceServicePrincipalToken != nil { | ||||
| 		networkResourceServicePrincipalTokenAuthorizer := autorest.NewBearerAuthorizer(networkResourceServicePrincipalToken) | ||||
| 		routeClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
| 		subnetClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
| 		routeTableClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
| 		loadBalancerClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
| 		securityGroupClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
| 		publicIPClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer | ||||
|  | ||||
| 		routeClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 		subnetClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 		routeTableClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 		loadBalancerClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 		securityGroupClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 		publicIPClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID | ||||
| 	} | ||||
|  | ||||
| 	// Initialize all azure clients based on client config | ||||
| 	az.InterfacesClient = interfaceclient.New(interfaceClientConfig) | ||||
| 	az.VirtualMachineSizesClient = vmsizeclient.New(vmSizeClientConfig) | ||||
| 	az.SnapshotsClient = snapshotclient.New(snapshotClientConfig) | ||||
| 	az.StorageAccountClient = storageaccountclient.New(storageAccountClientConfig) | ||||
| 	az.DisksClient = diskclient.New(diskClientConfig) | ||||
| 	az.VirtualMachinesClient = vmclient.New(vmClientConfig) | ||||
| 	az.VirtualMachineScaleSetsClient = vmssclient.New(vmssClientConfig) | ||||
| 	az.VirtualMachineScaleSetVMsClient = vmssvmclient.New(vmssVMClientConfig) | ||||
| 	az.RoutesClient = routeclient.New(routeClientConfig) | ||||
| 	az.SubnetsClient = subnetclient.New(subnetClientConfig) | ||||
| 	az.RouteTablesClient = routetableclient.New(routeTableClientConfig) | ||||
| 	az.LoadBalancerClient = loadbalancerclient.New(loadBalancerClientConfig) | ||||
| 	az.SecurityGroupsClient = securitygroupclient.New(securityGroupClientConfig) | ||||
| 	az.PublicIPAddressesClient = publicipclient.New(publicIPClientConfig) | ||||
| 	az.FileClient = fileclient.New(fileClientConfig) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getAzureClientConfig(servicePrincipalToken *adal.ServicePrincipalToken) *azclients.ClientConfig { | ||||
| 	azClientConfig := &azclients.ClientConfig{ | ||||
| 		CloudName:               az.Config.Cloud, | ||||
| 		Location:                az.Config.Location, | ||||
| 		SubscriptionID:          az.Config.SubscriptionID, | ||||
| 		ResourceManagerEndpoint: az.Environment.ResourceManagerEndpoint, | ||||
| 		Authorizer:              autorest.NewBearerAuthorizer(servicePrincipalToken), | ||||
| 		Backoff:                 &retry.Backoff{Steps: 1}, | ||||
| 	} | ||||
|  | ||||
| 	if az.Config.CloudProviderBackoff { | ||||
| 		azClientConfig.Backoff = &retry.Backoff{ | ||||
| 			Steps:    az.Config.CloudProviderBackoffRetries, | ||||
| 			Factor:   az.Config.CloudProviderBackoffExponent, | ||||
| 			Duration: time.Duration(az.Config.CloudProviderBackoffDuration) * time.Second, | ||||
| 			Jitter:   az.Config.CloudProviderBackoffJitter, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return azClientConfig | ||||
| } | ||||
|  | ||||
| // parseConfig returns a parsed configuration for an Azure cloudprovider config file | ||||
| func parseConfig(configReader io.Reader) (*Config, error) { | ||||
| 	var config Config | ||||
| 	if configReader == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	configContents, err := ioutil.ReadAll(configReader) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	err = yaml.Unmarshal(configContents, &config) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// The resource group name may be in different cases from different Azure APIs, hence it is converted to lower here. | ||||
| 	// See more context at https://github.com/kubernetes/kubernetes/issues/71994. | ||||
| 	config.ResourceGroup = strings.ToLower(config.ResourceGroup) | ||||
| 	return &config, nil | ||||
| } | ||||
|  | ||||
| // Initialize passes a Kubernetes clientBuilder interface to the cloud provider | ||||
| func (az *Cloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) { | ||||
| 	az.KubeClient = clientBuilder.ClientOrDie("azure-cloud-provider") | ||||
| 	az.eventBroadcaster = record.NewBroadcaster() | ||||
| 	az.eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: az.KubeClient.CoreV1().Events("")}) | ||||
| 	az.eventRecorder = az.eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "azure-cloud-provider"}) | ||||
| 	az.InitializeCloudFromSecret() | ||||
| } | ||||
|  | ||||
| // LoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise. | ||||
| func (az *Cloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) { | ||||
| 	return az, true | ||||
| } | ||||
|  | ||||
| // Instances returns an instances interface. Also returns true if the interface is supported, false otherwise. | ||||
| func (az *Cloud) Instances() (cloudprovider.Instances, bool) { | ||||
| 	return az, true | ||||
| } | ||||
|  | ||||
| // InstancesV2 returns an instancesV2 interface. Also returns true if the interface is supported, false otherwise. | ||||
| // TODO: implement ONLY for external cloud provider | ||||
| func (az *Cloud) InstancesV2() (cloudprovider.InstancesV2, bool) { | ||||
| 	return nil, false | ||||
| } | ||||
|  | ||||
| // Zones returns a zones interface. Also returns true if the interface is supported, false otherwise. | ||||
| func (az *Cloud) Zones() (cloudprovider.Zones, bool) { | ||||
| 	return az, true | ||||
| } | ||||
|  | ||||
| // Clusters returns a clusters interface.  Also returns true if the interface is supported, false otherwise. | ||||
| func (az *Cloud) Clusters() (cloudprovider.Clusters, bool) { | ||||
| 	return nil, false | ||||
| } | ||||
|  | ||||
| // Routes returns a routes interface along with whether the interface is supported. | ||||
| func (az *Cloud) Routes() (cloudprovider.Routes, bool) { | ||||
| 	return az, true | ||||
| } | ||||
|  | ||||
| // HasClusterID returns true if the cluster has a clusterID | ||||
| func (az *Cloud) HasClusterID() bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // ProviderName returns the cloud provider ID. | ||||
| func (az *Cloud) ProviderName() string { | ||||
| 	return CloudProviderName | ||||
| } | ||||
|  | ||||
| func initDiskControllers(az *Cloud) error { | ||||
| 	// Common controller contains the function | ||||
| 	// needed by both blob disk and managed disk controllers | ||||
|  | ||||
| 	common := &controllerCommon{ | ||||
| 		location:              az.Location, | ||||
| 		storageEndpointSuffix: az.Environment.StorageEndpointSuffix, | ||||
| 		resourceGroup:         az.ResourceGroup, | ||||
| 		subscriptionID:        az.SubscriptionID, | ||||
| 		cloud:                 az, | ||||
| 		vmLockMap:             newLockMap(), | ||||
| 	} | ||||
|  | ||||
| 	az.BlobDiskController = &BlobDiskController{common: common} | ||||
| 	az.ManagedDiskController = &ManagedDiskController{common: common} | ||||
| 	az.controllerCommon = common | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SetInformers sets informers for Azure cloud provider. | ||||
| func (az *Cloud) SetInformers(informerFactory informers.SharedInformerFactory) { | ||||
| 	klog.Infof("Setting up informers for Azure cloud provider") | ||||
| 	nodeInformer := informerFactory.Core().V1().Nodes().Informer() | ||||
| 	nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ | ||||
| 		AddFunc: func(obj interface{}) { | ||||
| 			node := obj.(*v1.Node) | ||||
| 			az.updateNodeCaches(nil, node) | ||||
| 		}, | ||||
| 		UpdateFunc: func(prev, obj interface{}) { | ||||
| 			prevNode := prev.(*v1.Node) | ||||
| 			newNode := obj.(*v1.Node) | ||||
| 			az.updateNodeCaches(prevNode, newNode) | ||||
| 		}, | ||||
| 		DeleteFunc: func(obj interface{}) { | ||||
| 			node, isNode := obj.(*v1.Node) | ||||
| 			// We can get DeletedFinalStateUnknown instead of *v1.Node here | ||||
| 			// and we need to handle that correctly. | ||||
| 			if !isNode { | ||||
| 				deletedState, ok := obj.(cache.DeletedFinalStateUnknown) | ||||
| 				if !ok { | ||||
| 					klog.Errorf("Received unexpected object: %v", obj) | ||||
| 					return | ||||
| 				} | ||||
| 				node, ok = deletedState.Obj.(*v1.Node) | ||||
| 				if !ok { | ||||
| 					klog.Errorf("DeletedFinalStateUnknown contained non-Node object: %v", deletedState.Obj) | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 			az.updateNodeCaches(node, nil) | ||||
| 		}, | ||||
| 	}) | ||||
| 	az.nodeInformerSynced = nodeInformer.HasSynced | ||||
| } | ||||
|  | ||||
| // updateNodeCaches updates local cache for node's zones and external resource groups. | ||||
| func (az *Cloud) updateNodeCaches(prevNode, newNode *v1.Node) { | ||||
| 	az.nodeCachesLock.Lock() | ||||
| 	defer az.nodeCachesLock.Unlock() | ||||
|  | ||||
| 	if prevNode != nil { | ||||
|  | ||||
| 		// Remove from nodeNames cache. | ||||
| 		az.nodeNames.Delete(prevNode.ObjectMeta.Name) | ||||
|  | ||||
| 		// Remove from nodeZones cache | ||||
| 		prevZone, ok := prevNode.ObjectMeta.Labels[v1.LabelTopologyZone] | ||||
|  | ||||
| 		if ok && az.isAvailabilityZone(prevZone) { | ||||
| 			az.nodeZones[prevZone].Delete(prevNode.ObjectMeta.Name) | ||||
| 			if az.nodeZones[prevZone].Len() == 0 { | ||||
| 				az.nodeZones[prevZone] = nil | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Remove from nodeZones cache if using deprecated LabelFailureDomainBetaZone | ||||
| 		prevZoneFailureDomain, ok := prevNode.ObjectMeta.Labels[v1.LabelFailureDomainBetaZone] | ||||
| 		if ok && az.isAvailabilityZone(prevZoneFailureDomain) { | ||||
| 			az.nodeZones[prevZone].Delete(prevNode.ObjectMeta.Name) | ||||
| 			if az.nodeZones[prevZone].Len() == 0 { | ||||
| 				az.nodeZones[prevZone] = nil | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Remove from nodeResourceGroups cache. | ||||
| 		_, ok = prevNode.ObjectMeta.Labels[externalResourceGroupLabel] | ||||
| 		if ok { | ||||
| 			delete(az.nodeResourceGroups, prevNode.ObjectMeta.Name) | ||||
| 		} | ||||
|  | ||||
| 		managed, ok := prevNode.ObjectMeta.Labels[managedByAzureLabel] | ||||
| 		isNodeManagedByCloudProvider := !ok || managed != "false" | ||||
|  | ||||
| 		// Remove from unmanagedNodes cache | ||||
| 		if !isNodeManagedByCloudProvider { | ||||
| 			az.unmanagedNodes.Delete(prevNode.ObjectMeta.Name) | ||||
| 		} | ||||
|  | ||||
| 		// if the node is being deleted from the cluster, exclude it from load balancers | ||||
| 		if newNode == nil { | ||||
| 			az.excludeLoadBalancerNodes.Insert(prevNode.ObjectMeta.Name) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if newNode != nil { | ||||
| 		// Add to nodeNames cache. | ||||
| 		az.nodeNames.Insert(newNode.ObjectMeta.Name) | ||||
|  | ||||
| 		// Add to nodeZones cache. | ||||
| 		newZone, ok := newNode.ObjectMeta.Labels[v1.LabelTopologyZone] | ||||
| 		if ok && az.isAvailabilityZone(newZone) { | ||||
| 			if az.nodeZones[newZone] == nil { | ||||
| 				az.nodeZones[newZone] = sets.NewString() | ||||
| 			} | ||||
| 			az.nodeZones[newZone].Insert(newNode.ObjectMeta.Name) | ||||
| 		} | ||||
|  | ||||
| 		// Add to nodeResourceGroups cache. | ||||
| 		newRG, ok := newNode.ObjectMeta.Labels[externalResourceGroupLabel] | ||||
| 		if ok && len(newRG) > 0 { | ||||
| 			az.nodeResourceGroups[newNode.ObjectMeta.Name] = strings.ToLower(newRG) | ||||
| 		} | ||||
|  | ||||
| 		_, hasExcludeBalancerLabel := newNode.ObjectMeta.Labels[v1.LabelNodeExcludeBalancers] | ||||
| 		managed, ok := newNode.ObjectMeta.Labels[managedByAzureLabel] | ||||
| 		isNodeManagedByCloudProvider := !ok || managed != "false" | ||||
|  | ||||
| 		// Update unmanagedNodes cache | ||||
| 		if !isNodeManagedByCloudProvider { | ||||
| 			az.unmanagedNodes.Insert(newNode.ObjectMeta.Name) | ||||
| 		} | ||||
|  | ||||
| 		// Update excludeLoadBalancerNodes cache | ||||
| 		switch { | ||||
| 		case !isNodeManagedByCloudProvider: | ||||
| 			az.excludeLoadBalancerNodes.Insert(newNode.ObjectMeta.Name) | ||||
|  | ||||
| 		case hasExcludeBalancerLabel: | ||||
| 			az.excludeLoadBalancerNodes.Insert(newNode.ObjectMeta.Name) | ||||
|  | ||||
| 		default: | ||||
| 			// Nodes not falling into the three cases above are valid backends and | ||||
| 			// should not appear in excludeLoadBalancerNodes cache. | ||||
| 			az.excludeLoadBalancerNodes.Delete(newNode.ObjectMeta.Name) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetActiveZones returns all the zones in which k8s nodes are currently running. | ||||
| func (az *Cloud) GetActiveZones() (sets.String, error) { | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return nil, fmt.Errorf("azure cloud provider doesn't have informers set") | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return nil, fmt.Errorf("node informer is not synced when trying to GetActiveZones") | ||||
| 	} | ||||
|  | ||||
| 	zones := sets.NewString() | ||||
| 	for zone, nodes := range az.nodeZones { | ||||
| 		if len(nodes) > 0 { | ||||
| 			zones.Insert(zone) | ||||
| 		} | ||||
| 	} | ||||
| 	return zones, nil | ||||
| } | ||||
|  | ||||
| // GetLocation returns the location in which k8s cluster is currently running. | ||||
| func (az *Cloud) GetLocation() string { | ||||
| 	return az.Location | ||||
| } | ||||
|  | ||||
| // GetNodeResourceGroup gets resource group for given node. | ||||
| func (az *Cloud) GetNodeResourceGroup(nodeName string) (string, error) { | ||||
| 	// Kubelet won't set az.nodeInformerSynced, always return configured resourceGroup. | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return az.ResourceGroup, nil | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return "", fmt.Errorf("node informer is not synced when trying to GetNodeResourceGroup") | ||||
| 	} | ||||
|  | ||||
| 	// Return external resource group if it has been cached. | ||||
| 	if cachedRG, ok := az.nodeResourceGroups[nodeName]; ok { | ||||
| 		return cachedRG, nil | ||||
| 	} | ||||
|  | ||||
| 	// Return resource group from cloud provider options. | ||||
| 	return az.ResourceGroup, nil | ||||
| } | ||||
|  | ||||
| // GetNodeNames returns a set of all node names in the k8s cluster. | ||||
| func (az *Cloud) GetNodeNames() (sets.String, error) { | ||||
| 	// Kubelet won't set az.nodeInformerSynced, return nil. | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return nil, fmt.Errorf("node informer is not synced when trying to GetNodeNames") | ||||
| 	} | ||||
|  | ||||
| 	return sets.NewString(az.nodeNames.List()...), nil | ||||
| } | ||||
|  | ||||
| // GetResourceGroups returns a set of resource groups that all nodes are running on. | ||||
| func (az *Cloud) GetResourceGroups() (sets.String, error) { | ||||
| 	// Kubelet won't set az.nodeInformerSynced, always return configured resourceGroup. | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return sets.NewString(az.ResourceGroup), nil | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return nil, fmt.Errorf("node informer is not synced when trying to GetResourceGroups") | ||||
| 	} | ||||
|  | ||||
| 	resourceGroups := sets.NewString(az.ResourceGroup) | ||||
| 	for _, rg := range az.nodeResourceGroups { | ||||
| 		resourceGroups.Insert(rg) | ||||
| 	} | ||||
|  | ||||
| 	return resourceGroups, nil | ||||
| } | ||||
|  | ||||
| // GetUnmanagedNodes returns a list of nodes not managed by Azure cloud provider (e.g. on-prem nodes). | ||||
| func (az *Cloud) GetUnmanagedNodes() (sets.String, error) { | ||||
| 	// Kubelet won't set az.nodeInformerSynced, always return nil. | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return nil, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes") | ||||
| 	} | ||||
|  | ||||
| 	return sets.NewString(az.unmanagedNodes.List()...), nil | ||||
| } | ||||
|  | ||||
| // ShouldNodeExcludedFromLoadBalancer returns true if node is unmanaged, in external resource group or labeled with "node.kubernetes.io/exclude-from-external-load-balancers". | ||||
| func (az *Cloud) ShouldNodeExcludedFromLoadBalancer(nodeName string) (bool, error) { | ||||
| 	// Kubelet won't set az.nodeInformerSynced, always return nil. | ||||
| 	if az.nodeInformerSynced == nil { | ||||
| 		return false, nil | ||||
| 	} | ||||
|  | ||||
| 	az.nodeCachesLock.RLock() | ||||
| 	defer az.nodeCachesLock.RUnlock() | ||||
| 	if !az.nodeInformerSynced() { | ||||
| 		return false, fmt.Errorf("node informer is not synced when trying to fetch node caches") | ||||
| 	} | ||||
|  | ||||
| 	// Return true if the node is in external resource group. | ||||
| 	if cachedRG, ok := az.nodeResourceGroups[nodeName]; ok && !strings.EqualFold(cachedRG, az.ResourceGroup) { | ||||
| 		return true, nil | ||||
| 	} | ||||
|  | ||||
| 	return az.excludeLoadBalancerNodes.Has(nodeName), nil | ||||
| } | ||||
| @@ -1,470 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// not active means the instance is under deleting from Azure VMSS. | ||||
| 	vmssVMNotActiveErrorMessage = "not an active Virtual Machine Scale Set VM instanceId" | ||||
|  | ||||
| 	// operationCanceledErrorMessage means the operation is canceled by another new operation. | ||||
| 	operationCanceledErrorMessage = "canceledandsupersededduetoanotheroperation" | ||||
|  | ||||
| 	cannotDeletePublicIPErrorMessageCode = "PublicIPAddressCannotBeDeleted" | ||||
|  | ||||
| 	referencedResourceNotProvisionedMessageCode = "ReferencedResourceNotProvisioned" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	pipErrorMessageRE = regexp.MustCompile(`(?:.*)/subscriptions/(?:.*)/resourceGroups/(.*)/providers/Microsoft.Network/publicIPAddresses/([^\s]+)(?:.*)`) | ||||
| ) | ||||
|  | ||||
| // RequestBackoff if backoff is disabled in cloud provider it | ||||
| // returns a new Backoff object steps = 1 | ||||
| // This is to make sure that the requested command executes | ||||
| // at least once | ||||
| func (az *Cloud) RequestBackoff() (resourceRequestBackoff wait.Backoff) { | ||||
| 	if az.CloudProviderBackoff { | ||||
| 		return az.ResourceRequestBackoff | ||||
| 	} | ||||
| 	resourceRequestBackoff = wait.Backoff{ | ||||
| 		Steps: 1, | ||||
| 	} | ||||
| 	return resourceRequestBackoff | ||||
| } | ||||
|  | ||||
| // Event creates a event for the specified object. | ||||
| func (az *Cloud) Event(obj runtime.Object, eventType, reason, message string) { | ||||
| 	if obj != nil && reason != "" { | ||||
| 		az.eventRecorder.Event(obj, eventType, reason, message) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetVirtualMachineWithRetry invokes az.getVirtualMachine with exponential backoff retry | ||||
| func (az *Cloud) GetVirtualMachineWithRetry(name types.NodeName, crt azcache.AzureCacheReadType) (compute.VirtualMachine, error) { | ||||
| 	var machine compute.VirtualMachine | ||||
| 	var retryErr error | ||||
| 	err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) { | ||||
| 		machine, retryErr = az.getVirtualMachine(name, crt) | ||||
| 		if retryErr == cloudprovider.InstanceNotFound { | ||||
| 			return true, cloudprovider.InstanceNotFound | ||||
| 		} | ||||
| 		if retryErr != nil { | ||||
| 			klog.Errorf("GetVirtualMachineWithRetry(%s): backoff failure, will retry, err=%v", name, retryErr) | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		klog.V(2).Infof("GetVirtualMachineWithRetry(%s): backoff success", name) | ||||
| 		return true, nil | ||||
| 	}) | ||||
| 	if err == wait.ErrWaitTimeout { | ||||
| 		err = retryErr | ||||
| 	} | ||||
| 	return machine, err | ||||
| } | ||||
|  | ||||
| // ListVirtualMachines invokes az.VirtualMachinesClient.List with exponential backoff retry | ||||
| func (az *Cloud) ListVirtualMachines(resourceGroup string) ([]compute.VirtualMachine, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	allNodes, rerr := az.VirtualMachinesClient.List(ctx, resourceGroup) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("VirtualMachinesClient.List(%v) failure with err=%v", resourceGroup, rerr) | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
| 	klog.V(2).Infof("VirtualMachinesClient.List(%v) success", resourceGroup) | ||||
| 	return allNodes, nil | ||||
| } | ||||
|  | ||||
| // getPrivateIPsForMachine is wrapper for optional backoff getting private ips | ||||
| // list of a node by name | ||||
| func (az *Cloud) getPrivateIPsForMachine(nodeName types.NodeName) ([]string, error) { | ||||
| 	return az.getPrivateIPsForMachineWithRetry(nodeName) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getPrivateIPsForMachineWithRetry(nodeName types.NodeName) ([]string, error) { | ||||
| 	var privateIPs []string | ||||
| 	err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) { | ||||
| 		var retryErr error | ||||
| 		privateIPs, retryErr = az.VMSet.GetPrivateIPsByNodeName(string(nodeName)) | ||||
| 		if retryErr != nil { | ||||
| 			// won't retry since the instance doesn't exist on Azure. | ||||
| 			if retryErr == cloudprovider.InstanceNotFound { | ||||
| 				return true, retryErr | ||||
| 			} | ||||
| 			klog.Errorf("GetPrivateIPsByNodeName(%s): backoff failure, will retry,err=%v", nodeName, retryErr) | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		klog.V(3).Infof("GetPrivateIPsByNodeName(%s): backoff success", nodeName) | ||||
| 		return true, nil | ||||
| 	}) | ||||
| 	return privateIPs, err | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getIPForMachine(nodeName types.NodeName) (string, string, error) { | ||||
| 	return az.GetIPForMachineWithRetry(nodeName) | ||||
| } | ||||
|  | ||||
| // GetIPForMachineWithRetry invokes az.getIPForMachine with exponential backoff retry | ||||
| func (az *Cloud) GetIPForMachineWithRetry(name types.NodeName) (string, string, error) { | ||||
| 	var ip, publicIP string | ||||
| 	err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) { | ||||
| 		var retryErr error | ||||
| 		ip, publicIP, retryErr = az.VMSet.GetIPByNodeName(string(name)) | ||||
| 		if retryErr != nil { | ||||
| 			klog.Errorf("GetIPForMachineWithRetry(%s): backoff failure, will retry,err=%v", name, retryErr) | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		klog.V(3).Infof("GetIPForMachineWithRetry(%s): backoff success", name) | ||||
| 		return true, nil | ||||
| 	}) | ||||
| 	return ip, publicIP, err | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateSecurityGroup invokes az.SecurityGroupsClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdateSecurityGroup(sg network.SecurityGroup) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.SecurityGroupsClient.CreateOrUpdate(ctx, az.SecurityGroupResourceGroup, *sg.Name, sg, pointer.StringDeref(sg.Etag, "")) | ||||
| 	klog.V(10).Infof("SecurityGroupsClient.CreateOrUpdate(%s): end", *sg.Name) | ||||
| 	if rerr == nil { | ||||
| 		// Invalidate the cache right after updating | ||||
| 		az.nsgCache.Delete(*sg.Name) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Invalidate the cache because ETAG precondition mismatch. | ||||
| 	if rerr.HTTPStatusCode == http.StatusPreconditionFailed { | ||||
| 		klog.V(3).Infof("SecurityGroup cache for %s is cleanup because of http.StatusPreconditionFailed", *sg.Name) | ||||
| 		az.nsgCache.Delete(*sg.Name) | ||||
| 	} | ||||
|  | ||||
| 	// Invalidate the cache because another new operation has canceled the current request. | ||||
| 	if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) { | ||||
| 		klog.V(3).Infof("SecurityGroup cache for %s is cleanup because CreateOrUpdateSecurityGroup is canceled by another operation", *sg.Name) | ||||
| 		az.nsgCache.Delete(*sg.Name) | ||||
| 	} | ||||
|  | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| func cleanupSubnetInFrontendIPConfigurations(lb *network.LoadBalancer) network.LoadBalancer { | ||||
| 	if lb.LoadBalancerPropertiesFormat == nil || lb.FrontendIPConfigurations == nil { | ||||
| 		return *lb | ||||
| 	} | ||||
|  | ||||
| 	frontendIPConfigurations := *lb.FrontendIPConfigurations | ||||
| 	for i := range frontendIPConfigurations { | ||||
| 		config := frontendIPConfigurations[i] | ||||
| 		if config.FrontendIPConfigurationPropertiesFormat != nil && | ||||
| 			config.Subnet != nil && | ||||
| 			config.Subnet.ID != nil { | ||||
| 			subnet := network.Subnet{ | ||||
| 				ID: config.Subnet.ID, | ||||
| 			} | ||||
| 			if config.Subnet.Name != nil { | ||||
| 				subnet.Name = config.FrontendIPConfigurationPropertiesFormat.Subnet.Name | ||||
| 			} | ||||
| 			config.FrontendIPConfigurationPropertiesFormat.Subnet = &subnet | ||||
| 			frontendIPConfigurations[i] = config | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	lb.FrontendIPConfigurations = &frontendIPConfigurations | ||||
| 	return *lb | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateLB invokes az.LoadBalancerClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdateLB(service *v1.Service, lb network.LoadBalancer) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	lb = cleanupSubnetInFrontendIPConfigurations(&lb) | ||||
|  | ||||
| 	rgName := az.getLoadBalancerResourceGroup() | ||||
| 	rerr := az.LoadBalancerClient.CreateOrUpdate(ctx, rgName, pointer.StringDeref(lb.Name, ""), lb, pointer.StringDeref(lb.Etag, "")) | ||||
| 	klog.V(10).Infof("LoadBalancerClient.CreateOrUpdate(%s): end", *lb.Name) | ||||
| 	if rerr == nil { | ||||
| 		// Invalidate the cache right after updating | ||||
| 		az.lbCache.Delete(*lb.Name) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Invalidate the cache because ETAG precondition mismatch. | ||||
| 	if rerr.HTTPStatusCode == http.StatusPreconditionFailed { | ||||
| 		klog.V(3).Infof("LoadBalancer cache for %s is cleanup because of http.StatusPreconditionFailed", pointer.StringDeref(lb.Name, "")) | ||||
| 		az.lbCache.Delete(*lb.Name) | ||||
| 	} | ||||
|  | ||||
| 	retryErrorMessage := rerr.Error().Error() | ||||
| 	// Invalidate the cache because another new operation has canceled the current request. | ||||
| 	if strings.Contains(strings.ToLower(retryErrorMessage), operationCanceledErrorMessage) { | ||||
| 		klog.V(3).Infof("LoadBalancer cache for %s is cleanup because CreateOrUpdate is canceled by another operation", pointer.StringDeref(lb.Name, "")) | ||||
| 		az.lbCache.Delete(*lb.Name) | ||||
| 	} | ||||
|  | ||||
| 	// The LB update may fail because the referenced PIP is not in the Succeeded provisioning state | ||||
| 	if strings.Contains(strings.ToLower(retryErrorMessage), strings.ToLower(referencedResourceNotProvisionedMessageCode)) { | ||||
| 		matches := pipErrorMessageRE.FindStringSubmatch(retryErrorMessage) | ||||
| 		if len(matches) != 3 { | ||||
| 			klog.Warningf("Failed to parse the retry error message %s", retryErrorMessage) | ||||
| 			return rerr.Error() | ||||
| 		} | ||||
| 		pipRG, pipName := matches[1], matches[2] | ||||
| 		klog.V(3).Infof("The public IP %s referenced by load balancer %s is not in Succeeded provisioning state, will try to update it", pipName, pointer.StringDeref(lb.Name, "")) | ||||
| 		pip, _, err := az.getPublicIPAddress(pipRG, pipName) | ||||
| 		if err != nil { | ||||
| 			klog.Warningf("Failed to get the public IP %s in resource group %s: %v", pipName, pipRG, err) | ||||
| 			return rerr.Error() | ||||
| 		} | ||||
| 		// Perform a dummy update to fix the provisioning state | ||||
| 		err = az.CreateOrUpdatePIP(service, pipRG, pip) | ||||
| 		if err != nil { | ||||
| 			klog.Warningf("Failed to update the public IP %s in resource group %s: %v", pipName, pipRG, err) | ||||
| 			return rerr.Error() | ||||
| 		} | ||||
| 		// Invalidate the LB cache, return the error, and the controller manager | ||||
| 		// would retry the LB update in the next reconcile loop | ||||
| 		az.lbCache.Delete(*lb.Name) | ||||
| 	} | ||||
|  | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| // ListLB invokes az.LoadBalancerClient.List with exponential backoff retry | ||||
| func (az *Cloud) ListLB(service *v1.Service) ([]network.LoadBalancer, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rgName := az.getLoadBalancerResourceGroup() | ||||
| 	allLBs, rerr := az.LoadBalancerClient.List(ctx, rgName) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsNotFound() { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		az.Event(service, v1.EventTypeWarning, "ListLoadBalancers", rerr.Error().Error()) | ||||
| 		klog.Errorf("LoadBalancerClient.List(%v) failure with err=%v", rgName, rerr) | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
| 	klog.V(2).Infof("LoadBalancerClient.List(%v) success", rgName) | ||||
| 	return allLBs, nil | ||||
| } | ||||
|  | ||||
| // ListPIP list the PIP resources in the given resource group | ||||
| func (az *Cloud) ListPIP(service *v1.Service, pipResourceGroup string) ([]network.PublicIPAddress, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	allPIPs, rerr := az.PublicIPAddressesClient.List(ctx, pipResourceGroup) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsNotFound() { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		az.Event(service, v1.EventTypeWarning, "ListPublicIPs", rerr.Error().Error()) | ||||
| 		klog.Errorf("PublicIPAddressesClient.List(%v) failure with err=%v", pipResourceGroup, rerr) | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("PublicIPAddressesClient.List(%v) success", pipResourceGroup) | ||||
| 	return allPIPs, nil | ||||
| } | ||||
|  | ||||
| // CreateOrUpdatePIP invokes az.PublicIPAddressesClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdatePIP(service *v1.Service, pipResourceGroup string, pip network.PublicIPAddress) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.PublicIPAddressesClient.CreateOrUpdate(ctx, pipResourceGroup, pointer.StringDeref(pip.Name, ""), pip) | ||||
| 	klog.V(10).Infof("PublicIPAddressesClient.CreateOrUpdate(%s, %s): end", pipResourceGroup, pointer.StringDeref(pip.Name, "")) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("PublicIPAddressesClient.CreateOrUpdate(%s, %s) failed: %s", pipResourceGroup, pointer.StringDeref(pip.Name, ""), rerr.Error().Error()) | ||||
| 		az.Event(service, v1.EventTypeWarning, "CreateOrUpdatePublicIPAddress", rerr.Error().Error()) | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateInterface invokes az.PublicIPAddressesClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdateInterface(service *v1.Service, nic network.Interface) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.InterfacesClient.CreateOrUpdate(ctx, az.ResourceGroup, *nic.Name, nic) | ||||
| 	klog.V(10).Infof("InterfacesClient.CreateOrUpdate(%s): end", *nic.Name) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("InterfacesClient.CreateOrUpdate(%s) failed: %s", *nic.Name, rerr.Error().Error()) | ||||
| 		az.Event(service, v1.EventTypeWarning, "CreateOrUpdateInterface", rerr.Error().Error()) | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeletePublicIP invokes az.PublicIPAddressesClient.Delete with exponential backoff retry | ||||
| func (az *Cloud) DeletePublicIP(service *v1.Service, pipResourceGroup string, pipName string) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.PublicIPAddressesClient.Delete(ctx, pipResourceGroup, pipName) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("PublicIPAddressesClient.Delete(%s) failed: %s", pipName, rerr.Error().Error()) | ||||
| 		az.Event(service, v1.EventTypeWarning, "DeletePublicIPAddress", rerr.Error().Error()) | ||||
|  | ||||
| 		if strings.Contains(rerr.Error().Error(), cannotDeletePublicIPErrorMessageCode) { | ||||
| 			klog.Warningf("DeletePublicIP for public IP %s failed with error %v, this is because other resources are referencing the public IP. The deletion of the service will continue.", pipName, rerr.Error()) | ||||
| 			return nil | ||||
| 		} | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteLB invokes az.LoadBalancerClient.Delete with exponential backoff retry | ||||
| func (az *Cloud) DeleteLB(service *v1.Service, lbName string) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rgName := az.getLoadBalancerResourceGroup() | ||||
| 	rerr := az.LoadBalancerClient.Delete(ctx, rgName, lbName) | ||||
| 	if rerr == nil { | ||||
| 		// Invalidate the cache right after updating | ||||
| 		az.lbCache.Delete(lbName) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	klog.Errorf("LoadBalancerClient.Delete(%s) failed: %s", lbName, rerr.Error().Error()) | ||||
| 	az.Event(service, v1.EventTypeWarning, "DeleteLoadBalancer", rerr.Error().Error()) | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateRouteTable invokes az.RouteTablesClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdateRouteTable(routeTable network.RouteTable) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.RouteTablesClient.CreateOrUpdate(ctx, az.RouteTableResourceGroup, az.RouteTableName, routeTable, pointer.StringDeref(routeTable.Etag, "")) | ||||
| 	if rerr == nil { | ||||
| 		// Invalidate the cache right after updating | ||||
| 		az.rtCache.Delete(*routeTable.Name) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Invalidate the cache because etag mismatch. | ||||
| 	if rerr.HTTPStatusCode == http.StatusPreconditionFailed { | ||||
| 		klog.V(3).Infof("Route table cache for %s is cleanup because of http.StatusPreconditionFailed", *routeTable.Name) | ||||
| 		az.rtCache.Delete(*routeTable.Name) | ||||
| 	} | ||||
| 	// Invalidate the cache because another new operation has canceled the current request. | ||||
| 	if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) { | ||||
| 		klog.V(3).Infof("Route table cache for %s is cleanup because CreateOrUpdateRouteTable is canceled by another operation", *routeTable.Name) | ||||
| 		az.rtCache.Delete(*routeTable.Name) | ||||
| 	} | ||||
| 	klog.Errorf("RouteTablesClient.CreateOrUpdate(%s) failed: %v", az.RouteTableName, rerr.Error()) | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateRoute invokes az.RoutesClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) CreateOrUpdateRoute(route network.Route) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.RoutesClient.CreateOrUpdate(ctx, az.RouteTableResourceGroup, az.RouteTableName, *route.Name, route, pointer.StringDeref(route.Etag, "")) | ||||
| 	klog.V(10).Infof("RoutesClient.CreateOrUpdate(%s): end", *route.Name) | ||||
| 	if rerr == nil { | ||||
| 		az.rtCache.Delete(az.RouteTableName) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if rerr.HTTPStatusCode == http.StatusPreconditionFailed { | ||||
| 		klog.V(3).Infof("Route cache for %s is cleanup because of http.StatusPreconditionFailed", *route.Name) | ||||
| 		az.rtCache.Delete(az.RouteTableName) | ||||
| 	} | ||||
| 	// Invalidate the cache because another new operation has canceled the current request. | ||||
| 	if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) { | ||||
| 		klog.V(3).Infof("Route cache for %s is cleanup because CreateOrUpdateRouteTable is canceled by another operation", *route.Name) | ||||
| 		az.rtCache.Delete(az.RouteTableName) | ||||
| 	} | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| // DeleteRouteWithName invokes az.RoutesClient.CreateOrUpdate with exponential backoff retry | ||||
| func (az *Cloud) DeleteRouteWithName(routeName string) error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rerr := az.RoutesClient.Delete(ctx, az.RouteTableResourceGroup, az.RouteTableName, routeName) | ||||
| 	klog.V(10).Infof("RoutesClient.Delete(%s,%s): end", az.RouteTableName, routeName) | ||||
| 	if rerr == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	klog.Errorf("RoutesClient.Delete(%s, %s) failed: %v", az.RouteTableName, routeName, rerr.Error()) | ||||
| 	return rerr.Error() | ||||
| } | ||||
|  | ||||
| // CreateOrUpdateVMSS invokes az.VirtualMachineScaleSetsClient.Update(). | ||||
| func (az *Cloud) CreateOrUpdateVMSS(resourceGroupName string, VMScaleSetName string, parameters compute.VirtualMachineScaleSet) *retry.Error { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// When vmss is being deleted, CreateOrUpdate API would report "the vmss is being deleted" error. | ||||
| 	// Since it is being deleted, we shouldn't send more CreateOrUpdate requests for it. | ||||
| 	klog.V(3).Infof("CreateOrUpdateVMSS: verify the status of the vmss being created or updated") | ||||
| 	vmss, rerr := az.VirtualMachineScaleSetsClient.Get(ctx, resourceGroupName, VMScaleSetName) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("CreateOrUpdateVMSS: error getting vmss(%s): %v", VMScaleSetName, rerr) | ||||
| 		return rerr | ||||
| 	} | ||||
| 	if vmss.ProvisioningState != nil && strings.EqualFold(*vmss.ProvisioningState, virtualMachineScaleSetsDeallocating) { | ||||
| 		klog.V(3).Infof("CreateOrUpdateVMSS: found vmss %s being deleted, skipping", VMScaleSetName) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	rerr = az.VirtualMachineScaleSetsClient.CreateOrUpdate(ctx, resourceGroupName, VMScaleSetName, parameters) | ||||
| 	klog.V(10).Infof("UpdateVmssVMWithRetry: VirtualMachineScaleSetsClient.CreateOrUpdate(%s): end", VMScaleSetName) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("CreateOrUpdateVMSS: error CreateOrUpdate vmss(%s): %v", VMScaleSetName, rerr) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,563 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/mockloadbalancerclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routeclient/mockrouteclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient/mocksecuritygroupclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestGetVirtualMachineWithRetry(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		vmClientErr *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			vmClientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound}, | ||||
| 			expectedErr: cloudprovider.InstanceNotFound, | ||||
| 		}, | ||||
| 		{ | ||||
| 			vmClientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(compute.VirtualMachine{}, test.vmClientErr) | ||||
|  | ||||
| 		vm, err := az.GetVirtualMachineWithRetry("vm", cache.CacheReadTypeDefault) | ||||
| 		assert.Empty(t, vm) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetPrivateIPsForMachine(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		vmClientErr        *retry.Error | ||||
| 		expectedPrivateIPs []string | ||||
| 		expectedErr        error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			expectedPrivateIPs: []string{"1.2.3.4"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			vmClientErr:        &retry.Error{HTTPStatusCode: http.StatusNotFound}, | ||||
| 			expectedErr:        cloudprovider.InstanceNotFound, | ||||
| 			expectedPrivateIPs: []string{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			vmClientErr:        &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr:        wait.ErrWaitTimeout, | ||||
| 			expectedPrivateIPs: []string{}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedVM := compute.VirtualMachine{ | ||||
| 		VirtualMachineProperties: &compute.VirtualMachineProperties{ | ||||
| 			AvailabilitySet: &compute.SubResource{ID: pointer.String("availability-set")}, | ||||
| 			NetworkProfile: &compute.NetworkProfile{ | ||||
| 				NetworkInterfaces: &[]compute.NetworkInterfaceReference{ | ||||
| 					{ | ||||
| 						NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{ | ||||
| 							Primary: pointer.Bool(true), | ||||
| 						}, | ||||
| 						ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedInterface := network.Interface{ | ||||
| 		InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ | ||||
| 			IPConfigurations: &[]network.InterfaceIPConfiguration{ | ||||
| 				{ | ||||
| 					InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ | ||||
| 						PrivateIPAddress: pointer.String("1.2.3.4"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(expectedVM, test.vmClientErr) | ||||
|  | ||||
| 		mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface) | ||||
| 		mockInterfaceClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).MaxTimes(1) | ||||
|  | ||||
| 		privateIPs, err := az.getPrivateIPsForMachine("vm") | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 		assert.Equal(t, test.expectedPrivateIPs, privateIPs) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetIPForMachineWithRetry(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr         *retry.Error | ||||
| 		expectedPrivateIP string | ||||
| 		expectedPublicIP  string | ||||
| 		expectedErr       error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			expectedPrivateIP: "1.2.3.4", | ||||
| 			expectedPublicIP:  "5.6.7.8", | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusNotFound}, | ||||
| 			expectedErr: wait.ErrWaitTimeout, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedVM := compute.VirtualMachine{ | ||||
| 		VirtualMachineProperties: &compute.VirtualMachineProperties{ | ||||
| 			AvailabilitySet: &compute.SubResource{ID: pointer.String("availability-set")}, | ||||
| 			NetworkProfile: &compute.NetworkProfile{ | ||||
| 				NetworkInterfaces: &[]compute.NetworkInterfaceReference{ | ||||
| 					{ | ||||
| 						NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{ | ||||
| 							Primary: pointer.Bool(true), | ||||
| 						}, | ||||
| 						ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedInterface := network.Interface{ | ||||
| 		InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ | ||||
| 			IPConfigurations: &[]network.InterfaceIPConfiguration{ | ||||
| 				{ | ||||
| 					InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ | ||||
| 						PrivateIPAddress: pointer.String("1.2.3.4"), | ||||
| 						PublicIPAddress: &network.PublicIPAddress{ | ||||
| 							ID: pointer.String("test/pip"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedPIP := network.PublicIPAddress{ | ||||
| 		PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ | ||||
| 			IPAddress: pointer.String("5.6.7.8"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(expectedVM, test.clientErr) | ||||
|  | ||||
| 		mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface) | ||||
| 		mockInterfaceClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).MaxTimes(1) | ||||
|  | ||||
| 		mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 		mockPIPClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(expectedPIP, nil).MaxTimes(1) | ||||
|  | ||||
| 		privateIP, publicIP, err := az.GetIPForMachineWithRetry("vm") | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 		assert.Equal(t, test.expectedPrivateIP, privateIP) | ||||
| 		assert.Equal(t, test.expectedPublicIP, publicIP) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateSecurityGroupCanceled(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	az.nsgCache.Set("sg", "test") | ||||
|  | ||||
| 	mockSGClient := az.SecurityGroupsClient.(*mocksecuritygroupclient.MockInterface) | ||||
| 	mockSGClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{ | ||||
| 		RawError: fmt.Errorf(operationCanceledErrorMessage), | ||||
| 	}) | ||||
| 	mockSGClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "sg", gomock.Any()).Return(network.SecurityGroup{}, nil) | ||||
|  | ||||
| 	err := az.CreateOrUpdateSecurityGroup(network.SecurityGroup{Name: pointer.String("sg")}) | ||||
| 	assert.EqualError(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")), err.Error()) | ||||
|  | ||||
| 	// security group should be removed from cache if the operation is canceled | ||||
| 	shouldBeEmpty, err := az.nsgCache.Get("sg", cache.CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Empty(t, shouldBeEmpty) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateLB(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	referencedResourceNotProvisionedRawErrorString := `Code="ReferencedResourceNotProvisioned" Message="Cannot proceed with operation because resource /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip used by resource /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb is not in Succeeded state. Resource is in Failed state and the last operation that updated/is updating the resource is PutPublicIpAddressOperation."` | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{RawError: fmt.Errorf(referencedResourceNotProvisionedRawErrorString)}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf(referencedResourceNotProvisionedRawErrorString)), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		az.lbCache.Set("lb", "test") | ||||
|  | ||||
| 		mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface) | ||||
| 		mockLBClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr) | ||||
| 		mockLBClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "lb", gomock.Any()).Return(network.LoadBalancer{}, nil) | ||||
|  | ||||
| 		mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 		mockPIPClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(nil).AnyTimes() | ||||
| 		mockPIPClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(network.PublicIPAddress{ | ||||
| 			Name: pointer.String("pip"), | ||||
| 			PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ | ||||
| 				ProvisioningState: pointer.String("Succeeded"), | ||||
| 			}, | ||||
| 		}, nil).AnyTimes() | ||||
|  | ||||
| 		err := az.CreateOrUpdateLB(&v1.Service{}, network.LoadBalancer{ | ||||
| 			Name: pointer.String("lb"), | ||||
| 			Etag: pointer.String("etag"), | ||||
| 		}) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
|  | ||||
| 		// loadbalancer should be removed from cache if the etag is mismatch or the operation is canceled | ||||
| 		shouldBeEmpty, err := az.lbCache.Get("lb", cache.CacheReadTypeDefault) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Empty(t, shouldBeEmpty) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestListLB(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusNotFound}, | ||||
| 			expectedErr: nil, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface) | ||||
| 		mockLBClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(nil, test.clientErr) | ||||
|  | ||||
| 		pips, err := az.ListLB(&v1.Service{}) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 		assert.Empty(t, pips) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestListPIP(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusNotFound}, | ||||
| 			expectedErr: nil, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 		mockPIPClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(nil, test.clientErr) | ||||
|  | ||||
| 		pips, err := az.ListPIP(&v1.Service{}, az.ResourceGroup) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 		assert.Empty(t, pips) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdatePIP(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 	mockPIPClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError}) | ||||
|  | ||||
| 	err := az.CreateOrUpdatePIP(&v1.Service{}, az.ResourceGroup, network.PublicIPAddress{Name: pointer.String("nic")}) | ||||
| 	assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateInterface(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface) | ||||
| 	mockInterfaceClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError}) | ||||
|  | ||||
| 	err := az.CreateOrUpdateInterface(&v1.Service{}, network.Interface{Name: pointer.String("nic")}) | ||||
| 	assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err) | ||||
| } | ||||
|  | ||||
| func TestDeletePublicIP(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 	mockPIPClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "pip").Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError}) | ||||
|  | ||||
| 	err := az.DeletePublicIP(&v1.Service{}, az.ResourceGroup, "pip") | ||||
| 	assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err) | ||||
| } | ||||
|  | ||||
| func TestDeleteLB(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface) | ||||
| 	mockLBClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "lb").Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError}) | ||||
|  | ||||
| 	err := az.DeleteLB(&v1.Service{}, "lb") | ||||
| 	assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateRouteTable(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		az.rtCache.Set("rt", "test") | ||||
|  | ||||
| 		mockRTClient := az.RouteTablesClient.(*mockroutetableclient.MockInterface) | ||||
| 		mockRTClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr) | ||||
| 		mockRTClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "rt", gomock.Any()).Return(network.RouteTable{}, nil) | ||||
|  | ||||
| 		err := az.CreateOrUpdateRouteTable(network.RouteTable{ | ||||
| 			Name: pointer.String("rt"), | ||||
| 			Etag: pointer.String("etag"), | ||||
| 		}) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
|  | ||||
| 		// route table should be removed from cache if the etag is mismatch or the operation is canceled | ||||
| 		shouldBeEmpty, err := az.rtCache.Get("rt", cache.CacheReadTypeDefault) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Empty(t, shouldBeEmpty) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateRoute(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   nil, | ||||
| 			expectedErr: nil, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
| 		az.rtCache.Set("rt", "test") | ||||
|  | ||||
| 		mockRTClient := az.RoutesClient.(*mockrouteclient.MockInterface) | ||||
| 		mockRTClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "rt", gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr) | ||||
|  | ||||
| 		mockRTableClient := az.RouteTablesClient.(*mockroutetableclient.MockInterface) | ||||
| 		mockRTableClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "rt", gomock.Any()).Return(network.RouteTable{}, nil) | ||||
|  | ||||
| 		err := az.CreateOrUpdateRoute(network.Route{ | ||||
| 			Name: pointer.String("rt"), | ||||
| 			Etag: pointer.String("etag"), | ||||
| 		}) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
|  | ||||
| 		shouldBeEmpty, err := az.rtCache.Get("rt", cache.CacheReadTypeDefault) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Empty(t, shouldBeEmpty) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDeleteRouteWithName(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   nil, | ||||
| 			expectedErr: nil, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
|  | ||||
| 		mockRTClient := az.RoutesClient.(*mockrouteclient.MockInterface) | ||||
| 		mockRTClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "rt", "rt").Return(test.clientErr) | ||||
|  | ||||
| 		err := az.DeleteRouteWithName("rt") | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateVMSS(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		vmss        compute.VirtualMachineScaleSet | ||||
| 		clientErr   *retry.Error | ||||
| 		expectedErr *retry.Error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 			expectedErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{HTTPStatusCode: http.StatusTooManyRequests}, | ||||
| 			expectedErr: &retry.Error{HTTPStatusCode: http.StatusTooManyRequests}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			clientErr:   &retry.Error{RawError: fmt.Errorf("azure cloud provider rate limited(write) for operation CreateOrUpdate")}, | ||||
| 			expectedErr: &retry.Error{RawError: fmt.Errorf("azure cloud provider rate limited(write) for operation CreateOrUpdate")}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			vmss: compute.VirtualMachineScaleSet{ | ||||
| 				VirtualMachineScaleSetProperties: &compute.VirtualMachineScaleSetProperties{ | ||||
| 					ProvisioningState: &virtualMachineScaleSetsDeallocating, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		az := GetTestCloud(ctrl) | ||||
|  | ||||
| 		mockVMSSClient := az.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface) | ||||
| 		mockVMSSClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, testVMSSName).Return(test.vmss, test.clientErr) | ||||
|  | ||||
| 		err := az.CreateOrUpdateVMSS(az.ResourceGroup, testVMSSName, compute.VirtualMachineScaleSet{}) | ||||
| 		assert.Equal(t, test.expectedErr, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRequestBackoff(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	az.CloudProviderBackoff = true | ||||
| 	az.ResourceRequestBackoff = wait.Backoff{Steps: 3} | ||||
|  | ||||
| 	backoff := az.RequestBackoff() | ||||
| 	assert.Equal(t, wait.Backoff{Steps: 3}, backoff) | ||||
|  | ||||
| } | ||||
| @@ -1,648 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/binary" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
| 	azstorage "github.com/Azure/azure-sdk-for-go/storage" | ||||
| 	"github.com/rubiojr/go-vhd/vhd" | ||||
|  | ||||
| 	kwait "k8s.io/apimachinery/pkg/util/wait" | ||||
| 	volerr "k8s.io/cloud-provider/volume/errors" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // Attention: blob disk feature is deprecated | ||||
| const ( | ||||
| 	vhdContainerName         = "vhds" | ||||
| 	useHTTPSForBlobBasedDisk = true | ||||
| 	blobServiceName          = "blob" | ||||
| ) | ||||
|  | ||||
| type storageAccountState struct { | ||||
| 	name                    string | ||||
| 	saType                  storage.SkuName | ||||
| 	key                     string | ||||
| 	diskCount               int32 | ||||
| 	isValidating            int32 | ||||
| 	defaultContainerCreated bool | ||||
| } | ||||
|  | ||||
| // BlobDiskController : blob disk controller struct | ||||
| type BlobDiskController struct { | ||||
| 	common   *controllerCommon | ||||
| 	accounts map[string]*storageAccountState | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	accountsLock = &sync.Mutex{} | ||||
| ) | ||||
|  | ||||
| func (c *BlobDiskController) initStorageAccounts() { | ||||
| 	accountsLock.Lock() | ||||
| 	defer accountsLock.Unlock() | ||||
|  | ||||
| 	if c.accounts == nil { | ||||
| 		// get accounts | ||||
| 		accounts, err := c.getAllStorageAccounts() | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("azureDisk - getAllStorageAccounts error: %v", err) | ||||
| 			c.accounts = make(map[string]*storageAccountState) | ||||
| 		} | ||||
| 		c.accounts = accounts | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CreateVolume creates a VHD blob in a storage account that has storageType and location using the given storage account. | ||||
| // If no storage account is given, search all the storage accounts associated with the resource group and pick one that | ||||
| // fits storage type and location. | ||||
| func (c *BlobDiskController) CreateVolume(blobName, accountName, accountType, location string, requestGB int) (string, string, int, error) { | ||||
| 	accountOptions := &AccountOptions{ | ||||
| 		Name:                   accountName, | ||||
| 		Type:                   accountType, | ||||
| 		Kind:                   string(defaultStorageAccountKind), | ||||
| 		ResourceGroup:          c.common.resourceGroup, | ||||
| 		Location:               location, | ||||
| 		EnableHTTPSTrafficOnly: true, | ||||
| 	} | ||||
| 	account, key, err := c.common.cloud.EnsureStorageAccount(accountOptions, dedicatedDiskAccountNamePrefix) | ||||
| 	if err != nil { | ||||
| 		return "", "", 0, fmt.Errorf("could not get storage key for storage account %s: %v", accountName, err) | ||||
| 	} | ||||
|  | ||||
| 	client, err := azstorage.NewBasicClientOnSovereignCloud(account, key, c.common.cloud.Environment) | ||||
| 	if err != nil { | ||||
| 		return "", "", 0, err | ||||
| 	} | ||||
| 	blobClient := client.GetBlobService() | ||||
|  | ||||
| 	// create a page blob in this account's vhd container | ||||
| 	diskName, diskURI, err := c.createVHDBlobDisk(blobClient, account, blobName, vhdContainerName, int64(requestGB)) | ||||
| 	if err != nil { | ||||
| 		return "", "", 0, err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(4).Infof("azureDisk - created vhd blob uri: %s", diskURI) | ||||
| 	return diskName, diskURI, requestGB, err | ||||
| } | ||||
|  | ||||
| // DeleteVolume deletes a VHD blob | ||||
| func (c *BlobDiskController) DeleteVolume(diskURI string) error { | ||||
| 	klog.V(4).Infof("azureDisk - begin to delete volume %s", diskURI) | ||||
| 	accountName, blob, err := c.common.cloud.getBlobNameAndAccountFromURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to parse vhd URI %v", err) | ||||
| 	} | ||||
| 	key, err := c.common.cloud.GetStorageAccesskey(accountName, c.common.resourceGroup) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("no key for storage account %s, err %v", accountName, err) | ||||
| 	} | ||||
| 	err = c.common.cloud.deleteVhdBlob(accountName, key, blob) | ||||
| 	if err != nil { | ||||
| 		klog.Warningf("azureDisk - failed to delete blob %s err: %v", diskURI, err) | ||||
| 		detail := err.Error() | ||||
| 		if strings.Contains(detail, errLeaseIDMissing) { | ||||
| 			// disk is still being used | ||||
| 			// see https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.storage.blob.protocol.bloberrorcodestrings.leaseidmissing.aspx | ||||
| 			return volerr.NewDeletedVolumeInUseError(fmt.Sprintf("disk %q is still in use while being deleted", diskURI)) | ||||
| 		} | ||||
| 		return fmt.Errorf("failed to delete vhd %v, account %s, blob %s, err: %v", diskURI, accountName, blob, err) | ||||
| 	} | ||||
| 	klog.V(4).Infof("azureDisk - blob %s deleted", diskURI) | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // get diskURI https://foo.blob.core.windows.net/vhds/bar.vhd and return foo (account) and bar.vhd (blob name) | ||||
| func (c *BlobDiskController) getBlobNameAndAccountFromURI(diskURI string) (string, string, error) { | ||||
| 	scheme := "http" | ||||
| 	if useHTTPSForBlobBasedDisk { | ||||
| 		scheme = "https" | ||||
| 	} | ||||
| 	host := fmt.Sprintf("%s://(.*).%s.%s", scheme, blobServiceName, c.common.storageEndpointSuffix) | ||||
| 	reStr := fmt.Sprintf("%s/%s/(.*)", host, vhdContainerName) | ||||
| 	re := regexp.MustCompile(reStr) | ||||
| 	res := re.FindSubmatch([]byte(diskURI)) | ||||
| 	if len(res) < 3 { | ||||
| 		return "", "", fmt.Errorf("invalid vhd URI for regex %s: %s", reStr, diskURI) | ||||
| 	} | ||||
| 	return string(res[1]), string(res[2]), nil | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) createVHDBlobDisk(blobClient azstorage.BlobStorageClient, accountName, vhdName, containerName string, sizeGB int64) (string, string, error) { | ||||
| 	container := blobClient.GetContainerReference(containerName) | ||||
| 	size := 1024 * 1024 * 1024 * sizeGB | ||||
| 	vhdSize := size + vhd.VHD_HEADER_SIZE /* header size */ | ||||
| 	// Blob name in URL must end with '.vhd' extension. | ||||
| 	vhdName = vhdName + ".vhd" | ||||
|  | ||||
| 	tags := make(map[string]string) | ||||
| 	tags["createdby"] = "k8sAzureDataDisk" | ||||
| 	klog.V(4).Infof("azureDisk - creating page blob %s in container %s account %s", vhdName, containerName, accountName) | ||||
|  | ||||
| 	blob := container.GetBlobReference(vhdName) | ||||
| 	blob.Properties.ContentLength = vhdSize | ||||
| 	blob.Metadata = tags | ||||
| 	err := blob.PutPageBlob(nil) | ||||
| 	if err != nil { | ||||
| 		// if container doesn't exist, create one and retry PutPageBlob | ||||
| 		detail := err.Error() | ||||
| 		if strings.Contains(detail, errContainerNotFound) { | ||||
| 			err = container.Create(&azstorage.CreateContainerOptions{Access: azstorage.ContainerAccessTypePrivate}) | ||||
| 			if err == nil { | ||||
| 				err = blob.PutPageBlob(nil) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("failed to put page blob %s in container %s: %v", vhdName, containerName, err) | ||||
| 	} | ||||
|  | ||||
| 	// add VHD signature to the blob | ||||
| 	h, err := createVHDHeader(uint64(size)) | ||||
| 	if err != nil { | ||||
| 		blob.DeleteIfExists(nil) | ||||
| 		return "", "", fmt.Errorf("failed to create vhd header, err: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	blobRange := azstorage.BlobRange{ | ||||
| 		Start: uint64(size), | ||||
| 		End:   uint64(vhdSize - 1), | ||||
| 	} | ||||
| 	if err = blob.WriteRange(blobRange, bytes.NewBuffer(h[:vhd.VHD_HEADER_SIZE]), nil); err != nil { | ||||
| 		klog.Infof("azureDisk - failed to put header page for data disk %s in container %s account %s, error was %s\n", | ||||
| 			vhdName, containerName, accountName, err.Error()) | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	scheme := "http" | ||||
| 	if useHTTPSForBlobBasedDisk { | ||||
| 		scheme = "https" | ||||
| 	} | ||||
|  | ||||
| 	host := fmt.Sprintf("%s://%s.%s.%s", scheme, accountName, blobServiceName, c.common.storageEndpointSuffix) | ||||
| 	uri := fmt.Sprintf("%s/%s/%s", host, containerName, vhdName) | ||||
| 	return vhdName, uri, nil | ||||
| } | ||||
|  | ||||
| // delete a vhd blob | ||||
| func (c *BlobDiskController) deleteVhdBlob(accountName, accountKey, blobName string) error { | ||||
| 	client, err := azstorage.NewBasicClientOnSovereignCloud(accountName, accountKey, c.common.cloud.Environment) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	blobSvc := client.GetBlobService() | ||||
|  | ||||
| 	container := blobSvc.GetContainerReference(vhdContainerName) | ||||
| 	blob := container.GetBlobReference(blobName) | ||||
| 	return blob.Delete(nil) | ||||
| } | ||||
|  | ||||
| // CreateBlobDisk : create a blob disk in a node | ||||
| func (c *BlobDiskController) CreateBlobDisk(dataDiskName string, storageAccountType storage.SkuName, sizeGB int) (string, error) { | ||||
| 	klog.V(4).Infof("azureDisk - creating blob data disk named:%s on StorageAccountType:%s", dataDiskName, storageAccountType) | ||||
|  | ||||
| 	c.initStorageAccounts() | ||||
|  | ||||
| 	storageAccountName, err := c.findSANameForDisk(storageAccountType) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	blobClient, err := c.getBlobSvcClient(storageAccountName) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	_, diskURI, err := c.createVHDBlobDisk(blobClient, storageAccountName, dataDiskName, vhdContainerName, int64(sizeGB)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	atomic.AddInt32(&c.accounts[storageAccountName].diskCount, 1) | ||||
|  | ||||
| 	return diskURI, nil | ||||
| } | ||||
|  | ||||
| // DeleteBlobDisk : delete a blob disk from a node | ||||
| func (c *BlobDiskController) DeleteBlobDisk(diskURI string) error { | ||||
| 	storageAccountName, vhdName, err := diskNameAndSANameFromURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, ok := c.accounts[storageAccountName] | ||||
| 	if !ok { | ||||
| 		// the storage account is specified by user | ||||
| 		klog.V(4).Infof("azureDisk - deleting volume %s", diskURI) | ||||
| 		return c.DeleteVolume(diskURI) | ||||
| 	} | ||||
|  | ||||
| 	blobSvc, err := c.getBlobSvcClient(storageAccountName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(4).Infof("azureDisk - About to delete vhd file %s on storage account %s container %s", vhdName, storageAccountName, vhdContainerName) | ||||
|  | ||||
| 	container := blobSvc.GetContainerReference(vhdContainerName) | ||||
| 	blob := container.GetBlobReference(vhdName) | ||||
| 	_, err = blob.DeleteIfExists(nil) | ||||
|  | ||||
| 	if c.accounts[storageAccountName].diskCount == -1 { | ||||
| 		if diskCount, err := c.getDiskCount(storageAccountName); err != nil { | ||||
| 			c.accounts[storageAccountName].diskCount = int32(diskCount) | ||||
| 		} else { | ||||
| 			klog.Warningf("azureDisk - failed to get disk count for %s however the delete disk operation was ok", storageAccountName) | ||||
| 			return nil // we have failed to acquire a new count. not an error condition | ||||
| 		} | ||||
| 	} | ||||
| 	atomic.AddInt32(&c.accounts[storageAccountName].diskCount, -1) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) getStorageAccountKey(SAName string) (string, error) { | ||||
| 	if account, exists := c.accounts[SAName]; exists && account.key != "" { | ||||
| 		return c.accounts[SAName].key, nil | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	listKeysResult, rerr := c.common.cloud.StorageAccountClient.ListKeys(ctx, c.common.resourceGroup, SAName) | ||||
| 	if rerr != nil { | ||||
| 		return "", rerr.Error() | ||||
| 	} | ||||
| 	if listKeysResult.Keys == nil { | ||||
| 		return "", fmt.Errorf("azureDisk - empty listKeysResult in storage account:%s keys", SAName) | ||||
| 	} | ||||
| 	for _, v := range *listKeysResult.Keys { | ||||
| 		if v.Value != nil && *v.Value == "key1" { | ||||
| 			if _, ok := c.accounts[SAName]; !ok { | ||||
| 				klog.Warningf("azureDisk - account %s was not cached while getting keys", SAName) | ||||
| 				return *v.Value, nil | ||||
| 			} | ||||
| 			c.accounts[SAName].key = *v.Value | ||||
| 			return c.accounts[SAName].key, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return "", fmt.Errorf("couldn't find key named key1 in storage account:%s keys", SAName) | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) getBlobSvcClient(SAName string) (azstorage.BlobStorageClient, error) { | ||||
| 	key := "" | ||||
| 	var client azstorage.Client | ||||
| 	var blobSvc azstorage.BlobStorageClient | ||||
| 	var err error | ||||
| 	if key, err = c.getStorageAccountKey(SAName); err != nil { | ||||
| 		return blobSvc, err | ||||
| 	} | ||||
|  | ||||
| 	if client, err = azstorage.NewBasicClientOnSovereignCloud(SAName, key, c.common.cloud.Environment); err != nil { | ||||
| 		return blobSvc, err | ||||
| 	} | ||||
|  | ||||
| 	blobSvc = client.GetBlobService() | ||||
| 	return blobSvc, nil | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) ensureDefaultContainer(storageAccountName string) error { | ||||
| 	var err error | ||||
| 	var blobSvc azstorage.BlobStorageClient | ||||
|  | ||||
| 	// short circuit the check via local cache | ||||
| 	// we are forgiving the fact that account may not be in cache yet | ||||
| 	if v, ok := c.accounts[storageAccountName]; ok && v.defaultContainerCreated { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// not cached, check existence and readiness | ||||
| 	bExist, provisionState, _ := c.getStorageAccountState(storageAccountName) | ||||
|  | ||||
| 	// account does not exist | ||||
| 	if !bExist { | ||||
| 		return fmt.Errorf("azureDisk - account %s does not exist while trying to create/ensure default container", storageAccountName) | ||||
| 	} | ||||
|  | ||||
| 	// account exists but not ready yet | ||||
| 	if provisionState != storage.Succeeded { | ||||
| 		// we don't want many attempts to validate the account readiness | ||||
| 		// here hence we are locking | ||||
| 		counter := 1 | ||||
| 		for swapped := atomic.CompareAndSwapInt32(&c.accounts[storageAccountName].isValidating, 0, 1); swapped != true; { | ||||
| 			time.Sleep(3 * time.Second) | ||||
| 			counter = counter + 1 | ||||
| 			// check if we passed the max sleep | ||||
| 			if counter >= 20 { | ||||
| 				return fmt.Errorf("azureDisk - timeout waiting to acquire lock to validate account:%s readiness", storageAccountName) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// swapped | ||||
| 		defer func() { | ||||
| 			c.accounts[storageAccountName].isValidating = 0 | ||||
| 		}() | ||||
|  | ||||
| 		// short circuit the check again. | ||||
| 		if v, ok := c.accounts[storageAccountName]; ok && v.defaultContainerCreated { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		err = kwait.ExponentialBackoff(defaultBackOff, func() (bool, error) { | ||||
| 			_, provisionState, err := c.getStorageAccountState(storageAccountName) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				klog.V(4).Infof("azureDisk - GetStorageAccount:%s err %s", storageAccountName, err.Error()) | ||||
| 				return false, nil // error performing the query - retryable | ||||
| 			} | ||||
|  | ||||
| 			if provisionState == storage.Succeeded { | ||||
| 				return true, nil | ||||
| 			} | ||||
|  | ||||
| 			klog.V(4).Infof("azureDisk - GetStorageAccount:%s not ready yet (not flagged Succeeded by ARM)", storageAccountName) | ||||
| 			return false, nil // back off and see if the account becomes ready on next retry | ||||
| 		}) | ||||
| 		// we have failed to ensure that account is ready for us to create | ||||
| 		// the default vhd container | ||||
| 		if err != nil { | ||||
| 			if err == kwait.ErrWaitTimeout { | ||||
| 				return fmt.Errorf("azureDisk - timed out waiting for storage account %s to become ready", storageAccountName) | ||||
| 			} | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if blobSvc, err = c.getBlobSvcClient(storageAccountName); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	container := blobSvc.GetContainerReference(vhdContainerName) | ||||
| 	bCreated, err := container.CreateIfNotExists(&azstorage.CreateContainerOptions{Access: azstorage.ContainerAccessTypePrivate}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if bCreated { | ||||
| 		klog.V(2).Infof("azureDisk - storage account:%s had no default container(%s) and it was created \n", storageAccountName, vhdContainerName) | ||||
| 	} | ||||
|  | ||||
| 	// flag so we no longer have to check on ARM | ||||
| 	c.accounts[storageAccountName].defaultContainerCreated = true | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Gets Disk counts per storage account | ||||
| func (c *BlobDiskController) getDiskCount(SAName string) (int, error) { | ||||
| 	// if we have it in cache | ||||
| 	if c.accounts[SAName].diskCount != -1 { | ||||
| 		return int(c.accounts[SAName].diskCount), nil | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	var blobSvc azstorage.BlobStorageClient | ||||
|  | ||||
| 	if err = c.ensureDefaultContainer(SAName); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	if blobSvc, err = c.getBlobSvcClient(SAName); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	params := azstorage.ListBlobsParameters{} | ||||
|  | ||||
| 	container := blobSvc.GetContainerReference(vhdContainerName) | ||||
| 	response, err := container.ListBlobs(params) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	klog.V(4).Infof("azure-Disk -  refreshed data count for account %s and found %v", SAName, len(response.Blobs)) | ||||
| 	c.accounts[SAName].diskCount = int32(len(response.Blobs)) | ||||
|  | ||||
| 	return int(c.accounts[SAName].diskCount), nil | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) getAllStorageAccounts() (map[string]*storageAccountState, error) { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) | ||||
| 	defer cancel() | ||||
| 	accountList, rerr := c.common.cloud.StorageAccountClient.ListByResourceGroup(ctx, c.common.resourceGroup) | ||||
| 	if rerr != nil { | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	accounts := make(map[string]*storageAccountState) | ||||
| 	for _, v := range accountList { | ||||
| 		if v.Name == nil || v.Sku == nil { | ||||
| 			klog.Info("azureDisk - accountListResult Name or Sku is nil") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !strings.HasPrefix(*v.Name, sharedDiskAccountNamePrefix) { | ||||
| 			continue | ||||
| 		} | ||||
| 		klog.Infof("azureDisk - identified account %s as part of shared PVC accounts", *v.Name) | ||||
|  | ||||
| 		saState := &storageAccountState{ | ||||
| 			name:      *v.Name, | ||||
| 			saType:    (*v.Sku).Name, | ||||
| 			diskCount: -1, | ||||
| 		} | ||||
|  | ||||
| 		accounts[*v.Name] = saState | ||||
| 	} | ||||
|  | ||||
| 	return accounts, nil | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) createStorageAccount(storageAccountName string, storageAccountType storage.SkuName, location string, checkMaxAccounts bool) error { | ||||
| 	bExist, _, _ := c.getStorageAccountState(storageAccountName) | ||||
| 	if bExist { | ||||
| 		newAccountState := &storageAccountState{ | ||||
| 			diskCount: -1, | ||||
| 			saType:    storageAccountType, | ||||
| 			name:      storageAccountName, | ||||
| 		} | ||||
|  | ||||
| 		c.addAccountState(storageAccountName, newAccountState) | ||||
| 	} | ||||
| 	// Account Does not exist | ||||
| 	if !bExist { | ||||
| 		if len(c.accounts) == maxStorageAccounts && checkMaxAccounts { | ||||
| 			return fmt.Errorf("azureDisk - can not create new storage account, current storage accounts count:%v Max is:%v", len(c.accounts), maxStorageAccounts) | ||||
| 		} | ||||
|  | ||||
| 		klog.V(2).Infof("azureDisk - Creating storage account %s type %s", storageAccountName, string(storageAccountType)) | ||||
|  | ||||
| 		cp := storage.AccountCreateParameters{ | ||||
| 			Sku: &storage.Sku{Name: storageAccountType}, | ||||
| 			// switch to use StorageV2 as it's recommended according to https://docs.microsoft.com/en-us/azure/storage/common/storage-account-options | ||||
| 			Kind:     defaultStorageAccountKind, | ||||
| 			Tags:     map[string]*string{"created-by": pointer.String("azure-dd")}, | ||||
| 			Location: &location} | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
|  | ||||
| 		err := c.common.cloud.StorageAccountClient.Create(ctx, c.common.resourceGroup, storageAccountName, cp) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("Create Storage Account: %s, error: %v", storageAccountName, err) | ||||
| 		} | ||||
|  | ||||
| 		newAccountState := &storageAccountState{ | ||||
| 			diskCount: -1, | ||||
| 			saType:    storageAccountType, | ||||
| 			name:      storageAccountName, | ||||
| 		} | ||||
|  | ||||
| 		c.addAccountState(storageAccountName, newAccountState) | ||||
| 	} | ||||
|  | ||||
| 	// finally, make sure that we default container is created | ||||
| 	// before handing it back over | ||||
| 	return c.ensureDefaultContainer(storageAccountName) | ||||
| } | ||||
|  | ||||
| // finds a new suitable storageAccount for this disk | ||||
| func (c *BlobDiskController) findSANameForDisk(storageAccountType storage.SkuName) (string, error) { | ||||
| 	maxDiskCount := maxDisksPerStorageAccounts | ||||
| 	SAName := "" | ||||
| 	totalDiskCounts := 0 | ||||
| 	countAccounts := 0 // account of this type. | ||||
| 	for _, v := range c.accounts { | ||||
| 		// filter out any stand-alone disks/accounts | ||||
| 		if !strings.HasPrefix(v.name, sharedDiskAccountNamePrefix) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// note: we compute avg stratified by type. | ||||
| 		// this is to enable user to grow per SA type to avoid low | ||||
| 		// avg utilization on one account type skewing all data. | ||||
|  | ||||
| 		if v.saType == storageAccountType { | ||||
| 			// compute average | ||||
| 			dCount, err := c.getDiskCount(v.name) | ||||
| 			if err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
| 			totalDiskCounts = totalDiskCounts + dCount | ||||
| 			countAccounts = countAccounts + 1 | ||||
| 			// empty account | ||||
| 			if dCount == 0 { | ||||
| 				klog.V(2).Infof("azureDisk - account %s identified for a new disk  is because it has 0 allocated disks", v.name) | ||||
| 				return v.name, nil // short circuit, avg is good and no need to adjust | ||||
| 			} | ||||
| 			// if this account is less allocated | ||||
| 			if dCount < maxDiskCount { | ||||
| 				maxDiskCount = dCount | ||||
| 				SAName = v.name | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// if we failed to find storageaccount | ||||
| 	if SAName == "" { | ||||
| 		klog.V(2).Infof("azureDisk - failed to identify a suitable account for new disk and will attempt to create new account") | ||||
| 		SAName = generateStorageAccountName(sharedDiskAccountNamePrefix) | ||||
| 		err := c.createStorageAccount(SAName, storageAccountType, c.common.location, true) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return SAName, nil | ||||
| 	} | ||||
|  | ||||
| 	disksAfter := totalDiskCounts + 1 // with the new one! | ||||
|  | ||||
| 	avgUtilization := float64(disksAfter) / float64(countAccounts*maxDisksPerStorageAccounts) | ||||
| 	aboveAvg := avgUtilization > storageAccountUtilizationBeforeGrowing | ||||
|  | ||||
| 	// avg are not create and we should create more accounts if we can | ||||
| 	if aboveAvg && countAccounts < maxStorageAccounts { | ||||
| 		klog.V(2).Infof("azureDisk - shared storageAccounts utilization(%v) >  grow-at-avg-utilization (%v). New storage account will be created", avgUtilization, storageAccountUtilizationBeforeGrowing) | ||||
| 		SAName = generateStorageAccountName(sharedDiskAccountNamePrefix) | ||||
| 		err := c.createStorageAccount(SAName, storageAccountType, c.common.location, true) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return SAName, nil | ||||
| 	} | ||||
|  | ||||
| 	// averages are not ok and we are at capacity (max storage accounts allowed) | ||||
| 	if aboveAvg && countAccounts == maxStorageAccounts { | ||||
| 		klog.Infof("azureDisk - shared storageAccounts utilization(%v) > grow-at-avg-utilization (%v). But k8s maxed on SAs for PVC(%v). k8s will now exceed grow-at-avg-utilization without adding accounts", | ||||
| 			avgUtilization, storageAccountUtilizationBeforeGrowing, maxStorageAccounts) | ||||
| 	} | ||||
|  | ||||
| 	// we found a  storage accounts && [ avg are ok || we reached max sa count ] | ||||
| 	return SAName, nil | ||||
| } | ||||
|  | ||||
| // Gets storage account exist, provisionStatus, Error if any | ||||
| func (c *BlobDiskController) getStorageAccountState(storageAccountName string) (bool, storage.ProvisioningState, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	account, rerr := c.common.cloud.StorageAccountClient.GetProperties(ctx, c.common.resourceGroup, storageAccountName) | ||||
| 	if rerr != nil { | ||||
| 		return false, "", rerr.Error() | ||||
| 	} | ||||
| 	return true, account.AccountProperties.ProvisioningState, nil | ||||
| } | ||||
|  | ||||
| func (c *BlobDiskController) addAccountState(key string, state *storageAccountState) { | ||||
| 	accountsLock.Lock() | ||||
| 	defer accountsLock.Unlock() | ||||
|  | ||||
| 	if _, ok := c.accounts[key]; !ok { | ||||
| 		c.accounts[key] = state | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func createVHDHeader(size uint64) ([]byte, error) { | ||||
| 	h := vhd.CreateFixedHeader(size, &vhd.VHDOptions{}) | ||||
| 	b := new(bytes.Buffer) | ||||
| 	err := binary.Write(b, binary.BigEndian, h) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return b.Bytes(), nil | ||||
| } | ||||
|  | ||||
| func diskNameAndSANameFromURI(diskURI string) (string, string, error) { | ||||
| 	uri, err := url.Parse(diskURI) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	hostName := uri.Host | ||||
| 	storageAccountName := strings.Split(hostName, ".")[0] | ||||
|  | ||||
| 	segments := strings.Split(uri.Path, "/") | ||||
| 	diskNameVhd := segments[len(segments)-1] | ||||
|  | ||||
| 	return storageAccountName, diskNameVhd, nil | ||||
| } | ||||
| @@ -1,361 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
| 	azstorage "github.com/Azure/azure-sdk-for-go/storage" | ||||
| 	autorestazure "github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| var retryError500 = retry.Error{HTTPStatusCode: http.StatusInternalServerError} | ||||
|  | ||||
| func GetTestBlobDiskController(t *testing.T) BlobDiskController { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	az.Environment = autorestazure.PublicCloud | ||||
| 	common := &controllerCommon{cloud: az, resourceGroup: "rg", location: "westus"} | ||||
|  | ||||
| 	return BlobDiskController{ | ||||
| 		common:   common, | ||||
| 		accounts: make(map[string]*storageAccountState), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestInitStorageAccounts(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
| 	b.accounts = nil | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return([]storage.Account{}, &retryError500) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	b.initStorageAccounts() | ||||
| 	assert.Empty(t, b.accounts) | ||||
|  | ||||
| 	mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return([]storage.Account{ | ||||
| 		{ | ||||
| 			Name: pointer.String("ds-0"), | ||||
| 			Sku:  &storage.Sku{Name: "sku"}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	b.initStorageAccounts() | ||||
| 	assert.Equal(t, 1, len(b.accounts)) | ||||
| } | ||||
|  | ||||
| func TestCreateVolume(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{}, &retryError500) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	diskName, diskURI, requestGB, err := b.CreateVolume("testBlob", "testsa", "type", b.common.location, 10) | ||||
| 	expectedErr := fmt.Errorf("could not get storage key for storage account testsa: could not get storage key for "+ | ||||
| 		"storage account testsa: Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)) | ||||
| 	assert.EqualError(t, err, expectedErr.Error()) | ||||
| 	assert.Empty(t, diskName) | ||||
| 	assert.Empty(t, diskURI) | ||||
| 	assert.Zero(t, requestGB) | ||||
|  | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("dmFsdWUK"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	diskName, diskURI, requestGB, err = b.CreateVolume("testBlob", "testsa", "type", b.common.location, 10) | ||||
| 	expectedErrStr := "failed to put page blob testBlob.vhd in container vhds: storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled." | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErrStr)) | ||||
| 	assert.Empty(t, diskName) | ||||
| 	assert.Empty(t, diskURI) | ||||
| 	assert.Zero(t, requestGB) | ||||
| } | ||||
|  | ||||
| func TestDeleteVolume(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
| 	b.common.cloud.BlobDiskController = &b | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "foo").Return(storage.AccountListKeysResult{}, &retryError500).Times(2) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	fakeDiskURL := "fake" | ||||
| 	diskURL := "https://foo.blob./vhds/bar.vhd" | ||||
| 	err := b.DeleteVolume(diskURL) | ||||
| 	expectedErr := fmt.Errorf("no key for storage account foo, err Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)) | ||||
| 	assert.EqualError(t, err, expectedErr.Error()) | ||||
|  | ||||
| 	err = b.DeleteVolume(diskURL) | ||||
| 	assert.EqualError(t, err, expectedErr.Error()) | ||||
|  | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "foo").Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("dmFsdWUK"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
|  | ||||
| 	err = b.DeleteVolume(fakeDiskURL) | ||||
| 	expectedErr = fmt.Errorf("failed to parse vhd URI invalid vhd URI for regex https://(.*).blob./vhds/(.*): fake") | ||||
| 	assert.Equal(t, expectedErr, err) | ||||
|  | ||||
| 	err = b.DeleteVolume(diskURL) | ||||
| 	expectedErrStr := "failed to delete vhd https://foo.blob./vhds/bar.vhd, account foo, blob bar.vhd, err: storage: service returned error: " + | ||||
| 		"StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled." | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErrStr)) | ||||
| } | ||||
|  | ||||
| func TestCreateVHDBlobDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	b.common.cloud.Environment = autorestazure.PublicCloud | ||||
| 	client, err := azstorage.NewBasicClientOnSovereignCloud("testsa", "a2V5Cg==", b.common.cloud.Environment) | ||||
| 	assert.NoError(t, err) | ||||
| 	blobClient := client.GetBlobService() | ||||
|  | ||||
| 	_, _, err = b.createVHDBlobDisk(blobClient, "testsa", "blob", vhdContainerName, int64(10)) | ||||
| 	expectedErr := "failed to put page blob blob.vhd in container vhds: storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled." | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErr)) | ||||
| } | ||||
|  | ||||
| func TestGetAllStorageAccounts(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	expectedStorageAccounts := []storage.Account{ | ||||
| 		{ | ||||
| 			Name: pointer.String("this-should-be-skipped"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.String("this-should-be-skipped"), | ||||
| 			Sku:  &storage.Sku{Name: "sku"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.String("ds-0"), | ||||
| 			Sku:  &storage.Sku{Name: "sku"}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return(expectedStorageAccounts, nil) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	accounts, err := b.getAllStorageAccounts() | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, len(accounts)) | ||||
| } | ||||
|  | ||||
| func TestEnsureDefaultContainer(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500) | ||||
| 	err := b.ensureDefaultContainer("testsa") | ||||
| 	expectedErr := fmt.Errorf("azureDisk - account testsa does not exist while trying to create/ensure default container") | ||||
| 	assert.Equal(t, expectedErr, err) | ||||
|  | ||||
| 	b.accounts["testsa"] = &storageAccountState{defaultContainerCreated: true} | ||||
| 	err = b.ensureDefaultContainer("testsa") | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	b.accounts["testsa"] = &storageAccountState{isValidating: 0} | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{ | ||||
| 		AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Creating}, | ||||
| 	}, nil) | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500) | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{ | ||||
| 		AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Succeeded}, | ||||
| 	}, nil) | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("key1"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	err = b.ensureDefaultContainer("testsa") | ||||
| 	expectedErrStr := "storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled." | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErrStr)) | ||||
| } | ||||
|  | ||||
| func TestGetDiskCount(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	b.accounts["testsa"] = &storageAccountState{diskCount: 1} | ||||
| 	count, err := b.getDiskCount("testsa") | ||||
| 	assert.Equal(t, 1, count) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	b.accounts["testsa"] = &storageAccountState{diskCount: -1} | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500) | ||||
| 	count, err = b.getDiskCount("testsa") | ||||
| 	assert.Zero(t, count) | ||||
| 	expectedErr := fmt.Errorf("azureDisk - account testsa does not exist while trying to create/ensure default container") | ||||
| 	assert.Equal(t, expectedErr, err) | ||||
|  | ||||
| 	b.accounts["testsa"].defaultContainerCreated = true | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("key1"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	count, err = b.getDiskCount("testsa") | ||||
| 	expectedErrStr := "storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled." | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErrStr)) | ||||
| 	assert.Zero(t, count) | ||||
| } | ||||
|  | ||||
| func TestFindSANameForDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
|  | ||||
| 	b.accounts = map[string]*storageAccountState{ | ||||
| 		"this-shall-be-skipped": {name: "fake"}, | ||||
| 		"ds0": { | ||||
| 			name:      "ds0", | ||||
| 			saType:    storage.StandardGRS, | ||||
| 			diskCount: 50, | ||||
| 		}, | ||||
| 	} | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.Account{}, &retryError500).Times(2) | ||||
| 	mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.Account{ | ||||
| 		AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Succeeded}, | ||||
| 	}, nil).Times(2) | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("key1"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	mockSAClient.EXPECT().Create(gomock.Any(), b.common.resourceGroup, gomock.Any(), gomock.Any()).Return(nil) | ||||
| 	name, err := b.findSANameForDisk(storage.StandardGRS) | ||||
| 	expectedErr := "does not exist while trying to create/ensure default container" | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErr)) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Empty(t, name) | ||||
|  | ||||
| 	b.accounts = make(map[string]*storageAccountState) | ||||
| 	name, err = b.findSANameForDisk(storage.StandardGRS) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Empty(t, name) | ||||
|  | ||||
| 	b.accounts = map[string]*storageAccountState{ | ||||
| 		"ds0": { | ||||
| 			name:      "ds0", | ||||
| 			saType:    storage.StandardGRS, | ||||
| 			diskCount: 0, | ||||
| 		}, | ||||
| 	} | ||||
| 	name, err = b.findSANameForDisk(storage.StandardGRS) | ||||
| 	assert.Equal(t, "ds0", name) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	for i := 0; i < maxStorageAccounts; i++ { | ||||
| 		b.accounts[fmt.Sprintf("ds%d", i)] = &storageAccountState{ | ||||
| 			name:      fmt.Sprintf("ds%d", i), | ||||
| 			saType:    storage.StandardGRS, | ||||
| 			diskCount: 59, | ||||
| 		} | ||||
| 	} | ||||
| 	name, err = b.findSANameForDisk(storage.StandardGRS) | ||||
| 	assert.NotEmpty(t, name) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
|  | ||||
| func TestCreateBlobDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	b := GetTestBlobDiskController(t) | ||||
| 	b.accounts = map[string]*storageAccountState{ | ||||
| 		"ds0": { | ||||
| 			name:      "ds0", | ||||
| 			saType:    storage.StandardGRS, | ||||
| 			diskCount: 0, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	b.common.cloud.StorageAccountClient = mockSAClient | ||||
| 	mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.AccountListKeysResult{ | ||||
| 		Keys: &[]storage.AccountKey{ | ||||
| 			{ | ||||
| 				KeyName: pointer.String("key1"), | ||||
| 				Value:   pointer.String("key1"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil) | ||||
| 	diskURI, err := b.CreateBlobDisk("datadisk", storage.StandardGRS, 10) | ||||
| 	expectedErr := "failed to put page blob datadisk.vhd in container vhds: storage: service returned error: StatusCode=403" | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, strings.Contains(err.Error(), expectedErr)) | ||||
| 	assert.Empty(t, diskURI) | ||||
| } | ||||
| @@ -1,95 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	cloudConfigNamespace  = "kube-system" | ||||
| 	cloudConfigKey        = "cloud-config" | ||||
| 	cloudConfigSecretName = "azure-cloud-provider" | ||||
| ) | ||||
|  | ||||
| // The config type for Azure cloud provider secret. Supported values are: | ||||
| // * file   : The values are read from local cloud-config file. | ||||
| // * secret : The values from secret would override all configures from local cloud-config file. | ||||
| // * merge  : The values from secret would override only configurations that are explicitly set in the secret. This is the default value. | ||||
| type cloudConfigType string | ||||
|  | ||||
| const ( | ||||
| 	cloudConfigTypeFile   cloudConfigType = "file" | ||||
| 	cloudConfigTypeSecret cloudConfigType = "secret" | ||||
| 	cloudConfigTypeMerge  cloudConfigType = "merge" | ||||
| ) | ||||
|  | ||||
| // InitializeCloudFromSecret initializes Azure cloud provider from Kubernetes secret. | ||||
| func (az *Cloud) InitializeCloudFromSecret() { | ||||
| 	config, err := az.getConfigFromSecret() | ||||
| 	if err != nil { | ||||
| 		klog.Warningf("Failed to get cloud-config from secret: %v, skip initializing from secret", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if config == nil { | ||||
| 		// Skip re-initialization if the config is not override. | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := az.InitializeCloudFromConfig(config, true); err != nil { | ||||
| 		klog.Errorf("Failed to initialize Azure cloud provider: %v", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getConfigFromSecret() (*Config, error) { | ||||
| 	// Read config from file and no override, return nil. | ||||
| 	if az.Config.CloudConfigType == cloudConfigTypeFile { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	secret, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Get(context.TODO(), cloudConfigSecretName, metav1.GetOptions{}) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get secret %s: %v", cloudConfigSecretName, err) | ||||
| 	} | ||||
|  | ||||
| 	cloudConfigData, ok := secret.Data[cloudConfigKey] | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("cloud-config is not set in the secret (%s)", cloudConfigSecretName) | ||||
| 	} | ||||
|  | ||||
| 	config := Config{} | ||||
| 	if az.Config.CloudConfigType == "" || az.Config.CloudConfigType == cloudConfigTypeMerge { | ||||
| 		// Merge cloud config, set default value to existing config. | ||||
| 		config = az.Config | ||||
| 	} | ||||
|  | ||||
| 	err = yaml.Unmarshal(cloudConfigData, &config) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse Azure cloud-config: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return &config, nil | ||||
| } | ||||
| @@ -1,268 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	fakeclient "k8s.io/client-go/kubernetes/fake" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/auth" | ||||
| 	"k8s.io/utils/pointer" | ||||
| 	"sigs.k8s.io/yaml" | ||||
| ) | ||||
|  | ||||
| func getTestConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		AzureAuthConfig: auth.AzureAuthConfig{ | ||||
| 			TenantID:        "TenantID", | ||||
| 			SubscriptionID:  "SubscriptionID", | ||||
| 			AADClientID:     "AADClientID", | ||||
| 			AADClientSecret: "AADClientSecret", | ||||
| 		}, | ||||
| 		ResourceGroup:               "ResourceGroup", | ||||
| 		RouteTableName:              "RouteTableName", | ||||
| 		RouteTableResourceGroup:     "RouteTableResourceGroup", | ||||
| 		Location:                    "Location", | ||||
| 		SubnetName:                  "SubnetName", | ||||
| 		VnetName:                    "VnetName", | ||||
| 		PrimaryAvailabilitySetName:  "PrimaryAvailabilitySetName", | ||||
| 		PrimaryScaleSetName:         "PrimaryScaleSetName", | ||||
| 		LoadBalancerSku:             "LoadBalancerSku", | ||||
| 		ExcludeMasterFromStandardLB: pointer.Bool(true), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestCloudConfigTypeSecretConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		AzureAuthConfig: auth.AzureAuthConfig{ | ||||
| 			TenantID:       "TenantID", | ||||
| 			SubscriptionID: "SubscriptionID", | ||||
| 		}, | ||||
| 		ResourceGroup:           "ResourceGroup", | ||||
| 		RouteTableName:          "RouteTableName", | ||||
| 		RouteTableResourceGroup: "RouteTableResourceGroup", | ||||
| 		SecurityGroupName:       "SecurityGroupName", | ||||
| 		CloudConfigType:         cloudConfigTypeSecret, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestCloudConfigTypeMergeConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		AzureAuthConfig: auth.AzureAuthConfig{ | ||||
| 			TenantID:       "TenantID", | ||||
| 			SubscriptionID: "SubscriptionID", | ||||
| 		}, | ||||
| 		ResourceGroup:           "ResourceGroup", | ||||
| 		RouteTableName:          "RouteTableName", | ||||
| 		RouteTableResourceGroup: "RouteTableResourceGroup", | ||||
| 		SecurityGroupName:       "SecurityGroupName", | ||||
| 		CloudConfigType:         cloudConfigTypeMerge, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestCloudConfigTypeMergeConfigExpected() *Config { | ||||
| 	config := getTestConfig() | ||||
| 	config.SecurityGroupName = "SecurityGroupName" | ||||
| 	config.CloudConfigType = cloudConfigTypeMerge | ||||
| 	return config | ||||
| } | ||||
|  | ||||
| func TestGetConfigFromSecret(t *testing.T) { | ||||
| 	emptyConfig := &Config{} | ||||
| 	badConfig := &Config{ResourceGroup: "DuplicateColumnsIncloud-config"} | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		existingConfig *Config | ||||
| 		secretConfig   *Config | ||||
| 		expected       *Config | ||||
| 		expectErr      bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "Azure config shouldn't be override when cloud config type is file", | ||||
| 			existingConfig: &Config{ | ||||
| 				ResourceGroup:   "ResourceGroup1", | ||||
| 				CloudConfigType: cloudConfigTypeFile, | ||||
| 			}, | ||||
| 			secretConfig: getTestConfig(), | ||||
| 			expected:     nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Azure config should be override when cloud config type is secret", | ||||
| 			existingConfig: getTestCloudConfigTypeSecretConfig(), | ||||
| 			secretConfig:   getTestConfig(), | ||||
| 			expected:       getTestConfig(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Azure config should be override when cloud config type is merge", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			secretConfig:   getTestConfig(), | ||||
| 			expected:       getTestCloudConfigTypeMergeConfigExpected(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Error should be reported when secret doesn't exists", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Error should be reported when secret exists but cloud-config data is not provided", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			secretConfig:   emptyConfig, | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Error should be reported when it failed to parse Azure cloud-config", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			secretConfig:   badConfig, | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			az := &Cloud{ | ||||
| 				KubeClient: fakeclient.NewSimpleClientset(), | ||||
| 			} | ||||
| 			if test.existingConfig != nil { | ||||
| 				az.Config = *test.existingConfig | ||||
| 			} | ||||
| 			if test.secretConfig != nil { | ||||
| 				secret := &v1.Secret{ | ||||
| 					Type: v1.SecretTypeOpaque, | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      "azure-cloud-provider", | ||||
| 						Namespace: "kube-system", | ||||
| 					}, | ||||
| 				} | ||||
| 				if test.secretConfig != emptyConfig && test.secretConfig != badConfig { | ||||
| 					secretData, err := yaml.Marshal(test.secretConfig) | ||||
| 					assert.NoError(t, err, test.name) | ||||
| 					secret.Data = map[string][]byte{ | ||||
| 						"cloud-config": secretData, | ||||
| 					} | ||||
| 				} | ||||
| 				if test.secretConfig == badConfig { | ||||
| 					secret.Data = map[string][]byte{"cloud-config": []byte(`unknown: "hello",unknown: "hello"`)} | ||||
| 				} | ||||
| 				_, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) | ||||
| 				assert.NoError(t, err, test.name) | ||||
| 			} | ||||
|  | ||||
| 			real, err := az.getConfigFromSecret() | ||||
| 			if test.expectErr { | ||||
| 				assert.Error(t, err, test.name) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			assert.NoError(t, err, test.name) | ||||
| 			assert.Equal(t, test.expected, real, test.name) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestInitializeCloudFromSecret(t *testing.T) { | ||||
| 	emptyConfig := &Config{} | ||||
| 	unknownConfigTypeConfig := getTestConfig() | ||||
| 	unknownConfigTypeConfig.CloudConfigType = "UnknownConfigType" | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		existingConfig *Config | ||||
| 		secretConfig   *Config | ||||
| 		expected       *Config | ||||
| 		expectErr      bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "Azure config shouldn't be override when cloud config type is file", | ||||
| 			existingConfig: &Config{ | ||||
| 				ResourceGroup:   "ResourceGroup1", | ||||
| 				CloudConfigType: cloudConfigTypeFile, | ||||
| 			}, | ||||
| 			secretConfig: getTestConfig(), | ||||
| 			expected:     nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Azure config shouldn't be override when cloud config type is unknown", | ||||
| 			existingConfig: &Config{ | ||||
| 				ResourceGroup:   "ResourceGroup1", | ||||
| 				CloudConfigType: "UnknownConfigType", | ||||
| 			}, | ||||
| 			secretConfig: unknownConfigTypeConfig, | ||||
| 			expected:     nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Azure config should be override when cloud config type is secret", | ||||
| 			existingConfig: getTestCloudConfigTypeSecretConfig(), | ||||
| 			secretConfig:   getTestConfig(), | ||||
| 			expected:       getTestConfig(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Azure config should be override when cloud config type is merge", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			secretConfig:   getTestConfig(), | ||||
| 			expected:       getTestCloudConfigTypeMergeConfigExpected(), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Error should be reported when secret doesn't exists", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Error should be reported when secret exists but cloud-config data is not provided", | ||||
| 			existingConfig: getTestCloudConfigTypeMergeConfig(), | ||||
| 			secretConfig:   emptyConfig, | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			az := &Cloud{ | ||||
| 				KubeClient: fakeclient.NewSimpleClientset(), | ||||
| 			} | ||||
| 			if test.existingConfig != nil { | ||||
| 				az.Config = *test.existingConfig | ||||
| 			} | ||||
| 			if test.secretConfig != nil { | ||||
| 				secret := &v1.Secret{ | ||||
| 					Type: v1.SecretTypeOpaque, | ||||
| 					ObjectMeta: metav1.ObjectMeta{ | ||||
| 						Name:      "azure-cloud-provider", | ||||
| 						Namespace: "kube-system", | ||||
| 					}, | ||||
| 				} | ||||
| 				if test.secretConfig != emptyConfig { | ||||
| 					secretData, err := yaml.Marshal(test.secretConfig) | ||||
| 					assert.NoError(t, err, test.name) | ||||
| 					secret.Data = map[string][]byte{ | ||||
| 						"cloud-config": secretData, | ||||
| 					} | ||||
| 				} | ||||
| 				_, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) | ||||
| 				assert.NoError(t, err, test.name) | ||||
| 			} | ||||
|  | ||||
| 			az.InitializeCloudFromSecret() | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -1,460 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	kwait "k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	volerr "k8s.io/cloud-provider/volume/errors" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// for limits check https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#storage-limits | ||||
| 	maxStorageAccounts                     = 100 // max # is 200 (250 with special request). this allows 100 for everything else including stand alone disks | ||||
| 	maxDisksPerStorageAccounts             = 60 | ||||
| 	storageAccountUtilizationBeforeGrowing = 0.5 | ||||
| 	// Disk Caching is not supported for disks 4 TiB and larger | ||||
| 	// https://docs.microsoft.com/en-us/azure/virtual-machines/premium-storage-performance#disk-caching | ||||
| 	diskCachingLimit = 4096 // GiB | ||||
|  | ||||
| 	maxLUN               = 64 // max number of LUNs per VM | ||||
| 	errLeaseIDMissing    = "LeaseIdMissing" | ||||
| 	errContainerNotFound = "ContainerNotFound" | ||||
| 	errStatusCode400     = "statuscode=400" | ||||
| 	errInvalidParameter  = `code="invalidparameter"` | ||||
| 	errTargetInstanceIds = `target="instanceids"` | ||||
| 	sourceSnapshot       = "snapshot" | ||||
| 	sourceVolume         = "volume" | ||||
|  | ||||
| 	// WriteAcceleratorEnabled support for Azure Write Accelerator on Azure Disks | ||||
| 	// https://docs.microsoft.com/azure/virtual-machines/windows/how-to-enable-write-accelerator | ||||
| 	WriteAcceleratorEnabled = "writeacceleratorenabled" | ||||
|  | ||||
| 	// see https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#create-a-managed-disk-by-copying-a-snapshot. | ||||
| 	diskSnapshotPath = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/snapshots/%s" | ||||
|  | ||||
| 	// see https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#create-a-managed-disk-from-an-existing-managed-disk-in-the-same-or-different-subscription. | ||||
| 	managedDiskPath = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s" | ||||
| ) | ||||
|  | ||||
| var defaultBackOff = kwait.Backoff{ | ||||
| 	Steps:    20, | ||||
| 	Duration: 2 * time.Second, | ||||
| 	Factor:   1.5, | ||||
| 	Jitter:   0.0, | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	managedDiskPathRE  = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/disks/(.+)`) | ||||
| 	diskSnapshotPathRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/snapshots/(.+)`) | ||||
| ) | ||||
|  | ||||
| type controllerCommon struct { | ||||
| 	subscriptionID        string | ||||
| 	location              string | ||||
| 	storageEndpointSuffix string | ||||
| 	resourceGroup         string | ||||
| 	// store disk URI when disk is in attaching or detaching process | ||||
| 	diskAttachDetachMap sync.Map | ||||
| 	// vm disk map used to lock per vm update calls | ||||
| 	vmLockMap *lockMap | ||||
| 	cloud     *Cloud | ||||
| } | ||||
|  | ||||
| // getNodeVMSet gets the VMSet interface based on config.VMType and the real virtual machine type. | ||||
| func (c *controllerCommon) getNodeVMSet(nodeName types.NodeName, crt azcache.AzureCacheReadType) (VMSet, error) { | ||||
| 	// 1. vmType is standard, return cloud.VMSet directly. | ||||
| 	if c.cloud.VMType == vmTypeStandard { | ||||
| 		return c.cloud.VMSet, nil | ||||
| 	} | ||||
|  | ||||
| 	// 2. vmType is Virtual Machine Scale Set (vmss), convert vmSet to scaleSet. | ||||
| 	ss, ok := c.cloud.VMSet.(*scaleSet) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("error of converting vmSet (%q) to scaleSet with vmType %q", c.cloud.VMSet, c.cloud.VMType) | ||||
| 	} | ||||
|  | ||||
| 	// 3. If the node is managed by availability set, then return ss.availabilitySet. | ||||
| 	managedByAS, err := ss.isNodeManagedByAvailabilitySet(mapNodeNameToVMName(nodeName), crt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if managedByAS { | ||||
| 		// vm is managed by availability set. | ||||
| 		return ss.availabilitySet, nil | ||||
| 	} | ||||
|  | ||||
| 	// 4. Node is managed by vmss | ||||
| 	return ss, nil | ||||
| } | ||||
|  | ||||
| // AttachDisk attaches a vhd to vm. The vhd must exist, can be identified by diskName, diskURI. | ||||
| // return (lun, error) | ||||
| func (c *controllerCommon) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, cachingMode compute.CachingTypes) (int32, error) { | ||||
| 	diskEncryptionSetID := "" | ||||
| 	writeAcceleratorEnabled := false | ||||
|  | ||||
| 	vmset, err := c.getNodeVMSet(nodeName, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		return -1, err | ||||
| 	} | ||||
|  | ||||
| 	if isManagedDisk { | ||||
| 		diskName := path.Base(diskURI) | ||||
| 		resourceGroup, err := getResourceGroupFromDiskURI(diskURI) | ||||
| 		if err != nil { | ||||
| 			return -1, err | ||||
| 		} | ||||
|  | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
|  | ||||
| 		disk, rerr := c.cloud.DisksClient.Get(ctx, resourceGroup, diskName) | ||||
| 		if rerr != nil { | ||||
| 			return -1, rerr.Error() | ||||
| 		} | ||||
|  | ||||
| 		if disk.ManagedBy != nil && (disk.MaxShares == nil || *disk.MaxShares <= 1) { | ||||
| 			attachErr := fmt.Sprintf( | ||||
| 				"disk(%s) already attached to node(%s), could not be attached to node(%s)", | ||||
| 				diskURI, *disk.ManagedBy, nodeName) | ||||
| 			attachedNode, err := vmset.GetNodeNameByProviderID(*disk.ManagedBy) | ||||
| 			if err != nil { | ||||
| 				return -1, err | ||||
| 			} | ||||
| 			klog.V(2).Infof("found dangling volume %s attached to node %s", diskURI, attachedNode) | ||||
| 			danglingErr := volerr.NewDanglingError(attachErr, attachedNode, "") | ||||
| 			return -1, danglingErr | ||||
| 		} | ||||
|  | ||||
| 		if disk.DiskProperties != nil { | ||||
| 			if disk.DiskProperties.DiskSizeGB != nil && *disk.DiskProperties.DiskSizeGB >= diskCachingLimit && cachingMode != compute.CachingTypesNone { | ||||
| 				// Disk Caching is not supported for disks 4 TiB and larger | ||||
| 				// https://docs.microsoft.com/en-us/azure/virtual-machines/premium-storage-performance#disk-caching | ||||
| 				cachingMode = compute.CachingTypesNone | ||||
| 				klog.Warningf("size of disk(%s) is %dGB which is bigger than limit(%dGB), set cacheMode as None", | ||||
| 					diskURI, *disk.DiskProperties.DiskSizeGB, diskCachingLimit) | ||||
| 			} | ||||
|  | ||||
| 			if disk.DiskProperties.Encryption != nil && | ||||
| 				disk.DiskProperties.Encryption.DiskEncryptionSetID != nil { | ||||
| 				diskEncryptionSetID = *disk.DiskProperties.Encryption.DiskEncryptionSetID | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if v, ok := disk.Tags[WriteAcceleratorEnabled]; ok { | ||||
| 			if v != nil && strings.EqualFold(*v, "true") { | ||||
| 				writeAcceleratorEnabled = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	instanceid, err := c.cloud.InstanceID(context.TODO(), nodeName) | ||||
| 	if err != nil { | ||||
| 		klog.Warningf("failed to get azure instance id (%v) for node %s", err, nodeName) | ||||
| 		return -1, fmt.Errorf("failed to get azure instance id for node %q (%v)", nodeName, err) | ||||
| 	} | ||||
|  | ||||
| 	c.vmLockMap.LockEntry(strings.ToLower(string(nodeName))) | ||||
| 	defer c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName))) | ||||
|  | ||||
| 	lun, err := c.GetNextDiskLun(nodeName) | ||||
| 	if err != nil { | ||||
| 		klog.Warningf("no LUN available for instance %q (%v)", nodeName, err) | ||||
| 		return -1, fmt.Errorf("all LUNs are used, cannot attach volume (%s, %s) to instance %q (%v)", diskName, diskURI, instanceid, err) | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("Trying to attach volume %q lun %d to node %q.", diskURI, lun, nodeName) | ||||
| 	c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "attaching") | ||||
| 	defer c.diskAttachDetachMap.Delete(strings.ToLower(diskURI)) | ||||
| 	return lun, vmset.AttachDisk(isManagedDisk, diskName, diskURI, nodeName, lun, cachingMode, diskEncryptionSetID, writeAcceleratorEnabled) | ||||
| } | ||||
|  | ||||
| // DetachDisk detaches a disk from host. The vhd can be identified by diskName or diskURI. | ||||
| func (c *controllerCommon) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error { | ||||
| 	_, err := c.cloud.InstanceID(context.TODO(), nodeName) | ||||
| 	if err != nil { | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			// if host doesn't exist, no need to detach | ||||
| 			klog.Warningf("azureDisk - failed to get azure instance id(%q), DetachDisk(%s) will assume disk is already detached", | ||||
| 				nodeName, diskURI) | ||||
| 			return nil | ||||
| 		} | ||||
| 		klog.Warningf("failed to get azure instance id (%v)", err) | ||||
| 		return fmt.Errorf("failed to get azure instance id for node %q (%v)", nodeName, err) | ||||
| 	} | ||||
|  | ||||
| 	vmset, err := c.getNodeVMSet(nodeName, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("detach %v from node %q", diskURI, nodeName) | ||||
|  | ||||
| 	// make the lock here as small as possible | ||||
| 	c.vmLockMap.LockEntry(strings.ToLower(string(nodeName))) | ||||
| 	c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "detaching") | ||||
| 	err = vmset.DetachDisk(diskName, diskURI, nodeName) | ||||
| 	c.diskAttachDetachMap.Delete(strings.ToLower(diskURI)) | ||||
| 	c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName))) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		if isInstanceNotFoundError(err) { | ||||
| 			// if host doesn't exist, no need to detach | ||||
| 			klog.Warningf("azureDisk - got InstanceNotFoundError(%v), DetachDisk(%s) will assume disk is already detached", | ||||
| 				err, diskURI) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if retry.IsErrorRetriable(err) && c.cloud.CloudProviderBackoff { | ||||
| 			klog.Warningf("azureDisk - update backing off: detach disk(%s, %s), err: %v", diskName, diskURI, err) | ||||
| 			retryErr := kwait.ExponentialBackoff(c.cloud.RequestBackoff(), func() (bool, error) { | ||||
| 				c.vmLockMap.LockEntry(strings.ToLower(string(nodeName))) | ||||
| 				c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "detaching") | ||||
| 				err := vmset.DetachDisk(diskName, diskURI, nodeName) | ||||
| 				c.diskAttachDetachMap.Delete(strings.ToLower(diskURI)) | ||||
| 				c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName))) | ||||
|  | ||||
| 				retriable := false | ||||
| 				if err != nil && retry.IsErrorRetriable(err) { | ||||
| 					retriable = true | ||||
| 				} | ||||
| 				return !retriable, err | ||||
| 			}) | ||||
| 			if retryErr != nil { | ||||
| 				err = retryErr | ||||
| 				klog.V(2).Infof("azureDisk - update abort backoff: detach disk(%s, %s), err: %v", diskName, diskURI, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("azureDisk - detach disk(%s, %s) failed, err: %v", diskName, diskURI, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - detach disk(%s, %s) succeeded", diskName, diskURI) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // getNodeDataDisks invokes vmSet interfaces to get data disks for the node. | ||||
| func (c *controllerCommon) getNodeDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) { | ||||
| 	vmset, err := c.getNodeVMSet(nodeName, crt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return vmset.GetDataDisks(nodeName, crt) | ||||
| } | ||||
|  | ||||
| // GetDiskLun finds the lun on the host that the vhd is attached to, given a vhd's diskName and diskURI. | ||||
| func (c *controllerCommon) GetDiskLun(diskName, diskURI string, nodeName types.NodeName) (int32, error) { | ||||
| 	// getNodeDataDisks need to fetch the cached data/fresh data if cache expired here | ||||
| 	// to ensure we get LUN based on latest entry. | ||||
| 	disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("error of getting data disks for node %q: %v", nodeName, err) | ||||
| 		return -1, err | ||||
| 	} | ||||
|  | ||||
| 	for _, disk := range disks { | ||||
| 		if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) || | ||||
| 			(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) || | ||||
| 			(disk.ManagedDisk != nil && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) { | ||||
| 			if disk.ToBeDetached != nil && *disk.ToBeDetached { | ||||
| 				klog.Warningf("azureDisk - find disk(ToBeDetached): lun %d name %q uri %q", *disk.Lun, diskName, diskURI) | ||||
| 			} else { | ||||
| 				// found the disk | ||||
| 				klog.V(2).Infof("azureDisk - find disk: lun %d name %q uri %q", *disk.Lun, diskName, diskURI) | ||||
| 				return *disk.Lun, nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return -1, fmt.Errorf("cannot find Lun for disk %s", diskName) | ||||
| } | ||||
|  | ||||
| // GetNextDiskLun searches all vhd attachment on the host and find unused lun. Return -1 if all luns are used. | ||||
| func (c *controllerCommon) GetNextDiskLun(nodeName types.NodeName) (int32, error) { | ||||
| 	disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("error of getting data disks for node %q: %v", nodeName, err) | ||||
| 		return -1, err | ||||
| 	} | ||||
|  | ||||
| 	used := make([]bool, maxLUN) | ||||
| 	for _, disk := range disks { | ||||
| 		if disk.Lun != nil { | ||||
| 			used[*disk.Lun] = true | ||||
| 		} | ||||
| 	} | ||||
| 	for k, v := range used { | ||||
| 		if !v { | ||||
| 			return int32(k), nil | ||||
| 		} | ||||
| 	} | ||||
| 	return -1, fmt.Errorf("all luns are used") | ||||
| } | ||||
|  | ||||
| // DisksAreAttached checks if a list of volumes are attached to the node with the specified NodeName. | ||||
| func (c *controllerCommon) DisksAreAttached(diskNames []string, nodeName types.NodeName) (map[string]bool, error) { | ||||
| 	attached := make(map[string]bool) | ||||
| 	for _, diskName := range diskNames { | ||||
| 		attached[diskName] = false | ||||
| 	} | ||||
|  | ||||
| 	// doing stalled read for getNodeDataDisks to ensure we don't call ARM | ||||
| 	// for every reconcile call. The cache is invalidated after Attach/Detach | ||||
| 	// disk. So the new entry will be fetched and cached the first time reconcile | ||||
| 	// loop runs after the Attach/Disk OP which will reflect the latest model. | ||||
| 	disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			// if host doesn't exist, no need to detach | ||||
| 			klog.Warningf("azureDisk - Cannot find node %q, DisksAreAttached will assume disks %v are not attached to it.", | ||||
| 				nodeName, diskNames) | ||||
| 			return attached, nil | ||||
| 		} | ||||
|  | ||||
| 		return attached, err | ||||
| 	} | ||||
|  | ||||
| 	for _, disk := range disks { | ||||
| 		for _, diskName := range diskNames { | ||||
| 			if disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName) { | ||||
| 				attached[diskName] = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return attached, nil | ||||
| } | ||||
|  | ||||
| func filterDetachingDisks(unfilteredDisks []compute.DataDisk) []compute.DataDisk { | ||||
| 	filteredDisks := []compute.DataDisk{} | ||||
| 	for _, disk := range unfilteredDisks { | ||||
| 		if disk.ToBeDetached != nil && *disk.ToBeDetached { | ||||
| 			if disk.Name != nil { | ||||
| 				klog.V(2).Infof("Filtering disk: %s with ToBeDetached flag set.", *disk.Name) | ||||
| 			} | ||||
| 		} else { | ||||
| 			filteredDisks = append(filteredDisks, disk) | ||||
| 		} | ||||
| 	} | ||||
| 	return filteredDisks | ||||
| } | ||||
|  | ||||
| func (c *controllerCommon) filterNonExistingDisks(ctx context.Context, unfilteredDisks []compute.DataDisk) []compute.DataDisk { | ||||
| 	filteredDisks := []compute.DataDisk{} | ||||
| 	for _, disk := range unfilteredDisks { | ||||
| 		filter := false | ||||
| 		if disk.ManagedDisk != nil && disk.ManagedDisk.ID != nil { | ||||
| 			diskURI := *disk.ManagedDisk.ID | ||||
| 			exist, err := c.cloud.checkDiskExists(ctx, diskURI) | ||||
| 			if err != nil { | ||||
| 				klog.Errorf("checkDiskExists(%s) failed with error: %v", diskURI, err) | ||||
| 			} else { | ||||
| 				// only filter disk when checkDiskExists returns <false, nil> | ||||
| 				filter = !exist | ||||
| 				if filter { | ||||
| 					klog.Errorf("disk(%s) does not exist, removed from data disk list", diskURI) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !filter { | ||||
| 			filteredDisks = append(filteredDisks, disk) | ||||
| 		} | ||||
| 	} | ||||
| 	return filteredDisks | ||||
| } | ||||
|  | ||||
| func (c *controllerCommon) checkDiskExists(ctx context.Context, diskURI string) (bool, error) { | ||||
| 	diskName := path.Base(diskURI) | ||||
| 	resourceGroup, err := getResourceGroupFromDiskURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	if _, rerr := c.cloud.DisksClient.Get(ctx, resourceGroup, diskName); rerr != nil { | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		return false, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| func getValidCreationData(subscriptionID, resourceGroup, sourceResourceID, sourceType string) (compute.CreationData, error) { | ||||
| 	if sourceResourceID == "" { | ||||
| 		return compute.CreationData{ | ||||
| 			CreateOption: compute.Empty, | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	switch sourceType { | ||||
| 	case sourceSnapshot: | ||||
| 		if match := diskSnapshotPathRE.FindString(sourceResourceID); match == "" { | ||||
| 			sourceResourceID = fmt.Sprintf(diskSnapshotPath, subscriptionID, resourceGroup, sourceResourceID) | ||||
| 		} | ||||
|  | ||||
| 	case sourceVolume: | ||||
| 		if match := managedDiskPathRE.FindString(sourceResourceID); match == "" { | ||||
| 			sourceResourceID = fmt.Sprintf(managedDiskPath, subscriptionID, resourceGroup, sourceResourceID) | ||||
| 		} | ||||
| 	default: | ||||
| 		return compute.CreationData{ | ||||
| 			CreateOption: compute.Empty, | ||||
| 		}, nil | ||||
| 	} | ||||
|  | ||||
| 	splits := strings.Split(sourceResourceID, "/") | ||||
| 	if len(splits) > 9 { | ||||
| 		if sourceType == sourceSnapshot { | ||||
| 			return compute.CreationData{}, fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", sourceResourceID, diskSnapshotPathRE) | ||||
| 		} | ||||
| 		return compute.CreationData{}, fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", sourceResourceID, managedDiskPathRE) | ||||
| 	} | ||||
| 	return compute.CreationData{ | ||||
| 		CreateOption:     compute.Copy, | ||||
| 		SourceResourceID: &sourceResourceID, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func isInstanceNotFoundError(err error) bool { | ||||
| 	errMsg := strings.ToLower(err.Error()) | ||||
| 	if strings.Contains(errMsg, strings.ToLower(vmssVMNotActiveErrorMessage)) { | ||||
| 		return true | ||||
| 	} | ||||
| 	return strings.Contains(errMsg, errStatusCode400) && strings.Contains(errMsg, errInvalidParameter) && strings.Contains(errMsg, errTargetInstanceIds) | ||||
| } | ||||
| @@ -1,824 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestCommonAttachDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	maxShare := int32(1) | ||||
| 	goodInstanceID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/%s", "vm1") | ||||
| 	diskEncryptionSetID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/%s", "diskEncryptionSet-name") | ||||
| 	testTags := make(map[string]*string) | ||||
| 	testTags[WriteAcceleratorEnabled] = pointer.String("true") | ||||
| 	testCases := []struct { | ||||
| 		desc            string | ||||
| 		vmList          map[string]string | ||||
| 		nodeName        types.NodeName | ||||
| 		isDataDisksFull bool | ||||
| 		isBadDiskURI    bool | ||||
| 		isDiskUsed      bool | ||||
| 		existedDisk     compute.Disk | ||||
| 		expectedLun     int32 | ||||
| 		expectedErr     bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:        "LUN -1 and error shall be returned if there's no such instance corresponding to given nodeName", | ||||
| 			nodeName:    "vm1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedLun: -1, | ||||
| 			expectedErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:            "LUN -1 and error shall be returned if there's no available LUN for instance", | ||||
| 			vmList:          map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:        "vm1", | ||||
| 			isDataDisksFull: true, | ||||
| 			existedDisk:     compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedLun:     -1, | ||||
| 			expectedErr:     true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "correct LUN and no error shall be returned if everything is good", | ||||
| 			vmList:   map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName: "vm1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name"), | ||||
| 				DiskProperties: &compute.DiskProperties{ | ||||
| 					Encryption: &compute.Encryption{DiskEncryptionSetID: &diskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, | ||||
| 					DiskSizeGB: pointer.Int32(4096), | ||||
| 				}, | ||||
| 				Tags: testTags}, | ||||
| 			expectedLun: 1, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:         "an error shall be returned if there's invalid disk uri", | ||||
| 			vmList:       map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:     "vm1", | ||||
| 			isBadDiskURI: true, | ||||
| 			existedDisk:  compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedLun:  -1, | ||||
| 			expectedErr:  true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "an error shall be returned if attach an already attached disk with good ManagedBy instance id", | ||||
| 			vmList:      map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:    "vm1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name"), ManagedBy: pointer.String(goodInstanceID), DiskProperties: &compute.DiskProperties{MaxShares: &maxShare}}, | ||||
| 			expectedLun: -1, | ||||
| 			expectedErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "an error shall be returned if attach an already attached disk with bad ManagedBy instance id", | ||||
| 			vmList:      map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:    "vm1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name"), ManagedBy: pointer.String("test"), DiskProperties: &compute.DiskProperties{MaxShares: &maxShare}}, | ||||
| 			expectedLun: -1, | ||||
| 			expectedErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "an error shall be returned if there's no matching disk", | ||||
| 			vmList:      map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:    "vm1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name-1")}, | ||||
| 			expectedLun: -1, | ||||
| 			expectedErr: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
| 		if test.isBadDiskURI { | ||||
| 			diskURI = fmt.Sprintf("/baduri/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 				testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, test.vmList, test.isDataDisksFull) | ||||
| 		if test.isDiskUsed { | ||||
| 			vm0 := setTestVirtualMachines(testCloud, map[string]string{"vm0": "PowerState/Running"}, test.isDataDisksFull)[0] | ||||
| 			expectedVMs = append(expectedVMs, vm0) | ||||
| 		} | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		if len(expectedVMs) == 0 { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "disk-name").Return(test.existedDisk, nil).AnyTimes() | ||||
| 		mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("disk-name")).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 		lun, err := common.AttachDisk(true, "", diskURI, test.nodeName, compute.CachingTypesReadOnly) | ||||
| 		assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCommonAttachDiskWithVMSS(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc            string | ||||
| 		vmList          map[string]string | ||||
| 		vmssList        []string | ||||
| 		nodeName        types.NodeName | ||||
| 		isVMSS          bool | ||||
| 		isManagedBy     bool | ||||
| 		isManagedDisk   bool | ||||
| 		isDataDisksFull bool | ||||
| 		existedDisk     compute.Disk | ||||
| 		expectedLun     int32 | ||||
| 		expectedErr     bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:          "an error shall be returned if convert vmSet to scaleSet failed", | ||||
| 			vmList:        map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:      "vm1", | ||||
| 			isVMSS:        false, | ||||
| 			isManagedBy:   false, | ||||
| 			isManagedDisk: false, | ||||
| 			existedDisk:   compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedLun:   -1, | ||||
| 			expectedErr:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "an error shall be returned if convert vmSet to scaleSet success but node is not managed by availability set", | ||||
| 			vmssList:      []string{"vmss-vm-000001"}, | ||||
| 			nodeName:      "vmss1", | ||||
| 			isVMSS:        true, | ||||
| 			isManagedBy:   false, | ||||
| 			isManagedDisk: false, | ||||
| 			existedDisk:   compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedLun:   -1, | ||||
| 			expectedErr:   true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		testCloud.VMType = vmTypeVMSS | ||||
| 		if test.isVMSS { | ||||
| 			if test.isManagedBy { | ||||
| 				testCloud.DisableAvailabilitySetNodes = false | ||||
| 				testVMSSName := "vmss" | ||||
| 				expectedVMSS := compute.VirtualMachineScaleSet{Name: pointer.String(testVMSSName)} | ||||
| 				mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface) | ||||
| 				mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes() | ||||
|  | ||||
| 				expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, testVMSSName, "", 0, test.vmssList, "", false) | ||||
| 				mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface) | ||||
| 				mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, testVMSSName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes() | ||||
|  | ||||
| 				mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 				mockVMsClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachine{}, nil).AnyTimes() | ||||
| 			} else { | ||||
| 				testCloud.DisableAvailabilitySetNodes = true | ||||
| 			} | ||||
| 			ss, err := newScaleSet(testCloud) | ||||
| 			assert.NoError(t, err) | ||||
| 			testCloud.VMSet = ss | ||||
| 		} | ||||
|  | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
| 		if !test.isVMSS { | ||||
| 			expectedVMs := setTestVirtualMachines(testCloud, test.vmList, test.isDataDisksFull) | ||||
| 			mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 			for _, vm := range expectedVMs { | ||||
| 				mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 			} | ||||
| 			if len(expectedVMs) == 0 { | ||||
| 				mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 			} | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 			mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "disk-name").Return(test.existedDisk, nil).AnyTimes() | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("disk-name")).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		lun, err := common.AttachDisk(test.isManagedDisk, "test", diskURI, test.nodeName, compute.CachingTypesReadOnly) | ||||
| 		assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCommonDetachDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc             string | ||||
| 		vmList           map[string]string | ||||
| 		nodeName         types.NodeName | ||||
| 		diskName         string | ||||
| 		isErrorRetriable bool | ||||
| 		expectedErr      bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:        "error should not be returned if there's no such instance corresponding to given nodeName", | ||||
| 			nodeName:    "vm1", | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "an error should be returned if vmset detach failed with isErrorRetriable error", | ||||
| 			vmList:           map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:         "vm1", | ||||
| 			isErrorRetriable: true, | ||||
| 			expectedErr:      true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "no error shall be returned if there's no matching disk according to given diskName", | ||||
| 			vmList:      map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:    "vm1", | ||||
| 			diskName:    "disk2", | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "no error shall be returned if the disk exists", | ||||
| 			vmList:      map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:    "vm1", | ||||
| 			diskName:    "disk1", | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/disk-name", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup) | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, test.vmList, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		if len(expectedVMs) == 0 { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} | ||||
| 		if test.isErrorRetriable { | ||||
| 			testCloud.CloudProviderBackoff = true | ||||
| 			testCloud.ResourceRequestBackoff = wait.Backoff{Steps: 1} | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusBadRequest, Retriable: true, RawError: fmt.Errorf("Retriable: true")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		err := common.DetachDisk(test.diskName, diskURI, test.nodeName) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetDiskLun(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc        string | ||||
| 		diskName    string | ||||
| 		diskURI     string | ||||
| 		expectedLun int32 | ||||
| 		expectedErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:        "LUN -1 and error shall be returned if diskName != disk.Name or diskURI != disk.Vhd.URI", | ||||
| 			diskName:    "disk2", | ||||
| 			expectedLun: -1, | ||||
| 			expectedErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "correct LUN and no error shall be returned if diskName = disk.Name", | ||||
| 			diskName:    "disk1", | ||||
| 			expectedLun: 0, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		lun, err := common.GetDiskLun(test.diskName, test.diskURI, "vm1") | ||||
| 		assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetNextDiskLun(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc            string | ||||
| 		isDataDisksFull bool | ||||
| 		expectedLun     int32 | ||||
| 		expectedErr     bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:            "the minimal LUN shall be returned if there's enough room for extra disks", | ||||
| 			isDataDisksFull: false, | ||||
| 			expectedLun:     1, | ||||
| 			expectedErr:     false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:            "LUN -1 and  error shall be returned if there's no available LUN", | ||||
| 			isDataDisksFull: true, | ||||
| 			expectedLun:     -1, | ||||
| 			expectedErr:     true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, test.isDataDisksFull) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		lun, err := common.GetNextDiskLun("vm1") | ||||
| 		assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDisksAreAttached(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc             string | ||||
| 		diskNames        []string | ||||
| 		nodeName         types.NodeName | ||||
| 		expectedAttached map[string]bool | ||||
| 		expectedErr      bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:             "an error shall be returned if there's no such instance corresponding to given nodeName", | ||||
| 			diskNames:        []string{"disk1"}, | ||||
| 			nodeName:         "vm2", | ||||
| 			expectedAttached: map[string]bool{"disk1": false}, | ||||
| 			expectedErr:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "proper attach map shall be returned if everything is good", | ||||
| 			diskNames:        []string{"disk1", "disk2"}, | ||||
| 			nodeName:         "vm1", | ||||
| 			expectedAttached: map[string]bool{"disk1": true, "disk2": false}, | ||||
| 			expectedErr:      false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		common := &controllerCommon{ | ||||
| 			location:              testCloud.Location, | ||||
| 			storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 			resourceGroup:         testCloud.ResourceGroup, | ||||
| 			subscriptionID:        testCloud.SubscriptionID, | ||||
| 			cloud:                 testCloud, | ||||
| 			vmLockMap:             newLockMap(), | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 		attached, err := common.DisksAreAttached(test.diskNames, test.nodeName) | ||||
| 		assert.Equal(t, test.expectedAttached, attached, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFilteredDetachingDisks(t *testing.T) { | ||||
|  | ||||
| 	disks := []compute.DataDisk{ | ||||
| 		{ | ||||
| 			Name:         pointer.StringPtr("DiskName1"), | ||||
| 			ToBeDetached: pointer.BoolPtr(false), | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: pointer.StringPtr("ManagedID"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:         pointer.StringPtr("DiskName2"), | ||||
| 			ToBeDetached: pointer.BoolPtr(true), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:         pointer.StringPtr("DiskName3"), | ||||
| 			ToBeDetached: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:         pointer.StringPtr("DiskName4"), | ||||
| 			ToBeDetached: nil, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	filteredDisks := filterDetachingDisks(disks) | ||||
| 	assert.Equal(t, 3, len(filteredDisks)) | ||||
| 	assert.Equal(t, "DiskName1", *filteredDisks[0].Name) | ||||
| 	assert.Equal(t, "ManagedID", *filteredDisks[0].ManagedDisk.ID) | ||||
| 	assert.Equal(t, "DiskName3", *filteredDisks[1].Name) | ||||
|  | ||||
| 	disks = []compute.DataDisk{} | ||||
| 	filteredDisks = filterDetachingDisks(disks) | ||||
| 	assert.Equal(t, 0, len(filteredDisks)) | ||||
| } | ||||
|  | ||||
| func TestGetValidCreationData(t *testing.T) { | ||||
| 	sourceResourceSnapshotID := "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx" | ||||
| 	sourceResourceVolumeID := "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/xxx" | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		subscriptionID   string | ||||
| 		resourceGroup    string | ||||
| 		sourceResourceID string | ||||
| 		sourceType       string | ||||
| 		expected1        compute.CreationData | ||||
| 		expected2        error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "", | ||||
| 			sourceType:       "", | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption: compute.Empty, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption:     compute.Copy, | ||||
| 				SourceResourceID: &sourceResourceSnapshotID, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "xxx", | ||||
| 			resourceGroup:    "xxx", | ||||
| 			sourceResourceID: "xxx", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption:     compute.Copy, | ||||
| 				SourceResourceID: &sourceResourceSnapshotID, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/23/providers/Microsoft.Compute/disks/name", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1:        compute.CreationData{}, | ||||
| 			expected2:        fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots//subscriptions/23/providers/Microsoft.Compute/disks/name", diskSnapshotPathRE), | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "http://test.com/vhds/name", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1:        compute.CreationData{}, | ||||
| 			expected2:        fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots/http://test.com/vhds/name", diskSnapshotPathRE), | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/xxx/snapshots/xxx", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1:        compute.CreationData{}, | ||||
| 			expected2:        fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots//subscriptions/xxx/snapshots/xxx", diskSnapshotPathRE), | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx/snapshots/xxx/snapshots/xxx", | ||||
| 			sourceType:       sourceSnapshot, | ||||
| 			expected1:        compute.CreationData{}, | ||||
| 			expected2:        fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx/snapshots/xxx/snapshots/xxx", diskSnapshotPathRE), | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "xxx", | ||||
| 			sourceType:       "", | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption: compute.Empty, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/xxx", | ||||
| 			sourceType:       sourceVolume, | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption:     compute.Copy, | ||||
| 				SourceResourceID: &sourceResourceVolumeID, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "xxx", | ||||
| 			resourceGroup:    "xxx", | ||||
| 			sourceResourceID: "xxx", | ||||
| 			sourceType:       sourceVolume, | ||||
| 			expected1: compute.CreationData{ | ||||
| 				CreateOption:     compute.Copy, | ||||
| 				SourceResourceID: &sourceResourceVolumeID, | ||||
| 			}, | ||||
| 			expected2: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			subscriptionID:   "", | ||||
| 			resourceGroup:    "", | ||||
| 			sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx", | ||||
| 			sourceType:       sourceVolume, | ||||
| 			expected1:        compute.CreationData{}, | ||||
| 			expected2:        fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/disks//subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx", managedDiskPathRE), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		result, err := getValidCreationData(test.subscriptionID, test.resourceGroup, test.sourceResourceID, test.sourceType) | ||||
| 		if !reflect.DeepEqual(result, test.expected1) || !reflect.DeepEqual(err, test.expected2) { | ||||
| 			t.Errorf("input sourceResourceID: %v, sourceType: %v, getValidCreationData result: %v, expected1 : %v, err: %v, expected2: %v", test.sourceResourceID, test.sourceType, result, test.expected1, err, test.expected2) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCheckDiskExists(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	testCloud := GetTestCloud(ctrl) | ||||
| 	common := &controllerCommon{ | ||||
| 		location:              testCloud.Location, | ||||
| 		storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 		resourceGroup:         testCloud.ResourceGroup, | ||||
| 		subscriptionID:        testCloud.SubscriptionID, | ||||
| 		cloud:                 testCloud, | ||||
| 		vmLockMap:             newLockMap(), | ||||
| 	} | ||||
| 	// create a new disk before running test | ||||
| 	newDiskName := "newdisk" | ||||
| 	newDiskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 		testCloud.SubscriptionID, testCloud.ResourceGroup, newDiskName) | ||||
|  | ||||
| 	mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 	mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, newDiskName).Return(compute.Disk{}, nil).AnyTimes() | ||||
| 	mockDisksClient.EXPECT().Get(gomock.Any(), gomock.Not(testCloud.ResourceGroup), gomock.Any()).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		diskURI        string | ||||
| 		expectedResult bool | ||||
| 		expectedErr    bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			diskURI:        "incorrect disk URI format", | ||||
| 			expectedResult: false, | ||||
| 			expectedErr:    true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			diskURI:        "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/non-existing-disk", | ||||
| 			expectedResult: false, | ||||
| 			expectedErr:    false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			diskURI:        newDiskURI, | ||||
| 			expectedResult: true, | ||||
| 			expectedErr:    false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		exist, err := common.checkDiskExists(ctx, test.diskURI) | ||||
| 		assert.Equal(t, test.expectedResult, exist, "TestCase[%d]", i, exist) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d], return error: %v", i, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFilterNonExistingDisks(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	testCloud := GetTestCloud(ctrl) | ||||
| 	common := &controllerCommon{ | ||||
| 		location:              testCloud.Location, | ||||
| 		storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 		resourceGroup:         testCloud.ResourceGroup, | ||||
| 		subscriptionID:        testCloud.SubscriptionID, | ||||
| 		cloud:                 testCloud, | ||||
| 		vmLockMap:             newLockMap(), | ||||
| 	} | ||||
| 	// create a new disk before running test | ||||
| 	diskURIPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/", | ||||
| 		testCloud.SubscriptionID, testCloud.ResourceGroup) | ||||
| 	newDiskName := "newdisk" | ||||
| 	newDiskURI := diskURIPrefix + newDiskName | ||||
|  | ||||
| 	mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 	mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, newDiskName).Return(compute.Disk{}, nil).AnyTimes() | ||||
| 	mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not(newDiskName)).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 	disks := []compute.DataDisk{ | ||||
| 		{ | ||||
| 			Name: &newDiskName, | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: &newDiskURI, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.StringPtr("DiskName2"), | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: pointer.StringPtr(diskURIPrefix + "DiskName2"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.StringPtr("DiskName3"), | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: pointer.StringPtr(diskURIPrefix + "DiskName3"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: pointer.StringPtr("DiskName4"), | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: pointer.StringPtr(diskURIPrefix + "DiskName4"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	filteredDisks := common.filterNonExistingDisks(ctx, disks) | ||||
| 	assert.Equal(t, 1, len(filteredDisks)) | ||||
| 	assert.Equal(t, newDiskName, *filteredDisks[0].Name) | ||||
|  | ||||
| 	disks = []compute.DataDisk{} | ||||
| 	filteredDisks = filterDetachingDisks(disks) | ||||
| 	assert.Equal(t, 0, len(filteredDisks)) | ||||
| } | ||||
|  | ||||
| func TestFilterNonExistingDisksWithSpecialHTTPStatusCode(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	testCloud := GetTestCloud(ctrl) | ||||
| 	common := &controllerCommon{ | ||||
| 		location:              testCloud.Location, | ||||
| 		storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix, | ||||
| 		resourceGroup:         testCloud.ResourceGroup, | ||||
| 		subscriptionID:        testCloud.SubscriptionID, | ||||
| 		cloud:                 testCloud, | ||||
| 		vmLockMap:             newLockMap(), | ||||
| 	} | ||||
| 	// create a new disk before running test | ||||
| 	diskURIPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/", | ||||
| 		testCloud.SubscriptionID, testCloud.ResourceGroup) | ||||
| 	newDiskName := "specialdisk" | ||||
| 	newDiskURI := diskURIPrefix + newDiskName | ||||
|  | ||||
| 	mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 	mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Eq(newDiskName)).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusBadRequest, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 	disks := []compute.DataDisk{ | ||||
| 		{ | ||||
| 			Name: &newDiskName, | ||||
| 			ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 				ID: &newDiskURI, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	filteredDisks := common.filterNonExistingDisks(ctx, disks) | ||||
| 	assert.Equal(t, 1, len(filteredDisks)) | ||||
| 	assert.Equal(t, newDiskName, *filteredDisks[0].Name) | ||||
| } | ||||
|  | ||||
| func TestIsInstanceNotFoundError(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		errMsg         string | ||||
| 		expectedResult bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			errMsg:         "", | ||||
| 			expectedResult: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			errMsg:         "other error", | ||||
| 			expectedResult: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			errMsg:         "The provided instanceId 857 is not an active Virtual Machine Scale Set VM instanceId.", | ||||
| 			expectedResult: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			errMsg:         `compute.VirtualMachineScaleSetVMsClient#Update: Failure sending request: StatusCode=400 -- Original Error: Code="InvalidParameter" Message="The provided instanceId 1181 is not an active Virtual Machine Scale Set VM instanceId." Target="instanceIds"`, | ||||
| 			expectedResult: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		result := isInstanceNotFoundError(fmt.Errorf(test.errMsg)) | ||||
| 		assert.Equal(t, test.expectedResult, result, "TestCase[%d]", i, result) | ||||
| 	} | ||||
| } | ||||
| @@ -1,204 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // AttachDisk attaches a vhd to vm | ||||
| // the vhd must exist, can be identified by diskName, diskURI, and lun. | ||||
| func (as *availabilitySet) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error { | ||||
| 	vm, err := as.getVirtualMachine(nodeName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	vmName := mapNodeNameToVMName(nodeName) | ||||
| 	nodeResourceGroup, err := as.GetNodeResourceGroup(vmName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	disks := make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks)) | ||||
| 	copy(disks, *vm.StorageProfile.DataDisks) | ||||
|  | ||||
| 	if isManagedDisk { | ||||
| 		managedDisk := &compute.ManagedDiskParameters{ID: &diskURI} | ||||
| 		if diskEncryptionSetID == "" { | ||||
| 			if vm.StorageProfile.OsDisk != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID != nil { | ||||
| 				// set diskEncryptionSet as value of os disk by default | ||||
| 				diskEncryptionSetID = *vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID | ||||
| 			} | ||||
| 		} | ||||
| 		if diskEncryptionSetID != "" { | ||||
| 			managedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: &diskEncryptionSetID} | ||||
| 		} | ||||
| 		disks = append(disks, | ||||
| 			compute.DataDisk{ | ||||
| 				Name:                    &diskName, | ||||
| 				Lun:                     &lun, | ||||
| 				Caching:                 cachingMode, | ||||
| 				CreateOption:            "attach", | ||||
| 				ManagedDisk:             managedDisk, | ||||
| 				WriteAcceleratorEnabled: pointer.Bool(writeAcceleratorEnabled), | ||||
| 			}) | ||||
| 	} else { | ||||
| 		disks = append(disks, | ||||
| 			compute.DataDisk{ | ||||
| 				Name: &diskName, | ||||
| 				Vhd: &compute.VirtualHardDisk{ | ||||
| 					URI: &diskURI, | ||||
| 				}, | ||||
| 				Lun:          &lun, | ||||
| 				Caching:      cachingMode, | ||||
| 				CreateOption: "attach", | ||||
| 			}) | ||||
| 	} | ||||
|  | ||||
| 	newVM := compute.VirtualMachineUpdate{ | ||||
| 		VirtualMachineProperties: &compute.VirtualMachineProperties{ | ||||
| 			StorageProfile: &compute.StorageProfile{ | ||||
| 				DataDisks: &disks, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) with DiskEncryptionSetID(%s)", nodeResourceGroup, vmName, diskName, diskURI, diskEncryptionSetID) | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Invalidate the cache right after updating | ||||
| 	defer as.cloud.vmCache.Delete(vmName) | ||||
|  | ||||
| 	rerr := as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "attach_disk") | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("azureDisk - attach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, vmName, rerr) | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, vmName) | ||||
| 			disks := as.filterNonExistingDisks(ctx, *newVM.VirtualMachineProperties.StorageProfile.DataDisks) | ||||
| 			newVM.VirtualMachineProperties.StorageProfile.DataDisks = &disks | ||||
| 			rerr = as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "attach_disk") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) returned with %v", nodeResourceGroup, vmName, diskName, diskURI, rerr) | ||||
| 	if rerr != nil { | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DetachDisk detaches a disk from host | ||||
| // the vhd can be identified by diskName or diskURI | ||||
| func (as *availabilitySet) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error { | ||||
| 	vm, err := as.getVirtualMachine(nodeName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		// if host doesn't exist, no need to detach | ||||
| 		klog.Warningf("azureDisk - cannot find node %s, skip detaching disk(%s, %s)", nodeName, diskName, diskURI) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	vmName := mapNodeNameToVMName(nodeName) | ||||
| 	nodeResourceGroup, err := as.GetNodeResourceGroup(vmName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	disks := make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks)) | ||||
| 	copy(disks, *vm.StorageProfile.DataDisks) | ||||
|  | ||||
| 	bFoundDisk := false | ||||
| 	for i, disk := range disks { | ||||
| 		if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) || | ||||
| 			(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) || | ||||
| 			(disk.ManagedDisk != nil && diskURI != "" && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) { | ||||
| 			// found the disk | ||||
| 			klog.V(2).Infof("azureDisk - detach disk: name %q uri %q", diskName, diskURI) | ||||
| 			if strings.EqualFold(as.cloud.Environment.Name, AzureStackCloudName) { | ||||
| 				disks = append(disks[:i], disks[i+1:]...) | ||||
| 			} else { | ||||
| 				disks[i].ToBeDetached = pointer.Bool(true) | ||||
| 			} | ||||
| 			bFoundDisk = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !bFoundDisk { | ||||
| 		// only log here, next action is to update VM status with original meta data | ||||
| 		klog.Errorf("detach azure disk: disk %s not found, diskURI: %s", diskName, diskURI) | ||||
| 	} | ||||
|  | ||||
| 	newVM := compute.VirtualMachineUpdate{ | ||||
| 		VirtualMachineProperties: &compute.VirtualMachineProperties{ | ||||
| 			StorageProfile: &compute.StorageProfile{ | ||||
| 				DataDisks: &disks, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s)", nodeResourceGroup, vmName, diskName, diskURI) | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Invalidate the cache right after updating | ||||
| 	defer as.cloud.vmCache.Delete(vmName) | ||||
|  | ||||
| 	rerr := as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "detach_disk") | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("azureDisk - detach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, vmName, rerr) | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, vmName) | ||||
| 			disks := as.filterNonExistingDisks(ctx, *vm.StorageProfile.DataDisks) | ||||
| 			newVM.VirtualMachineProperties.StorageProfile.DataDisks = &disks | ||||
| 			rerr = as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "detach_disk") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s) returned with %v", nodeResourceGroup, vmName, diskName, diskURI, rerr) | ||||
| 	if rerr != nil { | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetDataDisks gets a list of data disks attached to the node. | ||||
| func (as *availabilitySet) GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) { | ||||
| 	vm, err := as.getVirtualMachine(nodeName, crt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if vm.StorageProfile.DataDisks == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return *vm.StorageProfile.DataDisks, nil | ||||
| } | ||||
| @@ -1,251 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	fakeCacheTTL = 2 * time.Second | ||||
| ) | ||||
|  | ||||
| func TestStandardAttachDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc          string | ||||
| 		nodeName      types.NodeName | ||||
| 		isManagedDisk bool | ||||
| 		isAttachFail  bool | ||||
| 		expectedErr   bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:          "an error shall be returned if there's no corresponding vms", | ||||
| 			nodeName:      "vm2", | ||||
| 			isManagedDisk: true, | ||||
| 			expectedErr:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if everything's good", | ||||
| 			nodeName:      "vm1", | ||||
| 			isManagedDisk: true, | ||||
| 			expectedErr:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if everything's good with non managed disk", | ||||
| 			nodeName:      "vm1", | ||||
| 			isManagedDisk: false, | ||||
| 			expectedErr:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "an error shall be returned if update attach disk failed", | ||||
| 			nodeName:      "vm1", | ||||
| 			isManagedDisk: true, | ||||
| 			isAttachFail:  true, | ||||
| 			expectedErr:   true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		vmSet := testCloud.VMSet | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			vm.StorageProfile = &compute.StorageProfile{ | ||||
| 				OsDisk: &compute.OSDisk{ | ||||
| 					Name: pointer.String("osdisk1"), | ||||
| 					ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 						ID: pointer.String("ManagedID"), | ||||
| 						DiskEncryptionSet: &compute.DiskEncryptionSetParameters{ | ||||
| 							ID: pointer.String("DiskEncryptionSetID"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				DataDisks: &[]compute.DataDisk{}, | ||||
| 			} | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		if test.isAttachFail { | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		err := vmSet.AttachDisk(test.isManagedDisk, "", | ||||
| 			"uri", test.nodeName, 0, compute.CachingTypesReadOnly, "", false) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestStandardDetachDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		desc          string | ||||
| 		nodeName      types.NodeName | ||||
| 		diskName      string | ||||
| 		isDetachFail  bool | ||||
| 		expectedError bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if there's no corresponding vm", | ||||
| 			nodeName:      "vm2", | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if there's no corresponding disk", | ||||
| 			nodeName:      "vm1", | ||||
| 			diskName:      "disk2", | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if there's a corresponding disk", | ||||
| 			nodeName:      "vm1", | ||||
| 			diskName:      "disk1", | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "an error shall be returned if detach disk failed", | ||||
| 			nodeName:      "vm1", | ||||
| 			isDetachFail:  true, | ||||
| 			expectedError: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		vmSet := testCloud.VMSet | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		if test.isDetachFail { | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		err := vmSet.DetachDisk(test.diskName, "", test.nodeName) | ||||
| 		assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 		if !test.expectedError && test.diskName != "" { | ||||
| 			dataDisks, err := vmSet.GetDataDisks(test.nodeName, azcache.CacheReadTypeDefault) | ||||
| 			assert.Equal(t, true, len(dataDisks) == 1, "TestCase[%d]: %s, err: %v", i, test.desc, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetDataDisks(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	var testCases = []struct { | ||||
| 		desc              string | ||||
| 		nodeName          types.NodeName | ||||
| 		isDataDiskNull    bool | ||||
| 		expectedDataDisks []compute.DataDisk | ||||
| 		expectedError     bool | ||||
| 		crt               azcache.AzureCacheReadType | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:              "an error shall be returned if there's no corresponding vm", | ||||
| 			nodeName:          "vm2", | ||||
| 			expectedDataDisks: nil, | ||||
| 			expectedError:     true, | ||||
| 			crt:               azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "correct list of data disks shall be returned if everything is good", | ||||
| 			nodeName: "vm1", | ||||
| 			expectedDataDisks: []compute.DataDisk{ | ||||
| 				{ | ||||
| 					Lun:  pointer.Int32(0), | ||||
| 					Name: pointer.String("disk1"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedError: false, | ||||
| 			crt:           azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "correct list of data disks shall be returned if everything is good", | ||||
| 			nodeName: "vm1", | ||||
| 			expectedDataDisks: []compute.DataDisk{ | ||||
| 				{ | ||||
| 					Lun:  pointer.Int32(0), | ||||
| 					Name: pointer.String("disk1"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedError: false, | ||||
| 			crt:           azcache.CacheReadTypeUnsafe, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:              "nil shall be returned if DataDisk is null", | ||||
| 			nodeName:          "vm1", | ||||
| 			isDataDiskNull:    true, | ||||
| 			expectedDataDisks: nil, | ||||
| 			expectedError:     false, | ||||
| 			crt:               azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 	} | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		vmSet := testCloud.VMSet | ||||
| 		expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false) | ||||
| 		mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			if test.isDataDiskNull { | ||||
| 				vm.StorageProfile = &compute.StorageProfile{} | ||||
| 			} | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("vm1"), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		dataDisks, err := vmSet.GetDataDisks(test.nodeName, test.crt) | ||||
| 		assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
|  | ||||
| 		if test.crt == azcache.CacheReadTypeUnsafe { | ||||
| 			time.Sleep(fakeCacheTTL) | ||||
| 			dataDisks, err := vmSet.GetDataDisks(test.nodeName, test.crt) | ||||
| 			assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc) | ||||
| 			assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,207 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // AttachDisk attaches a vhd to vm | ||||
| // the vhd must exist, can be identified by diskName, diskURI, and lun. | ||||
| func (ss *scaleSet) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error { | ||||
| 	vmName := mapNodeNameToVMName(nodeName) | ||||
| 	ssName, instanceID, vm, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	nodeResourceGroup, err := ss.GetNodeResourceGroup(vmName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	disks := []compute.DataDisk{} | ||||
| 	if vm.StorageProfile != nil && vm.StorageProfile.DataDisks != nil { | ||||
| 		disks = make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks)) | ||||
| 		copy(disks, *vm.StorageProfile.DataDisks) | ||||
| 	} | ||||
| 	if isManagedDisk { | ||||
| 		managedDisk := &compute.ManagedDiskParameters{ID: &diskURI} | ||||
| 		if diskEncryptionSetID == "" { | ||||
| 			if vm.StorageProfile.OsDisk != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet != nil && | ||||
| 				vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID != nil { | ||||
| 				// set diskEncryptionSet as value of os disk by default | ||||
| 				diskEncryptionSetID = *vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID | ||||
| 			} | ||||
| 		} | ||||
| 		if diskEncryptionSetID != "" { | ||||
| 			managedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: &diskEncryptionSetID} | ||||
| 		} | ||||
| 		disks = append(disks, | ||||
| 			compute.DataDisk{ | ||||
| 				Name:                    &diskName, | ||||
| 				Lun:                     &lun, | ||||
| 				Caching:                 compute.CachingTypes(cachingMode), | ||||
| 				CreateOption:            "attach", | ||||
| 				ManagedDisk:             managedDisk, | ||||
| 				WriteAcceleratorEnabled: pointer.Bool(writeAcceleratorEnabled), | ||||
| 			}) | ||||
| 	} else { | ||||
| 		disks = append(disks, | ||||
| 			compute.DataDisk{ | ||||
| 				Name: &diskName, | ||||
| 				Vhd: &compute.VirtualHardDisk{ | ||||
| 					URI: &diskURI, | ||||
| 				}, | ||||
| 				Lun:          &lun, | ||||
| 				Caching:      compute.CachingTypes(cachingMode), | ||||
| 				CreateOption: "attach", | ||||
| 			}) | ||||
| 	} | ||||
| 	newVM := compute.VirtualMachineScaleSetVM{ | ||||
| 		VirtualMachineScaleSetVMProperties: &compute.VirtualMachineScaleSetVMProperties{ | ||||
| 			StorageProfile: &compute.StorageProfile{ | ||||
| 				DataDisks: &disks, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Invalidate the cache right after updating | ||||
| 	defer ss.deleteCacheForNode(vmName) | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) with DiskEncryptionSetID(%s)", nodeResourceGroup, nodeName, diskName, diskURI, diskEncryptionSetID) | ||||
| 	rerr := ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "attach_disk") | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("azureDisk - attach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, nodeName, rerr) | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, nodeName) | ||||
| 			disks := ss.filterNonExistingDisks(ctx, *newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks) | ||||
| 			newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks = &disks | ||||
| 			rerr = ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "attach_disk") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s)  returned with %v", nodeResourceGroup, nodeName, diskName, diskURI, rerr) | ||||
| 	if rerr != nil { | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DetachDisk detaches a disk from host | ||||
| // the vhd can be identified by diskName or diskURI | ||||
| func (ss *scaleSet) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error { | ||||
| 	vmName := mapNodeNameToVMName(nodeName) | ||||
| 	ssName, instanceID, vm, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	nodeResourceGroup, err := ss.GetNodeResourceGroup(vmName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	disks := []compute.DataDisk{} | ||||
| 	if vm.StorageProfile != nil && vm.StorageProfile.DataDisks != nil { | ||||
| 		disks = make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks)) | ||||
| 		copy(disks, *vm.StorageProfile.DataDisks) | ||||
| 	} | ||||
| 	bFoundDisk := false | ||||
| 	for i, disk := range disks { | ||||
| 		if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) || | ||||
| 			(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) || | ||||
| 			(disk.ManagedDisk != nil && diskURI != "" && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) { | ||||
| 			// found the disk | ||||
| 			klog.V(2).Infof("azureDisk - detach disk: name %q uri %q", diskName, diskURI) | ||||
| 			if strings.EqualFold(ss.cloud.Environment.Name, AzureStackCloudName) { | ||||
| 				disks = append(disks[:i], disks[i+1:]...) | ||||
| 			} else { | ||||
| 				disks[i].ToBeDetached = pointer.Bool(true) | ||||
| 			} | ||||
| 			bFoundDisk = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if !bFoundDisk { | ||||
| 		// only log here, next action is to update VM status with original meta data | ||||
| 		klog.Errorf("detach azure disk: disk %s not found, diskURI: %s", diskName, diskURI) | ||||
| 	} | ||||
|  | ||||
| 	newVM := compute.VirtualMachineScaleSetVM{ | ||||
| 		VirtualMachineScaleSetVMProperties: &compute.VirtualMachineScaleSetVMProperties{ | ||||
| 			StorageProfile: &compute.StorageProfile{ | ||||
| 				DataDisks: &disks, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Invalidate the cache right after updating | ||||
| 	defer ss.deleteCacheForNode(vmName) | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s)", nodeResourceGroup, nodeName, diskName, diskURI) | ||||
| 	rerr := ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "detach_disk") | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("azureDisk - detach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, nodeName, rerr) | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, nodeName) | ||||
| 			disks := ss.filterNonExistingDisks(ctx, *newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks) | ||||
| 			newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks = &disks | ||||
| 			rerr = ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "detach_disk") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s) returned with %v", nodeResourceGroup, nodeName, diskName, diskURI, rerr) | ||||
| 	if rerr != nil { | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetDataDisks gets a list of data disks attached to the node. | ||||
| func (ss *scaleSet) GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) { | ||||
| 	_, _, vm, err := ss.getVmssVM(string(nodeName), crt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if vm.StorageProfile == nil || vm.StorageProfile.DataDisks == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return *vm.StorageProfile.DataDisks, nil | ||||
| } | ||||
| @@ -1,326 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestAttachDiskWithVMSS(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	fakeStatusNotFoundVMSSName := types.NodeName("FakeStatusNotFoundVMSSName") | ||||
| 	testCases := []struct { | ||||
| 		desc           string | ||||
| 		vmList         map[string]string | ||||
| 		vmssVMList     []string | ||||
| 		vmssName       types.NodeName | ||||
| 		vmssvmName     types.NodeName | ||||
| 		isManagedDisk  bool | ||||
| 		existedDisk    compute.Disk | ||||
| 		expectedErr    bool | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if it is invalid vmss name", | ||||
| 			vmssVMList:     []string{"vmss-vm-000001"}, | ||||
| 			vmssName:       "vm1", | ||||
| 			vmssvmName:     "vm1", | ||||
| 			isManagedDisk:  false, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("not a vmss instance"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if everything is good with managed disk", | ||||
| 			vmssVMList:    []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:      "vmss00", | ||||
| 			vmssvmName:    "vmss00-vm-000000", | ||||
| 			isManagedDisk: true, | ||||
| 			existedDisk:   compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedErr:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:          "no error shall be returned if everything is good with non-managed disk", | ||||
| 			vmssVMList:    []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:      "vmss00", | ||||
| 			vmssvmName:    "vmss00-vm-000000", | ||||
| 			isManagedDisk: false, | ||||
| 			existedDisk:   compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedErr:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if response StatusNotFound", | ||||
| 			vmssVMList:     []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:       fakeStatusNotFoundVMSSName, | ||||
| 			vmssvmName:     "vmss00-vm-000000", | ||||
| 			isManagedDisk:  false, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String("disk-name")}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 404, RawError: %w", cloudprovider.InstanceNotFound), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		scaleSetName := string(test.vmssName) | ||||
| 		ss, err := newTestScaleSet(ctrl) | ||||
| 		assert.NoError(t, err, test.desc) | ||||
| 		testCloud := ss.cloud | ||||
| 		testCloud.PrimaryScaleSetName = scaleSetName | ||||
| 		expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false) | ||||
| 		mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface) | ||||
| 		mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes() | ||||
| 		mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1) | ||||
| 		mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1) | ||||
|  | ||||
| 		expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, test.vmssVMList, "succeeded", false) | ||||
| 		for _, vmssvm := range expectedVMSSVMs { | ||||
| 			vmssvm.StorageProfile = &compute.StorageProfile{ | ||||
| 				OsDisk: &compute.OSDisk{ | ||||
| 					Name: pointer.String("osdisk1"), | ||||
| 					ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 						ID: pointer.String("ManagedID"), | ||||
| 						DiskEncryptionSet: &compute.DiskEncryptionSetParameters{ | ||||
| 							ID: pointer.String("DiskEncryptionSetID"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				DataDisks: &[]compute.DataDisk{}, | ||||
| 			} | ||||
| 		} | ||||
| 		mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface) | ||||
| 		mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes() | ||||
| 		if scaleSetName == string(fakeStatusNotFoundVMSSName) { | ||||
| 			mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
|  | ||||
| 		err = ss.AttachDisk(test.isManagedDisk, "disk-name", diskURI, test.vmssvmName, 0, compute.CachingTypesReadWrite, "", true) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDetachDiskWithVMSS(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	fakeStatusNotFoundVMSSName := types.NodeName("FakeStatusNotFoundVMSSName") | ||||
| 	diskName := "disk-name" | ||||
| 	testCases := []struct { | ||||
| 		desc           string | ||||
| 		vmList         map[string]string | ||||
| 		vmssVMList     []string | ||||
| 		vmssName       types.NodeName | ||||
| 		vmssvmName     types.NodeName | ||||
| 		existedDisk    compute.Disk | ||||
| 		expectedErr    bool | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if it is invalid vmss name", | ||||
| 			vmssVMList:     []string{"vmss-vm-000001"}, | ||||
| 			vmssName:       "vm1", | ||||
| 			vmssvmName:     "vm1", | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(diskName)}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("not a vmss instance"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "no error shall be returned if everything is good", | ||||
| 			vmssVMList:  []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:    "vmss00", | ||||
| 			vmssvmName:  "vmss00-vm-000000", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String(diskName)}, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if response StatusNotFound", | ||||
| 			vmssVMList:     []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:       fakeStatusNotFoundVMSSName, | ||||
| 			vmssvmName:     "vmss00-vm-000000", | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(diskName)}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 404, RawError: %w", cloudprovider.InstanceNotFound), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "no error shall be returned if everything is good and the attaching disk does not match data disk", | ||||
| 			vmssVMList:  []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"}, | ||||
| 			vmssName:    "vmss00", | ||||
| 			vmssvmName:  "vmss00-vm-000000", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk-name-err")}, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		scaleSetName := string(test.vmssName) | ||||
| 		ss, err := newTestScaleSet(ctrl) | ||||
| 		assert.NoError(t, err, test.desc) | ||||
| 		testCloud := ss.cloud | ||||
| 		testCloud.PrimaryScaleSetName = scaleSetName | ||||
| 		expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false) | ||||
| 		mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface) | ||||
| 		mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes() | ||||
| 		mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1) | ||||
| 		mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1) | ||||
|  | ||||
| 		expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, test.vmssVMList, "succeeded", false) | ||||
| 		for _, vmssvm := range expectedVMSSVMs { | ||||
| 			vmssvm.StorageProfile = &compute.StorageProfile{ | ||||
| 				OsDisk: &compute.OSDisk{ | ||||
| 					Name: pointer.String("osdisk1"), | ||||
| 					ManagedDisk: &compute.ManagedDiskParameters{ | ||||
| 						ID: pointer.String("ManagedID"), | ||||
| 						DiskEncryptionSet: &compute.DiskEncryptionSetParameters{ | ||||
| 							ID: pointer.String("DiskEncryptionSetID"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				DataDisks: &[]compute.DataDisk{{ | ||||
| 					Lun:  pointer.Int32(0), | ||||
| 					Name: pointer.String(diskName), | ||||
| 				}}, | ||||
| 			} | ||||
| 		} | ||||
| 		mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface) | ||||
| 		mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes() | ||||
| 		if scaleSetName == string(fakeStatusNotFoundVMSSName) { | ||||
| 			mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		err = ss.DetachDisk(*test.existedDisk.Name, diskName, test.vmssvmName) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err) | ||||
|  | ||||
| 		if !test.expectedErr { | ||||
| 			dataDisks, err := ss.GetDataDisks(test.vmssvmName, azcache.CacheReadTypeDefault) | ||||
| 			assert.Equal(t, true, len(dataDisks) == 1, "TestCase[%d]: %s, actual data disk num: %d, err: %v", i, test.desc, len(dataDisks), err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetDataDisksWithVMSS(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	var testCases = []struct { | ||||
| 		desc              string | ||||
| 		nodeName          types.NodeName | ||||
| 		isDataDiskNull    bool | ||||
| 		expectedDataDisks []compute.DataDisk | ||||
| 		expectedErr       bool | ||||
| 		expectedErrMsg    error | ||||
| 		crt               azcache.AzureCacheReadType | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:              "an error shall be returned if there's no corresponding vm", | ||||
| 			nodeName:          "vmss00-vm-000001", | ||||
| 			expectedDataDisks: nil, | ||||
| 			expectedErr:       true, | ||||
| 			expectedErrMsg:    fmt.Errorf("instance not found"), | ||||
| 			crt:               azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "correct list of data disks shall be returned if everything is good", | ||||
| 			nodeName: "vmss00-vm-000000", | ||||
| 			expectedDataDisks: []compute.DataDisk{ | ||||
| 				{ | ||||
| 					Lun:  pointer.Int32(0), | ||||
| 					Name: pointer.String("disk1"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedErr: false, | ||||
| 			crt:         azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "correct list of data disks shall be returned if everything is good", | ||||
| 			nodeName: "vmss00-vm-000000", | ||||
| 			expectedDataDisks: []compute.DataDisk{ | ||||
| 				{ | ||||
| 					Lun:  pointer.Int32(0), | ||||
| 					Name: pointer.String("disk1"), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedErr: false, | ||||
| 			crt:         azcache.CacheReadTypeUnsafe, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:              "nil shall be returned if DataDisk is null", | ||||
| 			nodeName:          "vmss00-vm-000000", | ||||
| 			isDataDiskNull:    true, | ||||
| 			expectedDataDisks: nil, | ||||
| 			expectedErr:       false, | ||||
| 			crt:               azcache.CacheReadTypeDefault, | ||||
| 		}, | ||||
| 	} | ||||
| 	for i, test := range testCases { | ||||
| 		scaleSetName := string(test.nodeName) | ||||
| 		ss, err := newTestScaleSet(ctrl) | ||||
| 		assert.NoError(t, err, test.desc) | ||||
| 		testCloud := ss.cloud | ||||
| 		testCloud.PrimaryScaleSetName = scaleSetName | ||||
| 		expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false) | ||||
| 		mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface) | ||||
| 		mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes() | ||||
| 		mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1) | ||||
| 		mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1) | ||||
|  | ||||
| 		expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, []string{"vmss00-vm-000000"}, "succeeded", false) | ||||
| 		if !test.isDataDiskNull { | ||||
| 			for _, vmssvm := range expectedVMSSVMs { | ||||
| 				vmssvm.StorageProfile = &compute.StorageProfile{ | ||||
| 					DataDisks: &[]compute.DataDisk{{ | ||||
| 						Lun:  pointer.Int32(0), | ||||
| 						Name: pointer.String("disk1"), | ||||
| 					}}, | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface) | ||||
| 		mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes() | ||||
| 		mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 		dataDisks, err := ss.GetDataDisks(test.nodeName, test.crt) | ||||
| 		assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 	} | ||||
| } | ||||
| @@ -1,99 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/client-go/tools/record" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/auth" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/mockloadbalancerclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routeclient/mockrouteclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient/mocksecuritygroupclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/subnetclient/mocksubnetclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	errPreconditionFailedEtagMismatch = fmt.Errorf("PreconditionFailedEtagMismatch") | ||||
| ) | ||||
|  | ||||
| // GetTestCloud returns a fake azure cloud for unit tests in Azure related CSI drivers | ||||
| func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) { | ||||
| 	az = &Cloud{ | ||||
| 		Config: Config{ | ||||
| 			AzureAuthConfig: auth.AzureAuthConfig{ | ||||
| 				TenantID:       "tenant", | ||||
| 				SubscriptionID: "subscription", | ||||
| 			}, | ||||
| 			ResourceGroup:                "rg", | ||||
| 			VnetResourceGroup:            "rg", | ||||
| 			RouteTableResourceGroup:      "rg", | ||||
| 			SecurityGroupResourceGroup:   "rg", | ||||
| 			Location:                     "westus", | ||||
| 			VnetName:                     "vnet", | ||||
| 			SubnetName:                   "subnet", | ||||
| 			SecurityGroupName:            "nsg", | ||||
| 			RouteTableName:               "rt", | ||||
| 			PrimaryAvailabilitySetName:   "as", | ||||
| 			PrimaryScaleSetName:          "vmss", | ||||
| 			MaximumLoadBalancerRuleCount: 250, | ||||
| 			VMType:                       vmTypeStandard, | ||||
| 		}, | ||||
| 		nodeZones:                map[string]sets.String{}, | ||||
| 		nodeInformerSynced:       func() bool { return true }, | ||||
| 		nodeResourceGroups:       map[string]string{}, | ||||
| 		unmanagedNodes:           sets.NewString(), | ||||
| 		excludeLoadBalancerNodes: sets.NewString(), | ||||
| 		routeCIDRs:               map[string]string{}, | ||||
| 		eventRecorder:            &record.FakeRecorder{}, | ||||
| 	} | ||||
| 	az.DisksClient = mockdiskclient.NewMockInterface(ctrl) | ||||
| 	az.InterfacesClient = mockinterfaceclient.NewMockInterface(ctrl) | ||||
| 	az.LoadBalancerClient = mockloadbalancerclient.NewMockInterface(ctrl) | ||||
| 	az.PublicIPAddressesClient = mockpublicipclient.NewMockInterface(ctrl) | ||||
| 	az.RoutesClient = mockrouteclient.NewMockInterface(ctrl) | ||||
| 	az.RouteTablesClient = mockroutetableclient.NewMockInterface(ctrl) | ||||
| 	az.SecurityGroupsClient = mocksecuritygroupclient.NewMockInterface(ctrl) | ||||
| 	az.SubnetsClient = mocksubnetclient.NewMockInterface(ctrl) | ||||
| 	az.VirtualMachineScaleSetsClient = mockvmssclient.NewMockInterface(ctrl) | ||||
| 	az.VirtualMachineScaleSetVMsClient = mockvmssvmclient.NewMockInterface(ctrl) | ||||
| 	az.VirtualMachinesClient = mockvmclient.NewMockInterface(ctrl) | ||||
| 	az.VMSet = newAvailabilitySet(az) | ||||
| 	az.vmCache, _ = az.newVMCache() | ||||
| 	az.lbCache, _ = az.newLBCache() | ||||
| 	az.nsgCache, _ = az.newNSGCache() | ||||
| 	az.rtCache, _ = az.newRouteTableCache() | ||||
|  | ||||
| 	common := &controllerCommon{cloud: az, resourceGroup: "rg", location: "westus"} | ||||
| 	az.controllerCommon = common | ||||
| 	az.ManagedDiskController = &ManagedDiskController{common: common} | ||||
|  | ||||
| 	return az | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/fileclient" | ||||
| ) | ||||
|  | ||||
| // create file share | ||||
| func (az *Cloud) createFileShare(resourceGroupName, accountName string, shareOptions *fileclient.ShareOptions) error { | ||||
| 	return az.FileClient.CreateFileShare(resourceGroupName, accountName, shareOptions) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) deleteFileShare(resourceGroupName, accountName, name string) error { | ||||
| 	return az.FileClient.DeleteFileShare(resourceGroupName, accountName, name) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) resizeFileShare(resourceGroupName, accountName, name string, sizeGiB int) error { | ||||
| 	return az.FileClient.ResizeFileShare(resourceGroupName, accountName, name, sizeGiB) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getFileShare(resourceGroupName, accountName, name string) (storage.FileShare, error) { | ||||
| 	return az.FileClient.GetFileShare(resourceGroupName, accountName, name) | ||||
| } | ||||
| @@ -1,270 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	metadataCacheTTL           = time.Minute | ||||
| 	metadataCacheKey           = "InstanceMetadata" | ||||
| 	imdsInstanceAPIVersion     = "2019-03-11" | ||||
| 	imdsLoadBalancerAPIVersion = "2020-10-01" | ||||
| 	imdsServer                 = "http://169.254.169.254" | ||||
| 	imdsInstanceURI            = "/metadata/instance" | ||||
| 	imdsLoadBalancerURI        = "/metadata/loadbalancer" | ||||
| ) | ||||
|  | ||||
| // NetworkMetadata contains metadata about an instance's network | ||||
| type NetworkMetadata struct { | ||||
| 	Interface []NetworkInterface `json:"interface"` | ||||
| } | ||||
|  | ||||
| // NetworkInterface represents an instances network interface. | ||||
| type NetworkInterface struct { | ||||
| 	IPV4 NetworkData `json:"ipv4"` | ||||
| 	IPV6 NetworkData `json:"ipv6"` | ||||
| 	MAC  string      `json:"macAddress"` | ||||
| } | ||||
|  | ||||
| // NetworkData contains IP information for a network. | ||||
| type NetworkData struct { | ||||
| 	IPAddress []IPAddress `json:"ipAddress"` | ||||
| 	Subnet    []Subnet    `json:"subnet"` | ||||
| } | ||||
|  | ||||
| // IPAddress represents IP address information. | ||||
| type IPAddress struct { | ||||
| 	PrivateIP string `json:"privateIpAddress"` | ||||
| 	PublicIP  string `json:"publicIpAddress"` | ||||
| } | ||||
|  | ||||
| // Subnet represents subnet information. | ||||
| type Subnet struct { | ||||
| 	Address string `json:"address"` | ||||
| 	Prefix  string `json:"prefix"` | ||||
| } | ||||
|  | ||||
| // ComputeMetadata represents compute information | ||||
| type ComputeMetadata struct { | ||||
| 	Environment    string `json:"azEnvironment,omitempty"` | ||||
| 	SKU            string `json:"sku,omitempty"` | ||||
| 	Name           string `json:"name,omitempty"` | ||||
| 	Zone           string `json:"zone,omitempty"` | ||||
| 	VMSize         string `json:"vmSize,omitempty"` | ||||
| 	OSType         string `json:"osType,omitempty"` | ||||
| 	Location       string `json:"location,omitempty"` | ||||
| 	FaultDomain    string `json:"platformFaultDomain,omitempty"` | ||||
| 	UpdateDomain   string `json:"platformUpdateDomain,omitempty"` | ||||
| 	ResourceGroup  string `json:"resourceGroupName,omitempty"` | ||||
| 	VMScaleSetName string `json:"vmScaleSetName,omitempty"` | ||||
| 	SubscriptionID string `json:"subscriptionId,omitempty"` | ||||
| } | ||||
|  | ||||
| // InstanceMetadata represents instance information. | ||||
| type InstanceMetadata struct { | ||||
| 	Compute *ComputeMetadata `json:"compute,omitempty"` | ||||
| 	Network *NetworkMetadata `json:"network,omitempty"` | ||||
| } | ||||
|  | ||||
| // PublicIPMetadata represents the public IP metadata. | ||||
| type PublicIPMetadata struct { | ||||
| 	FrontendIPAddress string `json:"frontendIpAddress,omitempty"` | ||||
| 	PrivateIPAddress  string `json:"privateIpAddress,omitempty"` | ||||
| } | ||||
|  | ||||
| // LoadbalancerProfile represents load balancer profile in IMDS. | ||||
| type LoadbalancerProfile struct { | ||||
| 	PublicIPAddresses []PublicIPMetadata `json:"publicIpAddresses,omitempty"` | ||||
| } | ||||
|  | ||||
| // LoadBalancerMetadata represents load balancer metadata. | ||||
| type LoadBalancerMetadata struct { | ||||
| 	LoadBalancer *LoadbalancerProfile `json:"loadbalancer,omitempty"` | ||||
| } | ||||
|  | ||||
| // InstanceMetadataService knows how to query the Azure instance metadata server. | ||||
| type InstanceMetadataService struct { | ||||
| 	imdsServer string | ||||
| 	imsCache   *azcache.TimedCache | ||||
| } | ||||
|  | ||||
| // NewInstanceMetadataService creates an instance of the InstanceMetadataService accessor object. | ||||
| func NewInstanceMetadataService(imdsServer string) (*InstanceMetadataService, error) { | ||||
| 	ims := &InstanceMetadataService{ | ||||
| 		imdsServer: imdsServer, | ||||
| 	} | ||||
|  | ||||
| 	imsCache, err := azcache.NewTimedcache(metadataCacheTTL, ims.getMetadata) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	ims.imsCache = imsCache | ||||
| 	return ims, nil | ||||
| } | ||||
|  | ||||
| func (ims *InstanceMetadataService) getMetadata(key string) (interface{}, error) { | ||||
| 	instanceMetadata, err := ims.getInstanceMetadata(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if instanceMetadata.Network != nil && len(instanceMetadata.Network.Interface) > 0 { | ||||
| 		netInterface := instanceMetadata.Network.Interface[0] | ||||
| 		if (len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PublicIP) > 0) || | ||||
| 			(len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PublicIP) > 0) { | ||||
| 			// Return if public IP address has already part of instance metadata. | ||||
| 			return instanceMetadata, nil | ||||
| 		} | ||||
|  | ||||
| 		loadBalancerMetadata, err := ims.getLoadBalancerMetadata() | ||||
| 		if err != nil || loadBalancerMetadata == nil || loadBalancerMetadata.LoadBalancer == nil { | ||||
| 			// Log a warning since loadbalancer metadata may not be available when the VM | ||||
| 			// is not in standard LoadBalancer backend address pool. | ||||
| 			klog.V(4).Infof("Warning: failed to get loadbalancer metadata: %v", err) | ||||
| 			return instanceMetadata, nil | ||||
| 		} | ||||
|  | ||||
| 		publicIPs := loadBalancerMetadata.LoadBalancer.PublicIPAddresses | ||||
| 		if len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PrivateIP) > 0 { | ||||
| 			for _, pip := range publicIPs { | ||||
| 				if pip.PrivateIPAddress == netInterface.IPV4.IPAddress[0].PrivateIP { | ||||
| 					netInterface.IPV4.IPAddress[0].PublicIP = pip.FrontendIPAddress | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PrivateIP) > 0 { | ||||
| 			for _, pip := range publicIPs { | ||||
| 				if pip.PrivateIPAddress == netInterface.IPV6.IPAddress[0].PrivateIP { | ||||
| 					netInterface.IPV6.IPAddress[0].PublicIP = pip.FrontendIPAddress | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return instanceMetadata, nil | ||||
| } | ||||
|  | ||||
| func (ims *InstanceMetadataService) getInstanceMetadata(key string) (*InstanceMetadata, error) { | ||||
| 	req, err := http.NewRequest("GET", ims.imdsServer+imdsInstanceURI, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Add("Metadata", "True") | ||||
| 	req.Header.Add("User-Agent", "golang/kubernetes-cloud-provider") | ||||
|  | ||||
| 	q := req.URL.Query() | ||||
| 	q.Add("format", "json") | ||||
| 	q.Add("api-version", imdsInstanceAPIVersion) | ||||
| 	req.URL.RawQuery = q.Encode() | ||||
|  | ||||
| 	client := &http.Client{} | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("failure of getting instance metadata with response %q", resp.Status) | ||||
| 	} | ||||
|  | ||||
| 	data, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	obj := InstanceMetadata{} | ||||
| 	err = json.Unmarshal(data, &obj) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &obj, nil | ||||
| } | ||||
|  | ||||
| func (ims *InstanceMetadataService) getLoadBalancerMetadata() (*LoadBalancerMetadata, error) { | ||||
| 	req, err := http.NewRequest("GET", ims.imdsServer+imdsLoadBalancerURI, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Add("Metadata", "True") | ||||
| 	req.Header.Add("User-Agent", "golang/kubernetes-cloud-provider") | ||||
|  | ||||
| 	q := req.URL.Query() | ||||
| 	q.Add("format", "json") | ||||
| 	q.Add("api-version", imdsLoadBalancerAPIVersion) | ||||
| 	req.URL.RawQuery = q.Encode() | ||||
|  | ||||
| 	client := &http.Client{} | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("failure of getting loadbalancer metadata with response %q", resp.Status) | ||||
| 	} | ||||
|  | ||||
| 	data, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	obj := LoadBalancerMetadata{} | ||||
| 	err = json.Unmarshal(data, &obj) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &obj, nil | ||||
| } | ||||
|  | ||||
| // GetMetadata gets instance metadata from cache. | ||||
| // crt determines if we can get data from stalled cache/need fresh if cache expired. | ||||
| func (ims *InstanceMetadataService) GetMetadata(crt azcache.AzureCacheReadType) (*InstanceMetadata, error) { | ||||
| 	cache, err := ims.imsCache.Get(metadataCacheKey, crt) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Cache shouldn't be nil, but added a check in case something is wrong. | ||||
| 	if cache == nil { | ||||
| 		return nil, fmt.Errorf("failure of getting instance metadata") | ||||
| 	} | ||||
|  | ||||
| 	if metadata, ok := cache.(*InstanceMetadata); ok { | ||||
| 		return metadata, nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, fmt.Errorf("failure of getting instance metadata") | ||||
| } | ||||
| @@ -1,432 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	vmPowerStatePrefix       = "PowerState/" | ||||
| 	vmPowerStateStopped      = "stopped" | ||||
| 	vmPowerStateDeallocated  = "deallocated" | ||||
| 	vmPowerStateDeallocating = "deallocating" | ||||
|  | ||||
| 	// nodeNameEnvironmentName is the environment variable name for getting node name. | ||||
| 	// It is only used for out-of-tree cloud provider. | ||||
| 	nodeNameEnvironmentName = "NODE_NAME" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	errNodeNotInitialized = fmt.Errorf("providerID is empty, the node is not initialized yet") | ||||
| ) | ||||
|  | ||||
| func (az *Cloud) addressGetter(nodeName types.NodeName) ([]v1.NodeAddress, error) { | ||||
| 	ip, publicIP, err := az.getIPForMachine(nodeName) | ||||
| 	if err != nil { | ||||
| 		klog.V(2).Infof("NodeAddresses(%s) abort backoff: %v", nodeName, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	addresses := []v1.NodeAddress{ | ||||
| 		{Type: v1.NodeInternalIP, Address: ip}, | ||||
| 		{Type: v1.NodeHostName, Address: string(nodeName)}, | ||||
| 	} | ||||
| 	if len(publicIP) > 0 { | ||||
| 		addresses = append(addresses, v1.NodeAddress{ | ||||
| 			Type:    v1.NodeExternalIP, | ||||
| 			Address: publicIP, | ||||
| 		}) | ||||
| 	} | ||||
| 	return addresses, nil | ||||
| } | ||||
|  | ||||
| // NodeAddresses returns the addresses of the specified instance. | ||||
| func (az *Cloud) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.NodeAddress, error) { | ||||
| 	// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(string(name)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		klog.V(4).Infof("NodeAddresses: omitting unmanaged node %q", name) | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.UseInstanceMetadata { | ||||
| 		metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		if metadata.Compute == nil || metadata.Network == nil { | ||||
| 			return nil, fmt.Errorf("failure of getting instance metadata") | ||||
| 		} | ||||
|  | ||||
| 		isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		// Not local instance, get addresses from Azure ARM API. | ||||
| 		if !isLocalInstance { | ||||
| 			if az.VMSet != nil { | ||||
| 				return az.addressGetter(name) | ||||
| 			} | ||||
|  | ||||
| 			// vmSet == nil indicates credentials are not provided. | ||||
| 			return nil, fmt.Errorf("no credentials provided for Azure cloud provider") | ||||
| 		} | ||||
|  | ||||
| 		return az.getLocalInstanceNodeAddresses(metadata.Network.Interface, string(name)) | ||||
| 	} | ||||
|  | ||||
| 	return az.addressGetter(name) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getLocalInstanceNodeAddresses(netInterfaces []NetworkInterface, nodeName string) ([]v1.NodeAddress, error) { | ||||
| 	if len(netInterfaces) == 0 { | ||||
| 		return nil, fmt.Errorf("no interface is found for the instance") | ||||
| 	} | ||||
|  | ||||
| 	// Use ip address got from instance metadata. | ||||
| 	netInterface := netInterfaces[0] | ||||
| 	addresses := []v1.NodeAddress{ | ||||
| 		{Type: v1.NodeHostName, Address: nodeName}, | ||||
| 	} | ||||
| 	if len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PrivateIP) > 0 { | ||||
| 		address := netInterface.IPV4.IPAddress[0] | ||||
| 		addresses = append(addresses, v1.NodeAddress{ | ||||
| 			Type:    v1.NodeInternalIP, | ||||
| 			Address: address.PrivateIP, | ||||
| 		}) | ||||
| 		if len(address.PublicIP) > 0 { | ||||
| 			addresses = append(addresses, v1.NodeAddress{ | ||||
| 				Type:    v1.NodeExternalIP, | ||||
| 				Address: address.PublicIP, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PrivateIP) > 0 { | ||||
| 		address := netInterface.IPV6.IPAddress[0] | ||||
| 		addresses = append(addresses, v1.NodeAddress{ | ||||
| 			Type:    v1.NodeInternalIP, | ||||
| 			Address: address.PrivateIP, | ||||
| 		}) | ||||
| 		if len(address.PublicIP) > 0 { | ||||
| 			addresses = append(addresses, v1.NodeAddress{ | ||||
| 				Type:    v1.NodeExternalIP, | ||||
| 				Address: address.PublicIP, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(addresses) == 1 { | ||||
| 		// No IP addresses is got from instance metadata service, clean up cache and report errors. | ||||
| 		az.metadata.imsCache.Delete(metadataCacheKey) | ||||
| 		return nil, fmt.Errorf("get empty IP addresses from instance metadata service") | ||||
| 	} | ||||
| 	return addresses, nil | ||||
| } | ||||
|  | ||||
| // NodeAddressesByProviderID returns the node addresses of an instances with the specified unique providerID | ||||
| // This method will not be called from the node that is requesting this ID. i.e. metadata service | ||||
| // and other local methods cannot be used here | ||||
| func (az *Cloud) NodeAddressesByProviderID(ctx context.Context, providerID string) ([]v1.NodeAddress, error) { | ||||
| 	if providerID == "" { | ||||
| 		return nil, errNodeNotInitialized | ||||
| 	} | ||||
|  | ||||
| 	// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	if az.IsNodeUnmanagedByProviderID(providerID) { | ||||
| 		klog.V(4).Infof("NodeAddressesByProviderID: omitting unmanaged node %q", providerID) | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	name, err := az.VMSet.GetNodeNameByProviderID(providerID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return az.NodeAddresses(ctx, name) | ||||
| } | ||||
|  | ||||
| // InstanceExistsByProviderID returns true if the instance with the given provider id still exists and is running. | ||||
| // If false is returned with no error, the instance will be immediately deleted by the cloud controller manager. | ||||
| func (az *Cloud) InstanceExistsByProviderID(ctx context.Context, providerID string) (bool, error) { | ||||
| 	if providerID == "" { | ||||
| 		return false, errNodeNotInitialized | ||||
| 	} | ||||
|  | ||||
| 	// Returns true for unmanaged nodes because azure cloud provider always assumes them exists. | ||||
| 	if az.IsNodeUnmanagedByProviderID(providerID) { | ||||
| 		klog.V(4).Infof("InstanceExistsByProviderID: assuming unmanaged node %q exists", providerID) | ||||
| 		return true, nil | ||||
| 	} | ||||
|  | ||||
| 	name, err := az.VMSet.GetNodeNameByProviderID(providerID) | ||||
| 	if err != nil { | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	_, err = az.InstanceID(ctx, name) | ||||
| 	if err != nil { | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| // InstanceShutdownByProviderID returns true if the instance is in safe state to detach volumes | ||||
| func (az *Cloud) InstanceShutdownByProviderID(ctx context.Context, providerID string) (bool, error) { | ||||
| 	if providerID == "" { | ||||
| 		return false, nil | ||||
| 	} | ||||
|  | ||||
| 	nodeName, err := az.VMSet.GetNodeNameByProviderID(providerID) | ||||
| 	if err != nil { | ||||
| 		// Returns false, so the controller manager will continue to check InstanceExistsByProviderID(). | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			return false, nil | ||||
| 		} | ||||
|  | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	powerStatus, err := az.VMSet.GetPowerStatusByNodeName(string(nodeName)) | ||||
| 	if err != nil { | ||||
| 		// Returns false, so the controller manager will continue to check InstanceExistsByProviderID(). | ||||
| 		if err == cloudprovider.InstanceNotFound { | ||||
| 			return false, nil | ||||
| 		} | ||||
|  | ||||
| 		return false, err | ||||
| 	} | ||||
| 	klog.V(3).Infof("InstanceShutdownByProviderID gets power status %q for node %q", powerStatus, nodeName) | ||||
|  | ||||
| 	provisioningState, err := az.VMSet.GetProvisioningStateByNodeName(string(nodeName)) | ||||
| 	if err != nil { | ||||
| 		// Returns false, so the controller manager will continue to check InstanceExistsByProviderID(). | ||||
| 		if errors.Is(err, cloudprovider.InstanceNotFound) { | ||||
| 			return false, nil | ||||
| 		} | ||||
|  | ||||
| 		return false, err | ||||
| 	} | ||||
| 	klog.V(3).Infof("InstanceShutdownByProviderID gets provisioning state %q for node %q", provisioningState, nodeName) | ||||
|  | ||||
| 	status := strings.ToLower(powerStatus) | ||||
| 	provisioningSucceeded := strings.EqualFold(strings.ToLower(provisioningState), strings.ToLower(string(compute.ProvisioningStateSucceeded))) | ||||
| 	return provisioningSucceeded && (status == vmPowerStateStopped || status == vmPowerStateDeallocated || status == vmPowerStateDeallocating), nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) isCurrentInstance(name types.NodeName, metadataVMName string) (bool, error) { | ||||
| 	var err error | ||||
| 	nodeName := mapNodeNameToVMName(name) | ||||
|  | ||||
| 	// VMSS vmName is not same with hostname, use hostname instead. | ||||
| 	if az.VMType == vmTypeVMSS { | ||||
| 		metadataVMName, err = os.Hostname() | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		// Use name from env variable "NODE_NAME" if it is set. | ||||
| 		nodeNameEnv := os.Getenv(nodeNameEnvironmentName) | ||||
| 		if nodeNameEnv != "" { | ||||
| 			metadataVMName = nodeNameEnv | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	metadataVMName = strings.ToLower(metadataVMName) | ||||
| 	return metadataVMName == nodeName, nil | ||||
| } | ||||
|  | ||||
| // InstanceID returns the cloud provider ID of the specified instance. | ||||
| // Note that if the instance does not exist or is no longer running, we must return ("", cloudprovider.InstanceNotFound) | ||||
| func (az *Cloud) InstanceID(ctx context.Context, name types.NodeName) (string, error) { | ||||
| 	nodeName := mapNodeNameToVMName(name) | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(nodeName) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		// InstanceID is same with nodeName for unmanaged nodes. | ||||
| 		klog.V(4).Infof("InstanceID: getting ID %q for unmanaged node %q", name, name) | ||||
| 		return nodeName, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.UseInstanceMetadata { | ||||
| 		metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		if metadata.Compute == nil { | ||||
| 			return "", fmt.Errorf("failure of getting instance metadata") | ||||
| 		} | ||||
|  | ||||
| 		isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		// Not local instance, get instanceID from Azure ARM API. | ||||
| 		if !isLocalInstance { | ||||
| 			if az.VMSet != nil { | ||||
| 				return az.VMSet.GetInstanceIDByNodeName(nodeName) | ||||
| 			} | ||||
|  | ||||
| 			// vmSet == nil indicates credentials are not provided. | ||||
| 			return "", fmt.Errorf("no credentials provided for Azure cloud provider") | ||||
| 		} | ||||
| 		return az.getLocalInstanceProviderID(metadata, nodeName) | ||||
| 	} | ||||
|  | ||||
| 	return az.VMSet.GetInstanceIDByNodeName(nodeName) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getLocalInstanceProviderID(metadata *InstanceMetadata, nodeName string) (string, error) { | ||||
| 	// Get resource group name and subscription ID. | ||||
| 	resourceGroup := strings.ToLower(metadata.Compute.ResourceGroup) | ||||
| 	subscriptionID := strings.ToLower(metadata.Compute.SubscriptionID) | ||||
|  | ||||
| 	// Compose instanceID based on nodeName for standard instance. | ||||
| 	if metadata.Compute.VMScaleSetName == "" { | ||||
| 		return az.getStandardMachineID(subscriptionID, resourceGroup, nodeName), nil | ||||
| 	} | ||||
|  | ||||
| 	// Get scale set name and instanceID from vmName for vmss. | ||||
| 	ssName, instanceID, err := extractVmssVMName(metadata.Compute.Name) | ||||
| 	if err != nil { | ||||
| 		if err == ErrorNotVmssInstance { | ||||
| 			// Compose machineID for standard Node. | ||||
| 			return az.getStandardMachineID(subscriptionID, resourceGroup, nodeName), nil | ||||
| 		} | ||||
| 		return "", err | ||||
| 	} | ||||
| 	// Compose instanceID based on ssName and instanceID for vmss instance. | ||||
| 	return az.getVmssMachineID(subscriptionID, resourceGroup, ssName, instanceID), nil | ||||
| } | ||||
|  | ||||
| // InstanceTypeByProviderID returns the cloudprovider instance type of the node with the specified unique providerID | ||||
| // This method will not be called from the node that is requesting this ID. i.e. metadata service | ||||
| // and other local methods cannot be used here | ||||
| func (az *Cloud) InstanceTypeByProviderID(ctx context.Context, providerID string) (string, error) { | ||||
| 	if providerID == "" { | ||||
| 		return "", errNodeNotInitialized | ||||
| 	} | ||||
|  | ||||
| 	// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	if az.IsNodeUnmanagedByProviderID(providerID) { | ||||
| 		klog.V(4).Infof("InstanceTypeByProviderID: omitting unmanaged node %q", providerID) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	name, err := az.VMSet.GetNodeNameByProviderID(providerID) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return az.InstanceType(ctx, name) | ||||
| } | ||||
|  | ||||
| // InstanceType returns the type of the specified instance. | ||||
| // Note that if the instance does not exist or is no longer running, we must return ("", cloudprovider.InstanceNotFound) | ||||
| // (Implementer Note): This is used by kubelet. Kubelet will label the node. Real log from kubelet: | ||||
| // | ||||
| //	Adding node label from cloud provider: beta.kubernetes.io/instance-type=[value] | ||||
| func (az *Cloud) InstanceType(ctx context.Context, name types.NodeName) (string, error) { | ||||
| 	// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(string(name)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		klog.V(4).Infof("InstanceType: omitting unmanaged node %q", name) | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	if az.UseInstanceMetadata { | ||||
| 		metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		if metadata.Compute == nil { | ||||
| 			return "", fmt.Errorf("failure of getting instance metadata") | ||||
| 		} | ||||
|  | ||||
| 		isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		if !isLocalInstance { | ||||
| 			if az.VMSet != nil { | ||||
| 				return az.VMSet.GetInstanceTypeByNodeName(string(name)) | ||||
| 			} | ||||
|  | ||||
| 			// vmSet == nil indicates credentials are not provided. | ||||
| 			return "", fmt.Errorf("no credentials provided for Azure cloud provider") | ||||
| 		} | ||||
|  | ||||
| 		if metadata.Compute.VMSize != "" { | ||||
| 			return metadata.Compute.VMSize, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return az.VMSet.GetInstanceTypeByNodeName(string(name)) | ||||
| } | ||||
|  | ||||
| // AddSSHKeyToAllInstances adds an SSH public key as a legal identity for all instances | ||||
| // expected format for the key is standard ssh-keygen format: <protocol> <blob> | ||||
| func (az *Cloud) AddSSHKeyToAllInstances(ctx context.Context, user string, keyData []byte) error { | ||||
| 	return cloudprovider.NotImplemented | ||||
| } | ||||
|  | ||||
| // CurrentNodeName returns the name of the node we are currently running on. | ||||
| // On Azure this is the hostname, so we just return the hostname. | ||||
| func (az *Cloud) CurrentNodeName(ctx context.Context, hostname string) (types.NodeName, error) { | ||||
| 	return types.NodeName(hostname), nil | ||||
| } | ||||
|  | ||||
| // mapNodeNameToVMName maps a k8s NodeName to an Azure VM Name | ||||
| // This is a simple string cast. | ||||
| func mapNodeNameToVMName(nodeName types.NodeName) string { | ||||
| 	return string(nodeName) | ||||
| } | ||||
| @@ -1,851 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // setTestVirtualMachines sets test virtual machine with powerstate. | ||||
| func setTestVirtualMachines(c *Cloud, vmList map[string]string, isDataDisksFull bool) []compute.VirtualMachine { | ||||
| 	expectedVMs := make([]compute.VirtualMachine, 0) | ||||
|  | ||||
| 	for nodeName, powerState := range vmList { | ||||
| 		instanceID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/%s", nodeName) | ||||
| 		vm := compute.VirtualMachine{ | ||||
| 			Name:     &nodeName, | ||||
| 			ID:       &instanceID, | ||||
| 			Location: &c.Location, | ||||
| 		} | ||||
| 		status := []compute.InstanceViewStatus{ | ||||
| 			{ | ||||
| 				Code: pointer.String(powerState), | ||||
| 			}, | ||||
| 			{ | ||||
| 				Code: pointer.String("ProvisioningState/succeeded"), | ||||
| 			}, | ||||
| 		} | ||||
| 		vm.VirtualMachineProperties = &compute.VirtualMachineProperties{ | ||||
| 			ProvisioningState: pointer.String(string(compute.ProvisioningStateSucceeded)), | ||||
| 			HardwareProfile: &compute.HardwareProfile{ | ||||
| 				VMSize: compute.VirtualMachineSizeTypesStandardA0, | ||||
| 			}, | ||||
| 			InstanceView: &compute.VirtualMachineInstanceView{ | ||||
| 				Statuses: &status, | ||||
| 			}, | ||||
| 			StorageProfile: &compute.StorageProfile{ | ||||
| 				DataDisks: &[]compute.DataDisk{}, | ||||
| 			}, | ||||
| 		} | ||||
| 		if !isDataDisksFull { | ||||
| 			vm.StorageProfile.DataDisks = &[]compute.DataDisk{{ | ||||
| 				Lun:  pointer.Int32(0), | ||||
| 				Name: pointer.String("disk1"), | ||||
| 			}} | ||||
| 		} else { | ||||
| 			dataDisks := make([]compute.DataDisk, maxLUN) | ||||
| 			for i := 0; i < maxLUN; i++ { | ||||
| 				dataDisks[i] = compute.DataDisk{Lun: pointer.Int32(int32(i))} | ||||
| 			} | ||||
| 			vm.StorageProfile.DataDisks = &dataDisks | ||||
| 		} | ||||
|  | ||||
| 		expectedVMs = append(expectedVMs, vm) | ||||
| 	} | ||||
|  | ||||
| 	return expectedVMs | ||||
| } | ||||
|  | ||||
| func TestInstanceID(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := GetTestCloud(ctrl) | ||||
|  | ||||
| 	testcases := []struct { | ||||
| 		name                string | ||||
| 		vmList              []string | ||||
| 		nodeName            string | ||||
| 		vmssName            string | ||||
| 		metadataName        string | ||||
| 		metadataTemplate    string | ||||
| 		vmType              string | ||||
| 		expectedID          string | ||||
| 		useInstanceMetadata bool | ||||
| 		useCustomImsCache   bool | ||||
| 		nilVMSet            bool | ||||
| 		expectedErrMsg      error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                "InstanceID should get instanceID if node's name are equal to metadataName", | ||||
| 			vmList:              []string{"vm1"}, | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedID:          "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "InstanceID should get vmss instanceID from local if node's name are equal to metadataName and metadata.Compute.VMScaleSetName is not null", | ||||
| 			vmList:              []string{"vmss1_0"}, | ||||
| 			vmssName:            "vmss1", | ||||
| 			nodeName:            "vmss1_0", | ||||
| 			metadataName:        "vmss1_0", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedID:          "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/0", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "InstanceID should get standard instanceID from local if node's name are equal to metadataName and format of nodeName is not compliance with vmss instance", | ||||
| 			vmList:              []string{"vmss1-0"}, | ||||
| 			vmssName:            "vmss1", | ||||
| 			nodeName:            "vmss1-0", | ||||
| 			metadataName:        "vmss1-0", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedID:          "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vmss1-0", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "InstanceID should get instanceID from Azure API if node is not local instance", | ||||
| 			vmList:              []string{"vm2"}, | ||||
| 			nodeName:            "vm2", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedID:          "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "InstanceID should get instanceID from Azure API if cloud.UseInstanceMetadata is false", | ||||
| 			vmList:       []string{"vm2"}, | ||||
| 			nodeName:     "vm2", | ||||
| 			metadataName: "vm2", | ||||
| 			vmType:       vmTypeStandard, | ||||
| 			expectedID:   "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "InstanceID should report error if node doesn't exist", | ||||
| 			vmList:              []string{"vm1"}, | ||||
| 			nodeName:            "vm3", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("instance not found"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "InstanceID should report error if metadata.Compute is nil", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			metadataTemplate:    `{"network":{"interface":[]}}`, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("failure of getting instance metadata"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if cloud.VMSet is nil", | ||||
| 			nodeName:            "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			nilVMSet:            true, | ||||
| 			expectedErrMsg:      fmt.Errorf("no credentials provided for Azure cloud provider"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if invoking GetMetadata returns error", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useCustomImsCache:   true, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("getError"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		if test.nilVMSet { | ||||
| 			cloud.VMSet = nil | ||||
| 		} else { | ||||
| 			cloud.VMSet = newAvailabilitySet(cloud) | ||||
| 		} | ||||
| 		cloud.Config.VMType = test.vmType | ||||
| 		cloud.Config.UseInstanceMetadata = test.useInstanceMetadata | ||||
| 		listener, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		mux := http.NewServeMux() | ||||
| 		mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			if test.metadataTemplate != "" { | ||||
| 				fmt.Fprintf(w, test.metadataTemplate) | ||||
| 			} else { | ||||
| 				fmt.Fprintf(w, "{\"compute\":{\"name\":\"%s\",\"VMScaleSetName\":\"%s\",\"subscriptionId\":\"subscription\",\"resourceGroupName\":\"rg\"}}", test.metadataName, test.vmssName) | ||||
| 			} | ||||
| 		})) | ||||
| 		go func() { | ||||
| 			http.Serve(listener, mux) | ||||
| 		}() | ||||
| 		defer listener.Close() | ||||
|  | ||||
| 		cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
| 		if test.useCustomImsCache { | ||||
| 			cloud.metadata.imsCache, err = azcache.NewTimedcache(metadataCacheTTL, func(key string) (interface{}, error) { | ||||
| 				return nil, fmt.Errorf("getError") | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 			} | ||||
| 		} | ||||
| 		vmListWithPowerState := make(map[string]string) | ||||
| 		for _, vm := range test.vmList { | ||||
| 			vmListWithPowerState[vm] = "" | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(cloud, vmListWithPowerState, false) | ||||
| 		mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm3", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		mockVMsClient.EXPECT().Update(gomock.Any(), cloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		instanceID, err := cloud.InstanceID(context.Background(), types.NodeName(test.nodeName)) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 		assert.Equal(t, test.expectedID, instanceID, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestInstanceShutdownByProviderID(t *testing.T) { | ||||
| 	testcases := []struct { | ||||
| 		name              string | ||||
| 		vmList            map[string]string | ||||
| 		nodeName          string | ||||
| 		providerID        string | ||||
| 		provisioningState string | ||||
| 		expected          bool | ||||
| 		expectedErrMsg    error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if the vm is in PowerState/Running status", | ||||
| 			vmList:     map[string]string{"vm1": "PowerState/Running"}, | ||||
| 			nodeName:   "vm1", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return true if the vm is in PowerState/Deallocated status", | ||||
| 			vmList:     map[string]string{"vm2": "PowerState/Deallocated"}, | ||||
| 			nodeName:   "vm2", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if the vm is in PowerState/Deallocating status", | ||||
| 			vmList:     map[string]string{"vm3": "PowerState/Deallocating"}, | ||||
| 			nodeName:   "vm3", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm3", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if the vm is in PowerState/Starting status", | ||||
| 			vmList:     map[string]string{"vm4": "PowerState/Starting"}, | ||||
| 			nodeName:   "vm4", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm4", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return true if the vm is in PowerState/Stopped status", | ||||
| 			vmList:     map[string]string{"vm5": "PowerState/Stopped"}, | ||||
| 			nodeName:   "vm5", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm5", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
|  | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if the vm is in PowerState/Stopping status", | ||||
| 			vmList:     map[string]string{"vm6": "PowerState/Stopping"}, | ||||
| 			nodeName:   "vm6", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm6", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if the vm is in PowerState/Unknown status", | ||||
| 			vmList:     map[string]string{"vm7": "PowerState/Unknown"}, | ||||
| 			nodeName:   "vm7", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm7", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceShutdownByProviderID should return false if node doesn't exist", | ||||
| 			vmList:     map[string]string{"vm1": "PowerState/running"}, | ||||
| 			nodeName:   "vm8", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm8", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:              "InstanceShutdownByProviderID should return false if the vm is in PowerState/Stopped state with Creating provisioning state", | ||||
| 			vmList:            map[string]string{"vm9": "PowerState/Stopped"}, | ||||
| 			nodeName:          "vm9", | ||||
| 			provisioningState: "Creating", | ||||
| 			providerID:        "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm9", | ||||
| 			expected:          false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "InstanceShutdownByProviderID should report error if providerID is null", | ||||
| 			nodeName: "vmm", | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "InstanceShutdownByProviderID should report error if providerID is invalid", | ||||
| 			providerID:     "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/VM/vm10", | ||||
| 			nodeName:       "vm10", | ||||
| 			expected:       false, | ||||
| 			expectedErrMsg: fmt.Errorf("error splitting providerID"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	for _, test := range testcases { | ||||
| 		cloud := GetTestCloud(ctrl) | ||||
| 		expectedVMs := setTestVirtualMachines(cloud, test.vmList, false) | ||||
| 		if test.provisioningState != "" { | ||||
| 			expectedVMs[0].ProvisioningState = pointer.String(test.provisioningState) | ||||
| 		} | ||||
| 		mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, test.nodeName, gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 		hasShutdown, err := cloud.InstanceShutdownByProviderID(context.Background(), test.providerID) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 		assert.Equal(t, test.expected, hasShutdown, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNodeAddresses(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	cloud := GetTestCloud(ctrl) | ||||
|  | ||||
| 	expectedVM := compute.VirtualMachine{ | ||||
| 		VirtualMachineProperties: &compute.VirtualMachineProperties{ | ||||
| 			NetworkProfile: &compute.NetworkProfile{ | ||||
| 				NetworkInterfaces: &[]compute.NetworkInterfaceReference{ | ||||
| 					{ | ||||
| 						NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{ | ||||
| 							Primary: pointer.Bool(true), | ||||
| 						}, | ||||
| 						ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedPIP := network.PublicIPAddress{ | ||||
| 		ID: pointer.String("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip1"), | ||||
| 		PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ | ||||
| 			IPAddress: pointer.String("192.168.1.12"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedInterface := network.Interface{ | ||||
| 		InterfacePropertiesFormat: &network.InterfacePropertiesFormat{ | ||||
| 			IPConfigurations: &[]network.InterfaceIPConfiguration{ | ||||
| 				{ | ||||
| 					InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ | ||||
| 						PrivateIPAddress: pointer.String("172.1.0.3"), | ||||
| 						PublicIPAddress:  &expectedPIP, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	expectedNodeAddress := []v1.NodeAddress{ | ||||
| 		{ | ||||
| 			Type:    v1.NodeInternalIP, | ||||
| 			Address: "172.1.0.3", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Type:    v1.NodeHostName, | ||||
| 			Address: "vm1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Type:    v1.NodeExternalIP, | ||||
| 			Address: "192.168.1.12", | ||||
| 		}, | ||||
| 	} | ||||
| 	metadataTemplate := `{"compute":{"name":"%s"},"network":{"interface":[{"ipv4":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]},"ipv6":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]}}]}}` | ||||
| 	loadbalancerTemplate := `{"loadbalancer": {"publicIpAddresses": [{"frontendIpAddress": "%s","privateIpAddress": "%s"},{"frontendIpAddress": "%s","privateIpAddress": "%s"}]}}` | ||||
| 	testcases := []struct { | ||||
| 		name                string | ||||
| 		nodeName            string | ||||
| 		metadataName        string | ||||
| 		metadataTemplate    string | ||||
| 		vmType              string | ||||
| 		ipV4                string | ||||
| 		ipV6                string | ||||
| 		ipV4Public          string | ||||
| 		ipV6Public          string | ||||
| 		loadBalancerSku     string | ||||
| 		expectedAddress     []v1.NodeAddress | ||||
| 		useInstanceMetadata bool | ||||
| 		useCustomImsCache   bool | ||||
| 		nilVMSet            bool | ||||
| 		expectedErrMsg      error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if metadata.Network is nil", | ||||
| 			metadataTemplate:    `{"compute":{"name":"vm1"}}`, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("failure of getting instance metadata"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if metadata.Compute is nil", | ||||
| 			metadataTemplate:    `{"network":{"interface":[]}}`, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("failure of getting instance metadata"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if metadata.Network.Interface is nil", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			metadataTemplate:    `{"compute":{"name":"vm1"},"network":{}}`, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("no interface is found for the instance"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error when invoke GetMetadata", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useCustomImsCache:   true, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("getError"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if cloud.VMSet is nil", | ||||
| 			nodeName:            "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			nilVMSet:            true, | ||||
| 			expectedErrMsg:      fmt.Errorf("no credentials provided for Azure cloud provider"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error when IPs are empty", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      fmt.Errorf("get empty IP addresses from instance metadata service"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should report error if node don't exist", | ||||
| 			nodeName:            "vm2", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedErrMsg:      wait.ErrWaitTimeout, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should get IP addresses from Azure API if node's name isn't equal to metadataName", | ||||
| 			nodeName:            "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedAddress:     expectedNodeAddress, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:            "NodeAddresses should get IP addresses from Azure API if useInstanceMetadata is false", | ||||
| 			nodeName:        "vm1", | ||||
| 			vmType:          vmTypeStandard, | ||||
| 			expectedAddress: expectedNodeAddress, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should get IP addresses from local IMDS if node's name is equal to metadataName", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			ipV4:                "10.240.0.1", | ||||
| 			ipV4Public:          "192.168.1.12", | ||||
| 			ipV6:                "1111:11111:00:00:1111:1111:000:111", | ||||
| 			ipV6Public:          "2222:22221:00:00:2222:2222:000:111", | ||||
| 			loadBalancerSku:     "basic", | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedAddress: []v1.NodeAddress{ | ||||
| 				{ | ||||
| 					Type:    v1.NodeHostName, | ||||
| 					Address: "vm1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "10.240.0.1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeExternalIP, | ||||
| 					Address: "192.168.1.12", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "1111:11111:00:00:1111:1111:000:111", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeExternalIP, | ||||
| 					Address: "2222:22221:00:00:2222:2222:000:111", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                "NodeAddresses should get IP addresses from local IMDS for standard LoadBalancer if node's name is equal to metadataName", | ||||
| 			nodeName:            "vm1", | ||||
| 			metadataName:        "vm1", | ||||
| 			vmType:              vmTypeStandard, | ||||
| 			ipV4:                "10.240.0.1", | ||||
| 			ipV4Public:          "192.168.1.12", | ||||
| 			ipV6:                "1111:11111:00:00:1111:1111:000:111", | ||||
| 			ipV6Public:          "2222:22221:00:00:2222:2222:000:111", | ||||
| 			loadBalancerSku:     "standard", | ||||
| 			useInstanceMetadata: true, | ||||
| 			expectedAddress: []v1.NodeAddress{ | ||||
| 				{ | ||||
| 					Type:    v1.NodeHostName, | ||||
| 					Address: "vm1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "10.240.0.1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeExternalIP, | ||||
| 					Address: "192.168.1.12", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "1111:11111:00:00:1111:1111:000:111", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeExternalIP, | ||||
| 					Address: "2222:22221:00:00:2222:2222:000:111", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		if test.nilVMSet { | ||||
| 			cloud.VMSet = nil | ||||
| 		} else { | ||||
| 			cloud.VMSet = newAvailabilitySet(cloud) | ||||
| 		} | ||||
| 		cloud.Config.VMType = test.vmType | ||||
| 		cloud.Config.UseInstanceMetadata = test.useInstanceMetadata | ||||
| 		listener, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		mux := http.NewServeMux() | ||||
| 		mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			if strings.Contains(r.RequestURI, imdsLoadBalancerURI) { | ||||
| 				fmt.Fprintf(w, loadbalancerTemplate, test.ipV4Public, test.ipV4, test.ipV6Public, test.ipV6) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if test.metadataTemplate != "" { | ||||
| 				fmt.Fprintf(w, test.metadataTemplate) | ||||
| 			} else { | ||||
| 				if test.loadBalancerSku == "standard" { | ||||
| 					fmt.Fprintf(w, metadataTemplate, test.metadataName, test.ipV4, "", test.ipV6, "") | ||||
| 				} else { | ||||
| 					fmt.Fprintf(w, metadataTemplate, test.metadataName, test.ipV4, test.ipV4Public, test.ipV6, test.ipV6Public) | ||||
| 				} | ||||
| 			} | ||||
| 		})) | ||||
| 		go func() { | ||||
| 			http.Serve(listener, mux) | ||||
| 		}() | ||||
| 		defer listener.Close() | ||||
|  | ||||
| 		cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		if test.useCustomImsCache { | ||||
| 			cloud.metadata.imsCache, err = azcache.NewTimedcache(metadataCacheTTL, func(key string) (interface{}, error) { | ||||
| 				return nil, fmt.Errorf("getError") | ||||
| 			}) | ||||
| 			if err != nil { | ||||
| 				t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 			} | ||||
| 		} | ||||
| 		mockVMClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		mockVMClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm1", gomock.Any()).Return(expectedVM, nil).AnyTimes() | ||||
| 		mockVMClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
|  | ||||
| 		mockPublicIPAddressesClient := cloud.PublicIPAddressesClient.(*mockpublicipclient.MockInterface) | ||||
| 		mockPublicIPAddressesClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "pip1", gomock.Any()).Return(expectedPIP, nil).AnyTimes() | ||||
|  | ||||
| 		mockInterfaceClient := cloud.InterfacesClient.(*mockinterfaceclient.MockInterface) | ||||
| 		mockInterfaceClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).AnyTimes() | ||||
|  | ||||
| 		ipAddresses, err := cloud.NodeAddresses(context.Background(), types.NodeName(test.nodeName)) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 		assert.Equal(t, test.expectedAddress, ipAddresses, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestInstanceExistsByProviderID(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	cloud := GetTestCloud(ctrl) | ||||
|  | ||||
| 	testcases := []struct { | ||||
| 		name           string | ||||
| 		vmList         []string | ||||
| 		nodeName       string | ||||
| 		providerID     string | ||||
| 		expected       bool | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return true if node exists", | ||||
| 			vmList:     []string{"vm2"}, | ||||
| 			nodeName:   "vm2", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return true if node is unmanaged", | ||||
| 			providerID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return false if node doesn't exist", | ||||
| 			vmList:     []string{"vm1"}, | ||||
| 			nodeName:   "vm3", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm3", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "InstanceExistsByProviderID should report error if providerID is invalid", | ||||
| 			providerID:     "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachine/vm3", | ||||
| 			expected:       false, | ||||
| 			expectedErrMsg: fmt.Errorf("error splitting providerID"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "InstanceExistsByProviderID should report error if providerID is null", | ||||
| 			expected:       false, | ||||
| 			expectedErrMsg: fmt.Errorf("providerID is empty, the node is not initialized yet"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		vmListWithPowerState := make(map[string]string) | ||||
| 		for _, vm := range test.vmList { | ||||
| 			vmListWithPowerState[vm] = "" | ||||
| 		} | ||||
| 		expectedVMs := setTestVirtualMachines(cloud, vmListWithPowerState, false) | ||||
| 		mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		for _, vm := range expectedVMs { | ||||
| 			mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm3", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes() | ||||
| 		mockVMsClient.EXPECT().Update(gomock.Any(), cloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		exist, err := cloud.InstanceExistsByProviderID(context.Background(), test.providerID) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 		assert.Equal(t, test.expected, exist, test.name) | ||||
| 	} | ||||
|  | ||||
| 	vmssTestCases := []struct { | ||||
| 		name       string | ||||
| 		providerID string | ||||
| 		scaleSet   string | ||||
| 		vmList     []string | ||||
| 		expected   bool | ||||
| 		rerr       *retry.Error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return true if VMSS and VM exist", | ||||
| 			providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmssee6c2/virtualMachines/0", | ||||
| 			scaleSet:   "vmssee6c2", | ||||
| 			vmList:     []string{"vmssee6c2000000"}, | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return false if VMSS exist but VM doesn't", | ||||
| 			providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmssee6c2/virtualMachines/0", | ||||
| 			scaleSet:   "vmssee6c2", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "InstanceExistsByProviderID should return false if VMSS doesn't exist", | ||||
| 			providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/missing-vmss/virtualMachines/0", | ||||
| 			rerr:       &retry.Error{HTTPStatusCode: 404}, | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range vmssTestCases { | ||||
| 		ss, err := newTestScaleSet(ctrl) | ||||
| 		assert.NoError(t, err, test.name) | ||||
| 		cloud.VMSet = ss | ||||
|  | ||||
| 		mockVMSSClient := mockvmssclient.NewMockInterface(ctrl) | ||||
| 		mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl) | ||||
| 		ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient | ||||
| 		ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient | ||||
|  | ||||
| 		expectedScaleSet := buildTestVMSS(test.scaleSet, test.scaleSet) | ||||
| 		mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, test.rerr).AnyTimes() | ||||
|  | ||||
| 		expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, test.scaleSet, "", 0, test.vmList, "succeeded", false) | ||||
| 		mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, test.rerr).AnyTimes() | ||||
|  | ||||
| 		mockVMsClient := ss.cloud.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 		mockVMsClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachine{}, nil).AnyTimes() | ||||
|  | ||||
| 		exist, _ := cloud.InstanceExistsByProviderID(context.Background(), test.providerID) | ||||
| 		assert.Equal(t, test.expected, exist, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNodeAddressesByProviderID(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	cloud := GetTestCloud(ctrl) | ||||
| 	cloud.Config.UseInstanceMetadata = true | ||||
| 	metadataTemplate := `{"compute":{"name":"%s"},"network":{"interface":[{"ipv4":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]},"ipv6":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]}}]}}` | ||||
|  | ||||
| 	testcases := []struct { | ||||
| 		name            string | ||||
| 		nodeName        string | ||||
| 		ipV4            string | ||||
| 		ipV6            string | ||||
| 		ipV4Public      string | ||||
| 		ipV6Public      string | ||||
| 		providerID      string | ||||
| 		expectedAddress []v1.NodeAddress | ||||
| 		expectedErrMsg  error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "NodeAddressesByProviderID should get both ipV4 and ipV6 private addresses", | ||||
| 			nodeName:   "vm1", | ||||
| 			providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 			ipV4:       "10.240.0.1", | ||||
| 			ipV6:       "1111:11111:00:00:1111:1111:000:111", | ||||
| 			expectedAddress: []v1.NodeAddress{ | ||||
| 				{ | ||||
| 					Type:    v1.NodeHostName, | ||||
| 					Address: "vm1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "10.240.0.1", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Type:    v1.NodeInternalIP, | ||||
| 					Address: "1111:11111:00:00:1111:1111:000:111", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "NodeAddressesByProviderID should report error when IPs are empty", | ||||
| 			nodeName:       "vm1", | ||||
| 			providerID:     "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 			expectedErrMsg: fmt.Errorf("get empty IP addresses from instance metadata service"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "NodeAddressesByProviderID should return nil if node is unmanaged", | ||||
| 			providerID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "NodeAddressesByProviderID should report error if providerID is invalid", | ||||
| 			providerID:     "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachine/vm3", | ||||
| 			expectedErrMsg: fmt.Errorf("error splitting providerID"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "NodeAddressesByProviderID should report error if providerID is null", | ||||
| 			expectedErrMsg: fmt.Errorf("providerID is empty, the node is not initialized yet"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		listener, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		mux := http.NewServeMux() | ||||
| 		mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			fmt.Fprintf(w, metadataTemplate, test.nodeName, test.ipV4, test.ipV4Public, test.ipV6, test.ipV6Public) | ||||
| 		})) | ||||
| 		go func() { | ||||
| 			http.Serve(listener, mux) | ||||
| 		}() | ||||
| 		defer listener.Close() | ||||
|  | ||||
| 		cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		ipAddresses, err := cloud.NodeAddressesByProviderID(context.Background(), test.providerID) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 		assert.Equal(t, test.expectedAddress, ipAddresses, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCurrentNodeName(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	cloud := GetTestCloud(ctrl) | ||||
|  | ||||
| 	hostname := "testvm" | ||||
| 	nodeName, err := cloud.CurrentNodeName(context.Background(), hostname) | ||||
| 	assert.Equal(t, types.NodeName(hostname), nodeName) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,399 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	kwait "k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudvolume "k8s.io/cloud-provider/volume" | ||||
| 	volumehelpers "k8s.io/cloud-provider/volume/helpers" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// default IOPS Caps & Throughput Cap (MBps) per https://docs.microsoft.com/en-us/azure/virtual-machines/linux/disks-ultra-ssd | ||||
| 	defaultDiskIOPSReadWrite = 500 | ||||
| 	defaultDiskMBpsReadWrite = 100 | ||||
|  | ||||
| 	diskEncryptionSetIDFormat = "/subscriptions/{subs-id}/resourceGroups/{rg-name}/providers/Microsoft.Compute/diskEncryptionSets/{diskEncryptionSet-name}" | ||||
| ) | ||||
|  | ||||
| // ManagedDiskController : managed disk controller struct | ||||
| type ManagedDiskController struct { | ||||
| 	common *controllerCommon | ||||
| } | ||||
|  | ||||
| // ManagedDiskOptions specifies the options of managed disks. | ||||
| type ManagedDiskOptions struct { | ||||
| 	// The name of the disk. | ||||
| 	DiskName string | ||||
| 	// The size in GB. | ||||
| 	SizeGB int | ||||
| 	// The name of PVC. | ||||
| 	PVCName string | ||||
| 	// The name of resource group. | ||||
| 	ResourceGroup string | ||||
| 	// The AvailabilityZone to create the disk. | ||||
| 	AvailabilityZone string | ||||
| 	// The tags of the disk. | ||||
| 	Tags map[string]string | ||||
| 	// The SKU of storage account. | ||||
| 	StorageAccountType compute.DiskStorageAccountTypes | ||||
| 	// IOPS Caps for UltraSSD disk | ||||
| 	DiskIOPSReadWrite string | ||||
| 	// Throughput Cap (MBps) for UltraSSD disk | ||||
| 	DiskMBpsReadWrite string | ||||
| 	// if SourceResourceID is not empty, then it's a disk copy operation(for snapshot) | ||||
| 	SourceResourceID string | ||||
| 	// The type of source | ||||
| 	SourceType string | ||||
| 	// ResourceId of the disk encryption set to use for enabling encryption at rest. | ||||
| 	DiskEncryptionSetID string | ||||
| 	// The maximum number of VMs that can attach to the disk at the same time. Value greater than one indicates a disk that can be mounted on multiple VMs at the same time. | ||||
| 	MaxShares int32 | ||||
| } | ||||
|  | ||||
| // CreateManagedDisk : create managed disk | ||||
| func (c *ManagedDiskController) CreateManagedDisk(options *ManagedDiskOptions) (string, error) { | ||||
| 	var err error | ||||
| 	klog.V(4).Infof("azureDisk - creating new managed Name:%s StorageAccountType:%s Size:%v", options.DiskName, options.StorageAccountType, options.SizeGB) | ||||
|  | ||||
| 	var createZones []string | ||||
| 	if len(options.AvailabilityZone) > 0 { | ||||
| 		requestedZone := c.common.cloud.GetZoneID(options.AvailabilityZone) | ||||
| 		if requestedZone != "" { | ||||
| 			createZones = append(createZones, requestedZone) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// insert original tags to newTags | ||||
| 	newTags := make(map[string]*string) | ||||
| 	azureDDTag := "kubernetes-azure-dd" | ||||
| 	newTags["created-by"] = &azureDDTag | ||||
| 	if options.Tags != nil { | ||||
| 		for k, v := range options.Tags { | ||||
| 			// Azure won't allow / (forward slash) in tags | ||||
| 			newKey := strings.Replace(k, "/", "-", -1) | ||||
| 			newValue := strings.Replace(v, "/", "-", -1) | ||||
| 			newTags[newKey] = &newValue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	diskSizeGB := int32(options.SizeGB) | ||||
| 	diskSku := compute.DiskStorageAccountTypes(options.StorageAccountType) | ||||
|  | ||||
| 	creationData, err := getValidCreationData(c.common.subscriptionID, options.ResourceGroup, options.SourceResourceID, options.SourceType) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	diskProperties := compute.DiskProperties{ | ||||
| 		DiskSizeGB:   &diskSizeGB, | ||||
| 		CreationData: &creationData, | ||||
| 	} | ||||
|  | ||||
| 	if diskSku == compute.UltraSSDLRS { | ||||
| 		diskIOPSReadWrite := int64(defaultDiskIOPSReadWrite) | ||||
| 		if options.DiskIOPSReadWrite != "" { | ||||
| 			v, err := strconv.Atoi(options.DiskIOPSReadWrite) | ||||
| 			if err != nil { | ||||
| 				return "", fmt.Errorf("AzureDisk - failed to parse DiskIOPSReadWrite: %v", err) | ||||
| 			} | ||||
| 			diskIOPSReadWrite = int64(v) | ||||
| 		} | ||||
| 		diskProperties.DiskIOPSReadWrite = pointer.Int64(diskIOPSReadWrite) | ||||
|  | ||||
| 		diskMBpsReadWrite := int64(defaultDiskMBpsReadWrite) | ||||
| 		if options.DiskMBpsReadWrite != "" { | ||||
| 			v, err := strconv.Atoi(options.DiskMBpsReadWrite) | ||||
| 			if err != nil { | ||||
| 				return "", fmt.Errorf("AzureDisk - failed to parse DiskMBpsReadWrite: %v", err) | ||||
| 			} | ||||
| 			diskMBpsReadWrite = int64(v) | ||||
| 		} | ||||
| 		diskProperties.DiskMBpsReadWrite = pointer.Int64(diskMBpsReadWrite) | ||||
| 	} else { | ||||
| 		if options.DiskIOPSReadWrite != "" { | ||||
| 			return "", fmt.Errorf("AzureDisk - DiskIOPSReadWrite parameter is only applicable in UltraSSD_LRS disk type") | ||||
| 		} | ||||
| 		if options.DiskMBpsReadWrite != "" { | ||||
| 			return "", fmt.Errorf("AzureDisk - DiskMBpsReadWrite parameter is only applicable in UltraSSD_LRS disk type") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if options.DiskEncryptionSetID != "" { | ||||
| 		if strings.Index(strings.ToLower(options.DiskEncryptionSetID), "/subscriptions/") != 0 { | ||||
| 			return "", fmt.Errorf("AzureDisk - format of DiskEncryptionSetID(%s) is incorrect, correct format: %s", options.DiskEncryptionSetID, diskEncryptionSetIDFormat) | ||||
| 		} | ||||
| 		diskProperties.Encryption = &compute.Encryption{ | ||||
| 			DiskEncryptionSetID: &options.DiskEncryptionSetID, | ||||
| 			Type:                compute.EncryptionAtRestWithCustomerKey, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if options.MaxShares > 1 { | ||||
| 		diskProperties.MaxShares = &options.MaxShares | ||||
| 	} | ||||
|  | ||||
| 	model := compute.Disk{ | ||||
| 		Location: &c.common.location, | ||||
| 		Tags:     newTags, | ||||
| 		Sku: &compute.DiskSku{ | ||||
| 			Name: diskSku, | ||||
| 		}, | ||||
| 		DiskProperties: &diskProperties, | ||||
| 	} | ||||
|  | ||||
| 	if len(createZones) > 0 { | ||||
| 		model.Zones = &createZones | ||||
| 	} | ||||
|  | ||||
| 	if options.ResourceGroup == "" { | ||||
| 		options.ResourceGroup = c.common.resourceGroup | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	rerr := c.common.cloud.DisksClient.CreateOrUpdate(ctx, options.ResourceGroup, options.DiskName, model) | ||||
| 	if rerr != nil { | ||||
| 		return "", rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	diskID := "" | ||||
|  | ||||
| 	err = kwait.ExponentialBackoff(defaultBackOff, func() (bool, error) { | ||||
| 		provisionState, id, err := c.GetDisk(options.ResourceGroup, options.DiskName) | ||||
| 		diskID = id | ||||
| 		// We are waiting for provisioningState==Succeeded | ||||
| 		// We don't want to hand-off managed disks to k8s while they are | ||||
| 		//still being provisioned, this is to avoid some race conditions | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 		if strings.ToLower(provisionState) == "succeeded" { | ||||
| 			return true, nil | ||||
| 		} | ||||
| 		return false, nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		klog.V(2).Infof("azureDisk - created new MD Name:%s StorageAccountType:%s Size:%v but was unable to confirm provisioningState in poll process", options.DiskName, options.StorageAccountType, options.SizeGB) | ||||
| 	} else { | ||||
| 		klog.V(2).Infof("azureDisk - created new MD Name:%s StorageAccountType:%s Size:%v", options.DiskName, options.StorageAccountType, options.SizeGB) | ||||
| 	} | ||||
|  | ||||
| 	return diskID, nil | ||||
| } | ||||
|  | ||||
| // DeleteManagedDisk : delete managed disk | ||||
| func (c *ManagedDiskController) DeleteManagedDisk(diskURI string) error { | ||||
| 	diskName := path.Base(diskURI) | ||||
| 	resourceGroup, err := getResourceGroupFromDiskURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	if _, ok := c.common.diskAttachDetachMap.Load(strings.ToLower(diskURI)); ok { | ||||
| 		return fmt.Errorf("failed to delete disk(%s) since it's in attaching or detaching state", diskURI) | ||||
| 	} | ||||
|  | ||||
| 	disk, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.V(2).Infof("azureDisk - disk(%s) is already deleted", diskURI) | ||||
| 			return nil | ||||
| 		} | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	if disk.ManagedBy != nil { | ||||
| 		return fmt.Errorf("disk(%s) already attached to node(%s), could not be deleted", diskURI, *disk.ManagedBy) | ||||
| 	} | ||||
|  | ||||
| 	rerr = c.common.cloud.DisksClient.Delete(ctx, resourceGroup, diskName) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.HTTPStatusCode == http.StatusNotFound { | ||||
| 			klog.V(2).Infof("azureDisk - disk(%s) is already deleted", diskURI) | ||||
| 			return nil | ||||
| 		} | ||||
| 		return rerr.Error() | ||||
| 	} | ||||
| 	// We don't need poll here, k8s will immediately stop referencing the disk | ||||
| 	// the disk will be eventually deleted - cleanly - by ARM | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - deleted a managed disk: %s", diskURI) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetDisk return: disk provisionState, diskID, error | ||||
| func (c *ManagedDiskController) GetDisk(resourceGroup, diskName string) (string, string, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	result, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName) | ||||
| 	if rerr != nil { | ||||
| 		return "", "", rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	if result.DiskProperties != nil && (*result.DiskProperties).ProvisioningState != nil { | ||||
| 		return *(*result.DiskProperties).ProvisioningState, *result.ID, nil | ||||
| 	} | ||||
|  | ||||
| 	return "", "", nil | ||||
| } | ||||
|  | ||||
| // ResizeDisk Expand the disk to new size | ||||
| func (c *ManagedDiskController) ResizeDisk(diskURI string, oldSize resource.Quantity, newSize resource.Quantity) (resource.Quantity, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	diskName := path.Base(diskURI) | ||||
| 	resourceGroup, err := getResourceGroupFromDiskURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		return oldSize, err | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName) | ||||
| 	if rerr != nil { | ||||
| 		return oldSize, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	if result.DiskProperties == nil || result.DiskProperties.DiskSizeGB == nil { | ||||
| 		return oldSize, fmt.Errorf("DiskProperties of disk(%s) is nil", diskName) | ||||
| 	} | ||||
|  | ||||
| 	// Azure resizes in chunks of GiB (not GB) | ||||
| 	requestGiB, err := volumehelpers.RoundUpToGiBInt32(newSize) | ||||
| 	if err != nil { | ||||
| 		return oldSize, err | ||||
| 	} | ||||
|  | ||||
| 	newSizeQuant := resource.MustParse(fmt.Sprintf("%dGi", requestGiB)) | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - begin to resize disk(%s) with new size(%d), old size(%v)", diskName, requestGiB, oldSize) | ||||
| 	// If disk already of greater or equal size than requested we return | ||||
| 	if *result.DiskProperties.DiskSizeGB >= requestGiB { | ||||
| 		return newSizeQuant, nil | ||||
| 	} | ||||
|  | ||||
| 	if result.DiskProperties.DiskState != compute.Unattached { | ||||
| 		return oldSize, fmt.Errorf("azureDisk - disk resize is only supported on Unattached disk, current disk state: %s, already attached to %s", result.DiskProperties.DiskState, pointer.StringDeref(result.ManagedBy, "")) | ||||
| 	} | ||||
|  | ||||
| 	diskParameter := compute.DiskUpdate{ | ||||
| 		DiskUpdateProperties: &compute.DiskUpdateProperties{ | ||||
| 			DiskSizeGB: &requestGiB, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel = getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	if rerr := c.common.cloud.DisksClient.Update(ctx, resourceGroup, diskName, diskParameter); rerr != nil { | ||||
| 		return oldSize, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("azureDisk - resize disk(%s) with new size(%d) completed", diskName, requestGiB) | ||||
|  | ||||
| 	return newSizeQuant, nil | ||||
| } | ||||
|  | ||||
| // get resource group name from a managed disk URI, e.g. return {group-name} according to | ||||
| // /subscriptions/{sub-id}/resourcegroups/{group-name}/providers/microsoft.compute/disks/{disk-id} | ||||
| // according to https://docs.microsoft.com/en-us/rest/api/compute/disks/get | ||||
| func getResourceGroupFromDiskURI(diskURI string) (string, error) { | ||||
| 	fields := strings.Split(diskURI, "/") | ||||
| 	if len(fields) != 9 || strings.ToLower(fields[3]) != "resourcegroups" { | ||||
| 		return "", fmt.Errorf("invalid disk URI: %s", diskURI) | ||||
| 	} | ||||
| 	return fields[4], nil | ||||
| } | ||||
|  | ||||
| // GetLabelsForVolume implements PVLabeler.GetLabelsForVolume | ||||
| func (c *Cloud) GetLabelsForVolume(ctx context.Context, pv *v1.PersistentVolume) (map[string]string, error) { | ||||
| 	// Ignore if not AzureDisk. | ||||
| 	if pv.Spec.AzureDisk == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	// Ignore any volumes that are being provisioned | ||||
| 	if pv.Spec.AzureDisk.DiskName == cloudvolume.ProvisionedVolumeName { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return c.GetAzureDiskLabels(pv.Spec.AzureDisk.DataDiskURI) | ||||
| } | ||||
|  | ||||
| // GetAzureDiskLabels gets availability zone labels for Azuredisk. | ||||
| func (c *Cloud) GetAzureDiskLabels(diskURI string) (map[string]string, error) { | ||||
| 	// Get disk's resource group. | ||||
| 	diskName := path.Base(diskURI) | ||||
| 	resourceGroup, err := getResourceGroupFromDiskURI(diskURI) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("Failed to get resource group for AzureDisk %q: %v", diskName, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	labels := map[string]string{ | ||||
| 		v1.LabelTopologyRegion: c.Location, | ||||
| 	} | ||||
| 	// no azure credential is set, return nil | ||||
| 	if c.DisksClient == nil { | ||||
| 		return labels, nil | ||||
| 	} | ||||
| 	// Get information of the disk. | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	disk, rerr := c.DisksClient.Get(ctx, resourceGroup, diskName) | ||||
| 	if rerr != nil { | ||||
| 		klog.Errorf("Failed to get information for AzureDisk %q: %v", diskName, rerr) | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	// Check whether availability zone is specified. | ||||
| 	if disk.Zones == nil || len(*disk.Zones) == 0 { | ||||
| 		klog.V(4).Infof("Azure disk %q is not zoned", diskName) | ||||
| 		return labels, nil | ||||
| 	} | ||||
|  | ||||
| 	zones := *disk.Zones | ||||
| 	zoneID, err := strconv.Atoi(zones[0]) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to parse zone %v for AzureDisk %v: %v", zones, diskName, err) | ||||
| 	} | ||||
|  | ||||
| 	zone := c.makeZone(c.Location, zoneID) | ||||
| 	klog.V(4).Infof("Got zone %q for Azure disk %q", zone, diskName) | ||||
| 	labels[v1.LabelTopologyZone] = zone | ||||
| 	return labels, nil | ||||
| } | ||||
| @@ -1,553 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	cloudvolume "k8s.io/cloud-provider/volume" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestCreateManagedDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	maxShare := int32(2) | ||||
| 	goodDiskEncryptionSetID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/%s", "diskEncryptionSet-name") | ||||
| 	badDiskEncryptionSetID := fmt.Sprintf("badDiskEncryptionSetID") | ||||
| 	testTags := make(map[string]*string) | ||||
| 	testTags[WriteAcceleratorEnabled] = pointer.String("true") | ||||
| 	testCases := []struct { | ||||
| 		desc                string | ||||
| 		diskID              string | ||||
| 		diskName            string | ||||
| 		storageAccountType  compute.DiskStorageAccountTypes | ||||
| 		diskIOPSReadWrite   string | ||||
| 		diskMBPSReadWrite   string | ||||
| 		diskEncryptionSetID string | ||||
| 		expectedDiskID      string | ||||
| 		existedDisk         compute.Disk | ||||
| 		expectedErr         bool | ||||
| 		expectedErrMsg      error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:                "disk Id and no error shall be returned if everything is good with UltraSSDLRS storage account", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.UltraSSDLRS, | ||||
| 			diskIOPSReadWrite:   "100", | ||||
| 			diskMBPSReadWrite:   "100", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "diskid1", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "disk Id and no error shall be returned if everything is good with StandardLRS storage account", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.StandardLRS, | ||||
| 			diskIOPSReadWrite:   "", | ||||
| 			diskMBPSReadWrite:   "", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "diskid1", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account but DiskIOPSReadWrite is invalid", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.UltraSSDLRS, | ||||
| 			diskIOPSReadWrite:   "invalid", | ||||
| 			diskMBPSReadWrite:   "100", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         true, | ||||
| 			expectedErrMsg:      fmt.Errorf("AzureDisk - failed to parse DiskIOPSReadWrite: strconv.Atoi: parsing \"invalid\": invalid syntax"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account but DiskMBPSReadWrite is invalid", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.UltraSSDLRS, | ||||
| 			diskIOPSReadWrite:   "100", | ||||
| 			diskMBPSReadWrite:   "invalid", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         true, | ||||
| 			expectedErrMsg:      fmt.Errorf("AzureDisk - failed to parse DiskMBpsReadWrite: strconv.Atoi: parsing \"invalid\": invalid syntax"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account with bad Disk EncryptionSetID", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.UltraSSDLRS, | ||||
| 			diskIOPSReadWrite:   "100", | ||||
| 			diskMBPSReadWrite:   "100", | ||||
| 			diskEncryptionSetID: badDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         true, | ||||
| 			expectedErrMsg:      fmt.Errorf("AzureDisk - format of DiskEncryptionSetID(%s) is incorrect, correct format: %s", badDiskEncryptionSetID, diskEncryptionSetIDFormat), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "disk Id and no error shall be returned if everything is good with StandardLRS storage account with not empty diskIOPSReadWrite", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.StandardLRS, | ||||
| 			diskIOPSReadWrite:   "100", | ||||
| 			diskMBPSReadWrite:   "", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         true, | ||||
| 			expectedErrMsg:      fmt.Errorf("AzureDisk - DiskIOPSReadWrite parameter is only applicable in UltraSSD_LRS disk type"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                "disk Id and no error shall be returned if everything is good with StandardLRS storage account with not empty diskMBPSReadWrite", | ||||
| 			diskID:              "diskid1", | ||||
| 			diskName:            "disk1", | ||||
| 			storageAccountType:  compute.StandardLRS, | ||||
| 			diskIOPSReadWrite:   "", | ||||
| 			diskMBPSReadWrite:   "100", | ||||
| 			diskEncryptionSetID: goodDiskEncryptionSetID, | ||||
| 			expectedDiskID:      "", | ||||
| 			existedDisk:         compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags}, | ||||
| 			expectedErr:         true, | ||||
| 			expectedErrMsg:      fmt.Errorf("AzureDisk - DiskMBpsReadWrite parameter is only applicable in UltraSSD_LRS disk type"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		managedDiskController := testCloud.ManagedDiskController | ||||
| 		volumeOptions := &ManagedDiskOptions{ | ||||
| 			DiskName:            test.diskName, | ||||
| 			StorageAccountType:  test.storageAccountType, | ||||
| 			ResourceGroup:       "", | ||||
| 			SizeGB:              1, | ||||
| 			Tags:                map[string]string{"tag1": "azure-tag1"}, | ||||
| 			AvailabilityZone:    "westus-testzone", | ||||
| 			DiskIOPSReadWrite:   test.diskIOPSReadWrite, | ||||
| 			DiskMBpsReadWrite:   test.diskMBPSReadWrite, | ||||
| 			DiskEncryptionSetID: test.diskEncryptionSetID, | ||||
| 			MaxShares:           maxShare, | ||||
| 		} | ||||
|  | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		//disk := getTestDisk(test.diskName) | ||||
| 		mockDisksClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes() | ||||
| 		mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes() | ||||
|  | ||||
| 		actualDiskID, err := managedDiskController.CreateManagedDisk(volumeOptions) | ||||
| 		assert.Equal(t, test.expectedDiskID, actualDiskID, "TestCase[%d]: %s", i, test.desc) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDeleteManagedDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	fakeGetDiskFailed := "fakeGetDiskFailed" | ||||
| 	testCases := []struct { | ||||
| 		desc           string | ||||
| 		diskName       string | ||||
| 		diskState      string | ||||
| 		existedDisk    compute.Disk | ||||
| 		expectedErr    bool | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if delete an attaching disk", | ||||
| 			diskName:       "disk1", | ||||
| 			diskState:      "attaching", | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String("disk1")}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("failed to delete disk(/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1) since it's in attaching or detaching state"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "no error shall be returned if everything is good", | ||||
| 			diskName:    "disk1", | ||||
| 			existedDisk: compute.Disk{Name: pointer.String("disk1")}, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:           "an error shall be returned if get disk failed", | ||||
| 			diskName:       fakeGetDiskFailed, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(fakeGetDiskFailed)}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		managedDiskController := testCloud.ManagedDiskController | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
| 		if test.diskState == "attaching" { | ||||
| 			managedDiskController.common.diskAttachDetachMap.Store(strings.ToLower(diskURI), test.diskState) | ||||
| 		} | ||||
|  | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		if test.diskName == fakeGetDiskFailed { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockDisksClient.EXPECT().Delete(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(nil).AnyTimes() | ||||
|  | ||||
| 		err := managedDiskController.DeleteManagedDisk(diskURI) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	fakeGetDiskFailed := "fakeGetDiskFailed" | ||||
| 	testCases := []struct { | ||||
| 		desc                      string | ||||
| 		diskName                  string | ||||
| 		existedDisk               compute.Disk | ||||
| 		expectedErr               bool | ||||
| 		expectedErrMsg            error | ||||
| 		expectedProvisioningState string | ||||
| 		expectedDiskID            string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:                      "no error shall be returned if get a normal disk without DiskProperties", | ||||
| 			diskName:                  "disk1", | ||||
| 			existedDisk:               compute.Disk{Name: pointer.String("disk1")}, | ||||
| 			expectedErr:               false, | ||||
| 			expectedProvisioningState: "", | ||||
| 			expectedDiskID:            "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:                      "an error shall be returned if get disk failed", | ||||
| 			diskName:                  fakeGetDiskFailed, | ||||
| 			existedDisk:               compute.Disk{Name: pointer.String(fakeGetDiskFailed)}, | ||||
| 			expectedErr:               true, | ||||
| 			expectedErrMsg:            fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")), | ||||
| 			expectedProvisioningState: "", | ||||
| 			expectedDiskID:            "", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		managedDiskController := testCloud.ManagedDiskController | ||||
|  | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		if test.diskName == fakeGetDiskFailed { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		provisioningState, diskid, err := managedDiskController.GetDisk(testCloud.ResourceGroup, test.diskName) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 		assert.Equal(t, test.expectedProvisioningState, provisioningState, "TestCase[%d]: %s, expected ProvisioningState: %v, return ProvisioningState: %v", i, test.desc, test.expectedProvisioningState, provisioningState) | ||||
| 		assert.Equal(t, test.expectedDiskID, diskid, "TestCase[%d]: %s, expected DiskID: %v, return DiskID: %v", i, test.desc, test.expectedDiskID, diskid) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestResizeDisk(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	diskSizeGB := int32(2) | ||||
| 	diskName := "disk1" | ||||
| 	fakeGetDiskFailed := "fakeGetDiskFailed" | ||||
| 	fakeCreateDiskFailed := "fakeCreateDiskFailed" | ||||
| 	testCases := []struct { | ||||
| 		desc             string | ||||
| 		diskName         string | ||||
| 		oldSize          resource.Quantity | ||||
| 		newSize          resource.Quantity | ||||
| 		existedDisk      compute.Disk | ||||
| 		expectedQuantity resource.Quantity | ||||
| 		expectedErr      bool | ||||
| 		expectedErrMsg   error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:             "new quantity and no error shall be returned if everything is good", | ||||
| 			diskName:         diskName, | ||||
| 			oldSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}}, | ||||
| 			expectedQuantity: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "new quantity and no error shall be returned if everything is good with DiskProperties is null", | ||||
| 			diskName:         diskName, | ||||
| 			oldSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String("disk1")}, | ||||
| 			expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      true, | ||||
| 			expectedErrMsg:   fmt.Errorf("DiskProperties of disk(%s) is nil", diskName), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "new quantity and no error shall be returned if everything is good with disk already of greater or equal size than requested", | ||||
| 			diskName:         diskName, | ||||
| 			oldSize:          *resource.NewQuantity(1*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}}, | ||||
| 			expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "an error shall be returned if everything is good but get disk failed", | ||||
| 			diskName:         fakeGetDiskFailed, | ||||
| 			oldSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String(fakeGetDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}}, | ||||
| 			expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      true, | ||||
| 			expectedErrMsg:   fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "an error shall be returned if everything is good but create disk failed", | ||||
| 			diskName:         fakeCreateDiskFailed, | ||||
| 			oldSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String(fakeCreateDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}}, | ||||
| 			expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      true, | ||||
| 			expectedErrMsg:   fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Create Disk failed")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:             "an error shall be returned if disk is not in Unattached state", | ||||
| 			diskName:         fakeCreateDiskFailed, | ||||
| 			oldSize:          *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			newSize:          *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI), | ||||
| 			existedDisk:      compute.Disk{Name: pointer.String(fakeCreateDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Attached}}, | ||||
| 			expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI), | ||||
| 			expectedErr:      true, | ||||
| 			expectedErrMsg:   fmt.Errorf("azureDisk - disk resize is only supported on Unattached disk, current disk state: Attached, already attached to "), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		managedDiskController := testCloud.ManagedDiskController | ||||
| 		diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 			testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name) | ||||
|  | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		if test.diskName == fakeGetDiskFailed { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes() | ||||
| 		} | ||||
| 		if test.diskName == fakeCreateDiskFailed { | ||||
| 			mockDisksClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(&retry.Error{RawError: fmt.Errorf("Create Disk failed")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockDisksClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes() | ||||
| 		} | ||||
|  | ||||
| 		result, err := managedDiskController.ResizeDisk(diskURI, test.oldSize, test.newSize) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 		assert.Equal(t, test.expectedQuantity.Value(), result.Value(), "TestCase[%d]: %s, expected Quantity: %v, return Quantity: %v", i, test.desc, test.expectedQuantity, result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetLabelsForVolume(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	testCloud0 := GetTestCloud(ctrl) | ||||
| 	diskName := "disk1" | ||||
| 	diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 		testCloud0.SubscriptionID, testCloud0.ResourceGroup, diskName) | ||||
| 	diskSizeGB := int32(30) | ||||
| 	fakeGetDiskFailed := "fakeGetDiskFailed" | ||||
| 	fakeGetDiskFailedDiskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", | ||||
| 		testCloud0.SubscriptionID, testCloud0.ResourceGroup, fakeGetDiskFailed) | ||||
| 	testCases := []struct { | ||||
| 		desc           string | ||||
| 		diskName       string | ||||
| 		pv             *v1.PersistentVolume | ||||
| 		existedDisk    compute.Disk | ||||
| 		expected       map[string]string | ||||
| 		expectedErr    bool | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:     "labels and no error shall be returned if everything is good", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    diskName, | ||||
| 							DataDiskURI: diskURI, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}}, | ||||
| 			expected: map[string]string{ | ||||
| 				v1.LabelTopologyRegion: testCloud0.Location, | ||||
| 				v1.LabelTopologyZone:   testCloud0.makeZone(testCloud0.Location, 1), | ||||
| 			}, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "an error shall be returned if everything is good with invalid zone", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    diskName, | ||||
| 							DataDiskURI: diskURI, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"invalid"}}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("failed to parse zone [invalid] for AzureDisk %v: %v", diskName, "strconv.Atoi: parsing \"invalid\": invalid syntax"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "nil shall be returned if everything is good with null Zones", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    diskName, | ||||
| 							DataDiskURI: diskURI, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}}, | ||||
| 			expected: map[string]string{ | ||||
| 				v1.LabelTopologyRegion: testCloud0.Location, | ||||
| 			}, | ||||
| 			expectedErr:    false, | ||||
| 			expectedErrMsg: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "an error shall be returned if everything is good with get disk failed", | ||||
| 			diskName: fakeGetDiskFailed, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    fakeGetDiskFailed, | ||||
| 							DataDiskURI: fakeGetDiskFailedDiskURI, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(fakeGetDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "an error shall be returned if everything is good with invalid DiskURI", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    diskName, | ||||
| 							DataDiskURI: "invalidDiskURI", | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk:    compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}}, | ||||
| 			expectedErr:    true, | ||||
| 			expectedErrMsg: fmt.Errorf("invalid disk URI: invalidDiskURI"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "nil shall be returned if everything is good but pv.Spec.AzureDisk.DiskName is cloudvolume.ProvisionedVolumeName", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{ | ||||
| 						AzureDisk: &v1.AzureDiskVolumeSource{ | ||||
| 							DiskName:    cloudvolume.ProvisionedVolumeName, | ||||
| 							DataDiskURI: diskURI, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}}, | ||||
| 			expected:    nil, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:     "nil shall be returned if everything is good but pv.Spec.AzureDisk is nil", | ||||
| 			diskName: diskName, | ||||
| 			pv: &v1.PersistentVolume{ | ||||
| 				Spec: v1.PersistentVolumeSpec{ | ||||
| 					PersistentVolumeSource: v1.PersistentVolumeSource{}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}}, | ||||
| 			expected:    nil, | ||||
| 			expectedErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range testCases { | ||||
| 		testCloud := GetTestCloud(ctrl) | ||||
| 		mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface) | ||||
| 		if test.diskName == fakeGetDiskFailed { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes() | ||||
| 		} else { | ||||
| 			mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes() | ||||
| 		} | ||||
| 		mockDisksClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		result, err := testCloud.GetLabelsForVolume(context.TODO(), test.pv) | ||||
| 		assert.Equal(t, test.expected, result, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expected, result) | ||||
| 		assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err) | ||||
| 	} | ||||
| } | ||||
| @@ -1,110 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| ) | ||||
|  | ||||
| // CloudProviderRateLimitConfig indicates the rate limit config for each clients. | ||||
| type CloudProviderRateLimitConfig struct { | ||||
| 	// The default rate limit config options. | ||||
| 	azclients.RateLimitConfig | ||||
|  | ||||
| 	// Rate limit config for each clients. Values would override default settings above. | ||||
| 	RouteRateLimit                  *azclients.RateLimitConfig `json:"routeRateLimit,omitempty" yaml:"routeRateLimit,omitempty"` | ||||
| 	SubnetsRateLimit                *azclients.RateLimitConfig `json:"subnetsRateLimit,omitempty" yaml:"subnetsRateLimit,omitempty"` | ||||
| 	InterfaceRateLimit              *azclients.RateLimitConfig `json:"interfaceRateLimit,omitempty" yaml:"interfaceRateLimit,omitempty"` | ||||
| 	RouteTableRateLimit             *azclients.RateLimitConfig `json:"routeTableRateLimit,omitempty" yaml:"routeTableRateLimit,omitempty"` | ||||
| 	LoadBalancerRateLimit           *azclients.RateLimitConfig `json:"loadBalancerRateLimit,omitempty" yaml:"loadBalancerRateLimit,omitempty"` | ||||
| 	PublicIPAddressRateLimit        *azclients.RateLimitConfig `json:"publicIPAddressRateLimit,omitempty" yaml:"publicIPAddressRateLimit,omitempty"` | ||||
| 	SecurityGroupRateLimit          *azclients.RateLimitConfig `json:"securityGroupRateLimit,omitempty" yaml:"securityGroupRateLimit,omitempty"` | ||||
| 	VirtualMachineRateLimit         *azclients.RateLimitConfig `json:"virtualMachineRateLimit,omitempty" yaml:"virtualMachineRateLimit,omitempty"` | ||||
| 	StorageAccountRateLimit         *azclients.RateLimitConfig `json:"storageAccountRateLimit,omitempty" yaml:"storageAccountRateLimit,omitempty"` | ||||
| 	DiskRateLimit                   *azclients.RateLimitConfig `json:"diskRateLimit,omitempty" yaml:"diskRateLimit,omitempty"` | ||||
| 	SnapshotRateLimit               *azclients.RateLimitConfig `json:"snapshotRateLimit,omitempty" yaml:"snapshotRateLimit,omitempty"` | ||||
| 	VirtualMachineScaleSetRateLimit *azclients.RateLimitConfig `json:"virtualMachineScaleSetRateLimit,omitempty" yaml:"virtualMachineScaleSetRateLimit,omitempty"` | ||||
| 	VirtualMachineSizeRateLimit     *azclients.RateLimitConfig `json:"virtualMachineSizesRateLimit,omitempty" yaml:"virtualMachineSizesRateLimit,omitempty"` | ||||
| } | ||||
|  | ||||
| // InitializeCloudProviderRateLimitConfig initializes rate limit configs. | ||||
| func InitializeCloudProviderRateLimitConfig(config *CloudProviderRateLimitConfig) { | ||||
| 	if config == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Assign read rate limit defaults if no configuration was passed in. | ||||
| 	if config.CloudProviderRateLimitQPS == 0 { | ||||
| 		config.CloudProviderRateLimitQPS = rateLimitQPSDefault | ||||
| 	} | ||||
| 	if config.CloudProviderRateLimitBucket == 0 { | ||||
| 		config.CloudProviderRateLimitBucket = rateLimitBucketDefault | ||||
| 	} | ||||
| 	// Assign write rate limit defaults if no configuration was passed in. | ||||
| 	if config.CloudProviderRateLimitQPSWrite == 0 { | ||||
| 		config.CloudProviderRateLimitQPSWrite = config.CloudProviderRateLimitQPS | ||||
| 	} | ||||
| 	if config.CloudProviderRateLimitBucketWrite == 0 { | ||||
| 		config.CloudProviderRateLimitBucketWrite = config.CloudProviderRateLimitBucket | ||||
| 	} | ||||
|  | ||||
| 	config.RouteRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.RouteRateLimit) | ||||
| 	config.SubnetsRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SubnetsRateLimit) | ||||
| 	config.InterfaceRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.InterfaceRateLimit) | ||||
| 	config.RouteTableRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.RouteTableRateLimit) | ||||
| 	config.LoadBalancerRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.LoadBalancerRateLimit) | ||||
| 	config.PublicIPAddressRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.PublicIPAddressRateLimit) | ||||
| 	config.SecurityGroupRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SecurityGroupRateLimit) | ||||
| 	config.VirtualMachineRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineRateLimit) | ||||
| 	config.StorageAccountRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.StorageAccountRateLimit) | ||||
| 	config.DiskRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.DiskRateLimit) | ||||
| 	config.SnapshotRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SnapshotRateLimit) | ||||
| 	config.VirtualMachineScaleSetRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineScaleSetRateLimit) | ||||
| 	config.VirtualMachineSizeRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineSizeRateLimit) | ||||
| } | ||||
|  | ||||
| // overrideDefaultRateLimitConfig overrides the default CloudProviderRateLimitConfig. | ||||
| func overrideDefaultRateLimitConfig(defaults, config *azclients.RateLimitConfig) *azclients.RateLimitConfig { | ||||
| 	// If config not set, apply defaults. | ||||
| 	if config == nil { | ||||
| 		return defaults | ||||
| 	} | ||||
|  | ||||
| 	// Remain disabled if it's set explicitly. | ||||
| 	if !config.CloudProviderRateLimit { | ||||
| 		return &azclients.RateLimitConfig{CloudProviderRateLimit: false} | ||||
| 	} | ||||
|  | ||||
| 	// Apply default values. | ||||
| 	if config.CloudProviderRateLimitQPS == 0 { | ||||
| 		config.CloudProviderRateLimitQPS = defaults.CloudProviderRateLimitQPS | ||||
| 	} | ||||
| 	if config.CloudProviderRateLimitBucket == 0 { | ||||
| 		config.CloudProviderRateLimitBucket = defaults.CloudProviderRateLimitBucket | ||||
| 	} | ||||
| 	if config.CloudProviderRateLimitQPSWrite == 0 { | ||||
| 		config.CloudProviderRateLimitQPSWrite = defaults.CloudProviderRateLimitQPSWrite | ||||
| 	} | ||||
| 	if config.CloudProviderRateLimitBucketWrite == 0 { | ||||
| 		config.CloudProviderRateLimitBucketWrite = defaults.CloudProviderRateLimitBucketWrite | ||||
| 	} | ||||
|  | ||||
| 	return config | ||||
| } | ||||
| @@ -1,180 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/auth" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	testAzureConfig = `{ | ||||
| 		"aadClientCertPassword": "aadClientCertPassword", | ||||
| 		"aadClientCertPath": "aadClientCertPath", | ||||
| 		"aadClientId": "aadClientId", | ||||
| 		"aadClientSecret": "aadClientSecret", | ||||
| 		"cloud":"AzurePublicCloud", | ||||
| 		"cloudProviderBackoff": true, | ||||
| 		"cloudProviderBackoffDuration": 1, | ||||
| 		"cloudProviderBackoffExponent": 1, | ||||
| 		"cloudProviderBackoffJitter": 1, | ||||
| 		"cloudProviderBackoffRetries": 1, | ||||
| 		"cloudProviderRatelimit": true, | ||||
| 		"cloudProviderRateLimitBucket": 1, | ||||
| 		"cloudProviderRateLimitBucketWrite": 1, | ||||
| 		"cloudProviderRateLimitQPS": 1, | ||||
| 		"cloudProviderRateLimitQPSWrite": 1, | ||||
| 		"virtualMachineScaleSetRateLimit": { | ||||
| 			"cloudProviderRatelimit": true, | ||||
| 			"cloudProviderRateLimitBucket": 2, | ||||
| 			"CloudProviderRateLimitBucketWrite": 2, | ||||
| 			"cloudProviderRateLimitQPS": 0, | ||||
| 			"CloudProviderRateLimitQPSWrite": 0 | ||||
| 		}, | ||||
| 		"loadBalancerRateLimit": { | ||||
| 			"cloudProviderRatelimit": false, | ||||
| 		}, | ||||
| 		"networkResourceTenantId": "networkResourceTenantId", | ||||
| 		"networkResourceSubscriptionId": "networkResourceSubscriptionId", | ||||
| 		"availabilitySetNodesCacheTTLInSeconds": 100, | ||||
| 		"vmssCacheTTLInSeconds": 100, | ||||
| 		"vmssVirtualMachinesCacheTTLInSeconds": 100, | ||||
| 		"vmCacheTTLInSeconds": 100, | ||||
| 		"loadBalancerCacheTTLInSeconds": 100, | ||||
| 		"nsgCacheTTLInSeconds": 100, | ||||
| 		"routeTableCacheTTLInSeconds": 100, | ||||
| 		"location": "location", | ||||
| 		"maximumLoadBalancerRuleCount": 1, | ||||
| 		"primaryAvailabilitySetName": "primaryAvailabilitySetName", | ||||
| 		"primaryScaleSetName": "primaryScaleSetName", | ||||
| 		"resourceGroup": "resourceGroup", | ||||
| 		"routeTableName": "routeTableName", | ||||
| 		"routeTableResourceGroup": "routeTableResourceGroup", | ||||
| 		"securityGroupName": "securityGroupName", | ||||
| 		"securityGroupResourceGroup": "securityGroupResourceGroup", | ||||
| 		"subnetName": "subnetName", | ||||
| 		"subscriptionId": "subscriptionId", | ||||
| 		"tenantId": "tenantId", | ||||
| 		"useInstanceMetadata": true, | ||||
| 		"useManagedIdentityExtension": true, | ||||
| 		"vnetName": "vnetName", | ||||
| 		"vnetResourceGroup": "vnetResourceGroup", | ||||
| 		vmType: "standard" | ||||
| 	}` | ||||
|  | ||||
| 	testDefaultRateLimitConfig = azclients.RateLimitConfig{ | ||||
| 		CloudProviderRateLimit:            true, | ||||
| 		CloudProviderRateLimitBucket:      1, | ||||
| 		CloudProviderRateLimitBucketWrite: 1, | ||||
| 		CloudProviderRateLimitQPS:         1, | ||||
| 		CloudProviderRateLimitQPSWrite:    1, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func TestParseConfig(t *testing.T) { | ||||
| 	expected := &Config{ | ||||
| 		AzureAuthConfig: auth.AzureAuthConfig{ | ||||
| 			AADClientCertPassword:         "aadClientCertPassword", | ||||
| 			AADClientCertPath:             "aadClientCertPath", | ||||
| 			AADClientID:                   "aadClientId", | ||||
| 			AADClientSecret:               "aadClientSecret", | ||||
| 			Cloud:                         "AzurePublicCloud", | ||||
| 			SubscriptionID:                "subscriptionId", | ||||
| 			TenantID:                      "tenantId", | ||||
| 			UseManagedIdentityExtension:   true, | ||||
| 			NetworkResourceTenantID:       "networkResourceTenantId", | ||||
| 			NetworkResourceSubscriptionID: "networkResourceSubscriptionId", | ||||
| 		}, | ||||
| 		CloudProviderBackoff:         true, | ||||
| 		CloudProviderBackoffDuration: 1, | ||||
| 		CloudProviderBackoffExponent: 1, | ||||
| 		CloudProviderBackoffJitter:   1, | ||||
| 		CloudProviderBackoffRetries:  1, | ||||
| 		CloudProviderRateLimitConfig: CloudProviderRateLimitConfig{ | ||||
| 			RateLimitConfig: testDefaultRateLimitConfig, | ||||
| 			LoadBalancerRateLimit: &azclients.RateLimitConfig{ | ||||
| 				CloudProviderRateLimit: false, | ||||
| 			}, | ||||
| 			VirtualMachineScaleSetRateLimit: &azclients.RateLimitConfig{ | ||||
| 				CloudProviderRateLimit:            true, | ||||
| 				CloudProviderRateLimitBucket:      2, | ||||
| 				CloudProviderRateLimitBucketWrite: 2, | ||||
| 			}, | ||||
| 		}, | ||||
| 		AvailabilitySetNodesCacheTTLInSeconds: 100, | ||||
| 		VmssCacheTTLInSeconds:                 100, | ||||
| 		VmssVirtualMachinesCacheTTLInSeconds:  100, | ||||
| 		VMCacheTTLInSeconds:                   100, | ||||
| 		LoadBalancerCacheTTLInSeconds:         100, | ||||
| 		NsgCacheTTLInSeconds:                  100, | ||||
| 		RouteTableCacheTTLInSeconds:           100, | ||||
| 		Location:                              "location", | ||||
| 		MaximumLoadBalancerRuleCount:          1, | ||||
| 		PrimaryAvailabilitySetName:            "primaryAvailabilitySetName", | ||||
| 		PrimaryScaleSetName:                   "primaryScaleSetName", | ||||
| 		ResourceGroup:                         "resourcegroup", | ||||
| 		RouteTableName:                        "routeTableName", | ||||
| 		RouteTableResourceGroup:               "routeTableResourceGroup", | ||||
| 		SecurityGroupName:                     "securityGroupName", | ||||
| 		SecurityGroupResourceGroup:            "securityGroupResourceGroup", | ||||
| 		SubnetName:                            "subnetName", | ||||
| 		UseInstanceMetadata:                   true, | ||||
| 		VMType:                                "standard", | ||||
| 		VnetName:                              "vnetName", | ||||
| 		VnetResourceGroup:                     "vnetResourceGroup", | ||||
| 	} | ||||
|  | ||||
| 	buffer := bytes.NewBufferString(testAzureConfig) | ||||
| 	config, err := parseConfig(buffer) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, expected, config) | ||||
| } | ||||
|  | ||||
| func TestInitializeCloudProviderRateLimitConfig(t *testing.T) { | ||||
| 	buffer := bytes.NewBufferString(testAzureConfig) | ||||
| 	config, err := parseConfig(buffer) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	InitializeCloudProviderRateLimitConfig(&config.CloudProviderRateLimitConfig) | ||||
| 	assert.Equal(t, config.LoadBalancerRateLimit, &azclients.RateLimitConfig{ | ||||
| 		CloudProviderRateLimit: false, | ||||
| 	}) | ||||
| 	assert.Equal(t, config.VirtualMachineScaleSetRateLimit, &azclients.RateLimitConfig{ | ||||
| 		CloudProviderRateLimit:            true, | ||||
| 		CloudProviderRateLimitBucket:      2, | ||||
| 		CloudProviderRateLimitBucketWrite: 2, | ||||
| 		CloudProviderRateLimitQPS:         1, | ||||
| 		CloudProviderRateLimitQPSWrite:    1, | ||||
| 	}) | ||||
| 	assert.Equal(t, config.VirtualMachineSizeRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.VirtualMachineRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.RouteRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.SubnetsRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.InterfaceRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.RouteTableRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.SecurityGroupRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.StorageAccountRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.DiskRateLimit, &testDefaultRateLimitConfig) | ||||
| 	assert.Equal(t, config.SnapshotRateLimit, &testDefaultRateLimitConfig) | ||||
| } | ||||
| @@ -1,579 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/metrics" | ||||
| 	utilnet "k8s.io/utils/net" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// routeUpdateInterval defines the route reconciling interval. | ||||
| 	routeUpdateInterval = 30 * time.Second | ||||
| ) | ||||
|  | ||||
| // routeOperation defines the allowed operations for route updating. | ||||
| type routeOperation string | ||||
|  | ||||
| // copied to minimize the number of cross reference | ||||
| // and exceptions in publishing and allowed imports. | ||||
| const ( | ||||
| 	routeNameFmt       = "%s____%s" | ||||
| 	routeNameSeparator = "____" | ||||
|  | ||||
| 	// Route operations. | ||||
| 	routeOperationAdd             routeOperation = "add" | ||||
| 	routeOperationDelete          routeOperation = "delete" | ||||
| 	routeTableOperationUpdateTags routeOperation = "updateRouteTableTags" | ||||
| ) | ||||
|  | ||||
| // delayedRouteOperation defines a delayed route operation which is used in delayedRouteUpdater. | ||||
| type delayedRouteOperation struct { | ||||
| 	route          network.Route | ||||
| 	routeTableTags map[string]*string | ||||
| 	operation      routeOperation | ||||
| 	result         chan error | ||||
| } | ||||
|  | ||||
| // wait waits for the operation completion and returns the result. | ||||
| func (op *delayedRouteOperation) wait() error { | ||||
| 	return <-op.result | ||||
| } | ||||
|  | ||||
| // delayedRouteUpdater defines a delayed route updater, which batches all the | ||||
| // route updating operations within "interval" period. | ||||
| // Example usage: | ||||
| // | ||||
| //	op, err := updater.addRouteOperation(routeOperationAdd, route) | ||||
| //	err = op.wait() | ||||
| type delayedRouteUpdater struct { | ||||
| 	az       *Cloud | ||||
| 	interval time.Duration | ||||
|  | ||||
| 	lock           sync.Mutex | ||||
| 	routesToUpdate []*delayedRouteOperation | ||||
| } | ||||
|  | ||||
| // newDelayedRouteUpdater creates a new delayedRouteUpdater. | ||||
| func newDelayedRouteUpdater(az *Cloud, interval time.Duration) *delayedRouteUpdater { | ||||
| 	return &delayedRouteUpdater{ | ||||
| 		az:             az, | ||||
| 		interval:       interval, | ||||
| 		routesToUpdate: make([]*delayedRouteOperation, 0), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // run starts the updater reconciling loop. | ||||
| func (d *delayedRouteUpdater) run() { | ||||
| 	err := wait.PollImmediateInfinite(d.interval, func() (bool, error) { | ||||
| 		d.updateRoutes() | ||||
| 		return false, nil | ||||
| 	}) | ||||
| 	if err != nil { // this should never happen, if it does, panic | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // updateRoutes invokes route table client to update all routes. | ||||
| func (d *delayedRouteUpdater) updateRoutes() { | ||||
| 	d.lock.Lock() | ||||
| 	defer d.lock.Unlock() | ||||
|  | ||||
| 	// No need to do any updating. | ||||
| 	if len(d.routesToUpdate) == 0 { | ||||
| 		klog.V(6).Info("updateRoutes: nothing to update, returning") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	defer func() { | ||||
| 		// Notify all the goroutines. | ||||
| 		for _, rt := range d.routesToUpdate { | ||||
| 			rt.result <- err | ||||
| 		} | ||||
| 		// Clear all the jobs. | ||||
| 		d.routesToUpdate = make([]*delayedRouteOperation, 0) | ||||
| 	}() | ||||
|  | ||||
| 	var ( | ||||
| 		routeTable       network.RouteTable | ||||
| 		existsRouteTable bool | ||||
| 	) | ||||
| 	routeTable, existsRouteTable, err = d.az.getRouteTable(azcache.CacheReadTypeDefault) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("getRouteTable() failed with error: %v", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// create route table if it doesn't exists yet. | ||||
| 	if !existsRouteTable { | ||||
| 		err = d.az.createRouteTable() | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("createRouteTable() failed with error: %v", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		routeTable, _, err = d.az.getRouteTable(azcache.CacheReadTypeDefault) | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("getRouteTable() failed with error: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// reconcile routes. | ||||
| 	dirty, onlyUpdateTags := false, true | ||||
| 	routes := []network.Route{} | ||||
| 	if routeTable.Routes != nil { | ||||
| 		routes = *routeTable.Routes | ||||
| 	} | ||||
|  | ||||
| 	routes, dirty = d.cleanupOutdatedRoutes(routes) | ||||
| 	if dirty { | ||||
| 		onlyUpdateTags = false | ||||
| 	} | ||||
|  | ||||
| 	for _, rt := range d.routesToUpdate { | ||||
| 		if rt.operation == routeTableOperationUpdateTags { | ||||
| 			routeTable.Tags = rt.routeTableTags | ||||
| 			dirty = true | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		routeMatch := false | ||||
| 		onlyUpdateTags = false | ||||
| 		for i, existingRoute := range routes { | ||||
| 			if strings.EqualFold(pointer.StringDeref(existingRoute.Name, ""), pointer.StringDeref(rt.route.Name, "")) { | ||||
| 				// delete the name-matched routes here (missing routes would be added later if the operation is add). | ||||
| 				routes = append(routes[:i], routes[i+1:]...) | ||||
| 				if existingRoute.RoutePropertiesFormat != nil && | ||||
| 					rt.route.RoutePropertiesFormat != nil && | ||||
| 					strings.EqualFold(pointer.StringDeref(existingRoute.AddressPrefix, ""), pointer.StringDeref(rt.route.AddressPrefix, "")) && | ||||
| 					strings.EqualFold(pointer.StringDeref(existingRoute.NextHopIPAddress, ""), pointer.StringDeref(rt.route.NextHopIPAddress, "")) { | ||||
| 					routeMatch = true | ||||
| 				} | ||||
| 				if rt.operation == routeOperationDelete { | ||||
| 					dirty = true | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Add missing routes if the operation is add. | ||||
| 		if rt.operation == routeOperationAdd { | ||||
| 			routes = append(routes, rt.route) | ||||
| 			if !routeMatch { | ||||
| 				dirty = true | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if dirty { | ||||
| 		if !onlyUpdateTags { | ||||
| 			klog.V(2).Infof("updateRoutes: updating routes") | ||||
| 			routeTable.Routes = &routes | ||||
| 		} | ||||
| 		err = d.az.CreateOrUpdateRouteTable(routeTable) | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("CreateOrUpdateRouteTable() failed with error: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // cleanupOutdatedRoutes deletes all non-dualstack routes when dualstack is enabled, | ||||
| // and deletes all dualstack routes when dualstack is not enabled. | ||||
| func (d *delayedRouteUpdater) cleanupOutdatedRoutes(existingRoutes []network.Route) (routes []network.Route, changed bool) { | ||||
| 	for i := len(existingRoutes) - 1; i >= 0; i-- { | ||||
| 		existingRouteName := pointer.StringDeref(existingRoutes[i].Name, "") | ||||
| 		split := strings.Split(existingRouteName, routeNameSeparator) | ||||
|  | ||||
| 		klog.V(4).Infof("cleanupOutdatedRoutes: checking route %s", existingRouteName) | ||||
|  | ||||
| 		// filter out unmanaged routes | ||||
| 		deleteRoute := false | ||||
| 		if d.az.nodeNames.Has(split[0]) { | ||||
| 			if d.az.ipv6DualStackEnabled && len(split) == 1 { | ||||
| 				klog.V(2).Infof("cleanupOutdatedRoutes: deleting outdated non-dualstack route %s", existingRouteName) | ||||
| 				deleteRoute = true | ||||
| 			} else if !d.az.ipv6DualStackEnabled && len(split) == 2 { | ||||
| 				klog.V(2).Infof("cleanupOutdatedRoutes: deleting outdated dualstack route %s", existingRouteName) | ||||
| 				deleteRoute = true | ||||
| 			} | ||||
|  | ||||
| 			if deleteRoute { | ||||
| 				existingRoutes = append(existingRoutes[:i], existingRoutes[i+1:]...) | ||||
| 				changed = true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return existingRoutes, changed | ||||
| } | ||||
|  | ||||
| // addRouteOperation adds the routeOperation to delayedRouteUpdater and returns a delayedRouteOperation. | ||||
| func (d *delayedRouteUpdater) addRouteOperation(operation routeOperation, route network.Route) (*delayedRouteOperation, error) { | ||||
| 	d.lock.Lock() | ||||
| 	defer d.lock.Unlock() | ||||
|  | ||||
| 	op := &delayedRouteOperation{ | ||||
| 		route:     route, | ||||
| 		operation: operation, | ||||
| 		result:    make(chan error), | ||||
| 	} | ||||
| 	d.routesToUpdate = append(d.routesToUpdate, op) | ||||
| 	return op, nil | ||||
| } | ||||
|  | ||||
| // addUpdateRouteTableTagsOperation adds a update route table tags operation to delayedRouteUpdater and returns a delayedRouteOperation. | ||||
| func (d *delayedRouteUpdater) addUpdateRouteTableTagsOperation(operation routeOperation, tags map[string]*string) (*delayedRouteOperation, error) { | ||||
| 	d.lock.Lock() | ||||
| 	defer d.lock.Unlock() | ||||
|  | ||||
| 	op := &delayedRouteOperation{ | ||||
| 		routeTableTags: tags, | ||||
| 		operation:      operation, | ||||
| 		result:         make(chan error), | ||||
| 	} | ||||
| 	d.routesToUpdate = append(d.routesToUpdate, op) | ||||
| 	return op, nil | ||||
| } | ||||
|  | ||||
| // ListRoutes lists all managed routes that belong to the specified clusterName | ||||
| func (az *Cloud) ListRoutes(ctx context.Context, clusterName string) ([]*cloudprovider.Route, error) { | ||||
| 	klog.V(10).Infof("ListRoutes: START clusterName=%q", clusterName) | ||||
| 	routeTable, existsRouteTable, err := az.getRouteTable(azcache.CacheReadTypeDefault) | ||||
| 	routes, err := processRoutes(az.ipv6DualStackEnabled, routeTable, existsRouteTable, err) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Compose routes for unmanaged routes so that node controller won't retry creating routes for them. | ||||
| 	unmanagedNodes, err := az.GetUnmanagedNodes() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	az.routeCIDRsLock.Lock() | ||||
| 	defer az.routeCIDRsLock.Unlock() | ||||
| 	for _, nodeName := range unmanagedNodes.List() { | ||||
| 		if cidr, ok := az.routeCIDRs[nodeName]; ok { | ||||
| 			routes = append(routes, &cloudprovider.Route{ | ||||
| 				Name:            nodeName, | ||||
| 				TargetNode:      mapRouteNameToNodeName(az.ipv6DualStackEnabled, nodeName), | ||||
| 				DestinationCIDR: cidr, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// ensure the route table is tagged as configured | ||||
| 	tags, changed := az.ensureRouteTableTagged(&routeTable) | ||||
| 	if changed { | ||||
| 		klog.V(2).Infof("ListRoutes: updating tags on route table %s", pointer.StringDeref(routeTable.Name, "")) | ||||
| 		op, err := az.routeUpdater.addUpdateRouteTableTagsOperation(routeTableOperationUpdateTags, tags) | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("ListRoutes: failed to add route table operation with error: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		// Wait for operation complete. | ||||
| 		err = op.wait() | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("ListRoutes: failed to update route table tags with error: %v", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return routes, nil | ||||
| } | ||||
|  | ||||
| // Injectable for testing | ||||
| func processRoutes(ipv6DualStackEnabled bool, routeTable network.RouteTable, exists bool, err error) ([]*cloudprovider.Route, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if !exists { | ||||
| 		return []*cloudprovider.Route{}, nil | ||||
| 	} | ||||
|  | ||||
| 	var kubeRoutes []*cloudprovider.Route | ||||
| 	if routeTable.RouteTablePropertiesFormat != nil && routeTable.Routes != nil { | ||||
| 		kubeRoutes = make([]*cloudprovider.Route, len(*routeTable.Routes)) | ||||
| 		for i, route := range *routeTable.Routes { | ||||
| 			instance := mapRouteNameToNodeName(ipv6DualStackEnabled, *route.Name) | ||||
| 			cidr := *route.AddressPrefix | ||||
| 			klog.V(10).Infof("ListRoutes: * instance=%q, cidr=%q", instance, cidr) | ||||
|  | ||||
| 			kubeRoutes[i] = &cloudprovider.Route{ | ||||
| 				Name:            *route.Name, | ||||
| 				TargetNode:      instance, | ||||
| 				DestinationCIDR: cidr, | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(10).Info("ListRoutes: FINISH") | ||||
| 	return kubeRoutes, nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) createRouteTable() error { | ||||
| 	routeTable := network.RouteTable{ | ||||
| 		Name:                       pointer.String(az.RouteTableName), | ||||
| 		Location:                   pointer.String(az.Location), | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{}, | ||||
| 	} | ||||
|  | ||||
| 	klog.V(3).Infof("createRouteTableIfNotExists: creating routetable. routeTableName=%q", az.RouteTableName) | ||||
| 	err := az.CreateOrUpdateRouteTable(routeTable) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Invalidate the cache right after updating | ||||
| 	az.rtCache.Delete(az.RouteTableName) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // CreateRoute creates the described managed route | ||||
| // route.Name will be ignored, although the cloud-provider may use nameHint | ||||
| // to create a more user-meaningful name. | ||||
| func (az *Cloud) CreateRoute(ctx context.Context, clusterName string, nameHint string, kubeRoute *cloudprovider.Route) error { | ||||
| 	mc := metrics.NewMetricContext("routes", "create_route", az.ResourceGroup, az.SubscriptionID, "") | ||||
| 	isOperationSucceeded := false | ||||
| 	defer func() { | ||||
| 		mc.ObserveOperationWithResult(isOperationSucceeded) | ||||
| 	}() | ||||
|  | ||||
| 	// Returns  for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	var targetIP string | ||||
| 	nodeName := string(kubeRoute.TargetNode) | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(nodeName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		if az.ipv6DualStackEnabled { | ||||
| 			//TODO (khenidak) add support for unmanaged nodes when the feature reaches beta | ||||
| 			return fmt.Errorf("unmanaged nodes are not supported in dual stack mode") | ||||
| 		} | ||||
| 		klog.V(2).Infof("CreateRoute: omitting unmanaged node %q", kubeRoute.TargetNode) | ||||
| 		az.routeCIDRsLock.Lock() | ||||
| 		defer az.routeCIDRsLock.Unlock() | ||||
| 		az.routeCIDRs[nodeName] = kubeRoute.DestinationCIDR | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	CIDRv6 := utilnet.IsIPv6CIDRString(string(kubeRoute.DestinationCIDR)) | ||||
| 	// if single stack IPv4 then get the IP for the primary ip config | ||||
| 	// single stack IPv6 is supported on dual stack host. So the IPv6 IP is secondary IP for both single stack IPv6 and dual stack | ||||
| 	// Get all private IPs for the machine and find the first one that matches the IPv6 family | ||||
| 	if !az.ipv6DualStackEnabled && !CIDRv6 { | ||||
| 		targetIP, _, err = az.getIPForMachine(kubeRoute.TargetNode) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
| 		// for dual stack and single stack IPv6 we need to select | ||||
| 		// a private ip that matches family of the cidr | ||||
| 		klog.V(4).Infof("CreateRoute: create route instance=%q cidr=%q is in dual stack mode", kubeRoute.TargetNode, kubeRoute.DestinationCIDR) | ||||
| 		nodePrivateIPs, err := az.getPrivateIPsForMachine(kubeRoute.TargetNode) | ||||
| 		if nil != err { | ||||
| 			klog.V(3).Infof("CreateRoute: create route: failed(GetPrivateIPsByNodeName) instance=%q cidr=%q with error=%v", kubeRoute.TargetNode, kubeRoute.DestinationCIDR, err) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		targetIP, err = findFirstIPByFamily(nodePrivateIPs, CIDRv6) | ||||
| 		if nil != err { | ||||
| 			klog.V(3).Infof("CreateRoute: create route: failed(findFirstIpByFamily) instance=%q cidr=%q with error=%v", kubeRoute.TargetNode, kubeRoute.DestinationCIDR, err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, string(kubeRoute.DestinationCIDR)) | ||||
| 	route := network.Route{ | ||||
| 		Name: pointer.String(routeName), | ||||
| 		RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 			AddressPrefix:    pointer.String(kubeRoute.DestinationCIDR), | ||||
| 			NextHopType:      network.RouteNextHopTypeVirtualAppliance, | ||||
| 			NextHopIPAddress: pointer.String(targetIP), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("CreateRoute: creating route for clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR) | ||||
| 	op, err := az.routeUpdater.addRouteOperation(routeOperationAdd, route) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("CreateRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Wait for operation complete. | ||||
| 	err = op.wait() | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("CreateRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("CreateRoute: route created. clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR) | ||||
| 	isOperationSucceeded = true | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteRoute deletes the specified managed route | ||||
| // Route should be as returned by ListRoutes | ||||
| func (az *Cloud) DeleteRoute(ctx context.Context, clusterName string, kubeRoute *cloudprovider.Route) error { | ||||
| 	mc := metrics.NewMetricContext("routes", "delete_route", az.ResourceGroup, az.SubscriptionID, "") | ||||
| 	isOperationSucceeded := false | ||||
| 	defer func() { | ||||
| 		mc.ObserveOperationWithResult(isOperationSucceeded) | ||||
| 	}() | ||||
|  | ||||
| 	// Returns  for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	nodeName := string(kubeRoute.TargetNode) | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(nodeName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		klog.V(2).Infof("DeleteRoute: omitting unmanaged node %q", kubeRoute.TargetNode) | ||||
| 		az.routeCIDRsLock.Lock() | ||||
| 		defer az.routeCIDRsLock.Unlock() | ||||
| 		delete(az.routeCIDRs, nodeName) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, string(kubeRoute.DestinationCIDR)) | ||||
| 	klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeName) | ||||
| 	route := network.Route{ | ||||
| 		Name:                  pointer.String(routeName), | ||||
| 		RoutePropertiesFormat: &network.RoutePropertiesFormat{}, | ||||
| 	} | ||||
| 	op, err := az.routeUpdater.addRouteOperation(routeOperationDelete, route) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Wait for operation complete. | ||||
| 	err = op.wait() | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Remove outdated ipv4 routes as well | ||||
| 	if az.ipv6DualStackEnabled { | ||||
| 		routeNameWithoutIPV6Suffix := strings.Split(routeName, routeNameSeparator)[0] | ||||
| 		klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeNameWithoutIPV6Suffix) | ||||
| 		route := network.Route{ | ||||
| 			Name:                  pointer.String(routeNameWithoutIPV6Suffix), | ||||
| 			RoutePropertiesFormat: &network.RoutePropertiesFormat{}, | ||||
| 		} | ||||
| 		op, err := az.routeUpdater.addRouteOperation(routeOperationDelete, route) | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Wait for operation complete. | ||||
| 		err = op.wait() | ||||
| 		if err != nil { | ||||
| 			klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	klog.V(2).Infof("DeleteRoute: route deleted. clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR) | ||||
| 	isOperationSucceeded = true | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // This must be kept in sync with mapRouteNameToNodeName. | ||||
| // These two functions enable stashing the instance name in the route | ||||
| // and then retrieving it later when listing. This is needed because | ||||
| // Azure does not let you put tags/descriptions on the Route itself. | ||||
| func mapNodeNameToRouteName(ipv6DualStackEnabled bool, nodeName types.NodeName, cidr string) string { | ||||
| 	if !ipv6DualStackEnabled { | ||||
| 		return fmt.Sprintf("%s", nodeName) | ||||
| 	} | ||||
| 	return fmt.Sprintf(routeNameFmt, nodeName, cidrtoRfc1035(cidr)) | ||||
| } | ||||
|  | ||||
| // Used with mapNodeNameToRouteName. See comment on mapNodeNameToRouteName. | ||||
| func mapRouteNameToNodeName(ipv6DualStackEnabled bool, routeName string) types.NodeName { | ||||
| 	if !ipv6DualStackEnabled { | ||||
| 		return types.NodeName(routeName) | ||||
| 	} | ||||
| 	parts := strings.Split(routeName, routeNameSeparator) | ||||
| 	nodeName := parts[0] | ||||
| 	return types.NodeName(nodeName) | ||||
|  | ||||
| } | ||||
|  | ||||
| // given a list of ips, return the first one | ||||
| // that matches the family requested | ||||
| // error if no match, or failure to parse | ||||
| // any of the ips | ||||
| func findFirstIPByFamily(ips []string, v6 bool) (string, error) { | ||||
| 	for _, ip := range ips { | ||||
| 		bIPv6 := utilnet.IsIPv6String(ip) | ||||
| 		if v6 == bIPv6 { | ||||
| 			return ip, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return "", fmt.Errorf("no match found matching the ipfamily requested") | ||||
| } | ||||
|  | ||||
| // strips : . / | ||||
| func cidrtoRfc1035(cidr string) string { | ||||
| 	cidr = strings.ReplaceAll(cidr, ":", "") | ||||
| 	cidr = strings.ReplaceAll(cidr, ".", "") | ||||
| 	cidr = strings.ReplaceAll(cidr, "/", "") | ||||
| 	return cidr | ||||
| } | ||||
|  | ||||
| // ensureRouteTableTagged ensures the route table is tagged as configured | ||||
| func (az *Cloud) ensureRouteTableTagged(rt *network.RouteTable) (map[string]*string, bool) { | ||||
| 	if az.Tags == "" { | ||||
| 		return nil, false | ||||
| 	} | ||||
| 	tags := parseTags(az.Tags) | ||||
| 	if rt.Tags == nil { | ||||
| 		rt.Tags = make(map[string]*string) | ||||
| 	} | ||||
|  | ||||
| 	tags, changed := reconcileTags(rt.Tags, tags) | ||||
| 	rt.Tags = tags | ||||
|  | ||||
| 	return rt.Tags, changed | ||||
| } | ||||
| @@ -1,813 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/mockvmsets" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestDeleteRoute(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	routeTableClient := mockroutetableclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	cloud := &Cloud{ | ||||
| 		RouteTablesClient: routeTableClient, | ||||
| 		Config: Config{ | ||||
| 			RouteTableResourceGroup: "foo", | ||||
| 			RouteTableName:          "bar", | ||||
| 			Location:                "location", | ||||
| 		}, | ||||
| 		unmanagedNodes:     sets.NewString(), | ||||
| 		nodeInformerSynced: func() bool { return true }, | ||||
| 	} | ||||
| 	cache, _ := cloud.newRouteTableCache() | ||||
| 	cloud.rtCache = cache | ||||
| 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond) | ||||
| 	go cloud.routeUpdater.run() | ||||
| 	route := cloudprovider.Route{ | ||||
| 		TargetNode:      "node", | ||||
| 		DestinationCIDR: "1.2.3.4/24", | ||||
| 	} | ||||
| 	routeName := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR) | ||||
| 	routeTables := network.RouteTable{ | ||||
| 		Name:     &cloud.RouteTableName, | ||||
| 		Location: &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 			Routes: &[]network.Route{ | ||||
| 				{ | ||||
| 					Name: &routeName, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	routeTablesAfterDeletion := network.RouteTable{ | ||||
| 		Name:     &cloud.RouteTableName, | ||||
| 		Location: &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 			Routes: &[]network.Route{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil) | ||||
| 	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterDeletion, "").Return(nil) | ||||
| 	err := cloud.DeleteRoute(context.TODO(), "cluster", &route) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error deleting route: %v", err) | ||||
| 		t.FailNow() | ||||
| 	} | ||||
|  | ||||
| 	// test delete route for unmanaged nodes. | ||||
| 	nodeName := "node1" | ||||
| 	nodeCIDR := "4.3.2.1/24" | ||||
| 	cloud.unmanagedNodes.Insert(nodeName) | ||||
| 	cloud.routeCIDRs = map[string]string{ | ||||
| 		nodeName: nodeCIDR, | ||||
| 	} | ||||
| 	route1 := cloudprovider.Route{ | ||||
| 		TargetNode:      mapRouteNameToNodeName(false, nodeName), | ||||
| 		DestinationCIDR: nodeCIDR, | ||||
| 	} | ||||
| 	err = cloud.DeleteRoute(context.TODO(), "cluster", &route1) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error deleting route: %v", err) | ||||
| 		t.FailNow() | ||||
| 	} | ||||
| 	cidr, found := cloud.routeCIDRs[nodeName] | ||||
| 	if found { | ||||
| 		t.Errorf("unexpected CIDR item (%q) for %s", cidr, nodeName) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateRoute(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	routeTableClient := mockroutetableclient.NewMockInterface(ctrl) | ||||
| 	mockVMSet := mockvmsets.NewMockVMSet(ctrl) | ||||
|  | ||||
| 	cloud := &Cloud{ | ||||
| 		RouteTablesClient: routeTableClient, | ||||
| 		VMSet:             mockVMSet, | ||||
| 		Config: Config{ | ||||
| 			RouteTableResourceGroup: "foo", | ||||
| 			RouteTableName:          "bar", | ||||
| 			Location:                "location", | ||||
| 		}, | ||||
| 		unmanagedNodes:     sets.NewString(), | ||||
| 		nodeInformerSynced: func() bool { return true }, | ||||
| 	} | ||||
| 	cache, _ := cloud.newRouteTableCache() | ||||
| 	cloud.rtCache = cache | ||||
| 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond) | ||||
| 	go cloud.routeUpdater.run() | ||||
|  | ||||
| 	route := cloudprovider.Route{TargetNode: "node", DestinationCIDR: "1.2.3.4/24"} | ||||
| 	nodePrivateIP := "2.4.6.8" | ||||
| 	networkRoute := &[]network.Route{ | ||||
| 		{ | ||||
| 			Name: pointer.String("node"), | ||||
| 			RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 				AddressPrefix:    pointer.String("1.2.3.4/24"), | ||||
| 				NextHopIPAddress: &nodePrivateIP, | ||||
| 				NextHopType:      network.RouteNextHopTypeVirtualAppliance, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name                  string | ||||
| 		routeTableName        string | ||||
| 		initialRoute          *[]network.Route | ||||
| 		updatedRoute          *[]network.Route | ||||
| 		hasUnmanagedNodes     bool | ||||
| 		nodeInformerNotSynced bool | ||||
| 		ipv6DualStackEnabled  bool | ||||
| 		routeTableNotExist    bool | ||||
| 		unmanagedNodeName     string | ||||
| 		routeCIDRs            map[string]string | ||||
| 		expectedRouteCIDRs    map[string]string | ||||
|  | ||||
| 		getIPError        error | ||||
| 		getErr            *retry.Error | ||||
| 		secondGetErr      *retry.Error | ||||
| 		createOrUpdateErr *retry.Error | ||||
| 		expectedErrMsg    error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "CreateRoute should create route if route doesn't exist", | ||||
| 			routeTableName: "rt1", | ||||
| 			updatedRoute:   networkRoute, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should report error if error occurs when invoke CreateOrUpdateRouteTable", | ||||
| 			routeTableName: "rt2", | ||||
| 			updatedRoute:   networkRoute, | ||||
| 			createOrUpdateErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusInternalServerError, | ||||
| 				RawError:       fmt.Errorf("CreateOrUpdate error"), | ||||
| 			}, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("CreateOrUpdate error")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should do nothing if route already exists", | ||||
| 			routeTableName: "rt3", | ||||
| 			initialRoute:   networkRoute, | ||||
| 			updatedRoute:   networkRoute, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should report error if error occurs when invoke createRouteTable", | ||||
| 			routeTableName: "rt4", | ||||
| 			getErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusNotFound, | ||||
| 				RawError:       cloudprovider.InstanceNotFound, | ||||
| 			}, | ||||
| 			createOrUpdateErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusInternalServerError, | ||||
| 				RawError:       fmt.Errorf("CreateOrUpdate error"), | ||||
| 			}, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("CreateOrUpdate error")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should report error if error occurs when invoke getRouteTable for the second time", | ||||
| 			routeTableName: "rt5", | ||||
| 			getErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusNotFound, | ||||
| 				RawError:       cloudprovider.InstanceNotFound, | ||||
| 			}, | ||||
| 			secondGetErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusInternalServerError, | ||||
| 				RawError:       fmt.Errorf("Get error"), | ||||
| 			}, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should report error if error occurs when invoke routeTableClient.Get", | ||||
| 			routeTableName: "rt6", | ||||
| 			getErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusInternalServerError, | ||||
| 				RawError:       fmt.Errorf("Get error"), | ||||
| 			}, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should report error if error occurs when invoke GetIPByNodeName", | ||||
| 			routeTableName: "rt7", | ||||
| 			getIPError:     fmt.Errorf("getIP error"), | ||||
| 			expectedErrMsg: wait.ErrWaitTimeout, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "CreateRoute should add route to cloud.RouteCIDRs if node is unmanaged", | ||||
| 			routeTableName:     "rt8", | ||||
| 			hasUnmanagedNodes:  true, | ||||
| 			unmanagedNodeName:  "node", | ||||
| 			routeCIDRs:         map[string]string{}, | ||||
| 			expectedRouteCIDRs: map[string]string{"node": "1.2.3.4/24"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                 "CreateRoute should report error if node is unmanaged and cloud.ipv6DualStackEnabled is true", | ||||
| 			hasUnmanagedNodes:    true, | ||||
| 			ipv6DualStackEnabled: true, | ||||
| 			unmanagedNodeName:    "node", | ||||
| 			expectedErrMsg:       fmt.Errorf("unmanaged nodes are not supported in dual stack mode"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "CreateRoute should create route if cloud.ipv6DualStackEnabled is true and route doesn't exist", | ||||
| 			routeTableName: "rt9", | ||||
| 			updatedRoute: &[]network.Route{ | ||||
| 				{ | ||||
| 					Name: pointer.String("node____123424"), | ||||
| 					RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 						AddressPrefix:    pointer.String("1.2.3.4/24"), | ||||
| 						NextHopIPAddress: &nodePrivateIP, | ||||
| 						NextHopType:      network.RouteNextHopTypeVirtualAppliance, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			ipv6DualStackEnabled: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "CreateRoute should report error if node informer is not synced", | ||||
| 			nodeInformerNotSynced: true, | ||||
| 			expectedErrMsg:        fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testCases { | ||||
| 		initialTable := network.RouteTable{ | ||||
| 			Name:     pointer.String(test.routeTableName), | ||||
| 			Location: &cloud.Location, | ||||
| 			RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 				Routes: test.initialRoute, | ||||
| 			}, | ||||
| 		} | ||||
| 		updatedTable := network.RouteTable{ | ||||
| 			Name:     pointer.String(test.routeTableName), | ||||
| 			Location: &cloud.Location, | ||||
| 			RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 				Routes: test.updatedRoute, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		cloud.RouteTableName = test.routeTableName | ||||
| 		cloud.ipv6DualStackEnabled = test.ipv6DualStackEnabled | ||||
| 		if test.hasUnmanagedNodes { | ||||
| 			cloud.unmanagedNodes.Insert(test.unmanagedNodeName) | ||||
| 			cloud.routeCIDRs = test.routeCIDRs | ||||
| 		} else { | ||||
| 			cloud.unmanagedNodes = sets.NewString() | ||||
| 			cloud.routeCIDRs = nil | ||||
| 		} | ||||
| 		if test.nodeInformerNotSynced { | ||||
| 			cloud.nodeInformerSynced = func() bool { return false } | ||||
| 		} else { | ||||
| 			cloud.nodeInformerSynced = func() bool { return true } | ||||
| 		} | ||||
|  | ||||
| 		mockVMSet.EXPECT().GetIPByNodeName(gomock.Any()).Return(nodePrivateIP, "", test.getIPError).MaxTimes(1) | ||||
| 		mockVMSet.EXPECT().GetPrivateIPsByNodeName("node").Return([]string{nodePrivateIP, "10.10.10.10"}, nil).MaxTimes(1) | ||||
| 		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.getErr).MaxTimes(1) | ||||
| 		routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, updatedTable, "").Return(test.createOrUpdateErr).MaxTimes(1) | ||||
|  | ||||
| 		//Here is the second invocation when route table doesn't exist | ||||
| 		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.secondGetErr).MaxTimes(1) | ||||
|  | ||||
| 		err := cloud.CreateRoute(context.TODO(), "cluster", "unused", &route) | ||||
| 		assert.Equal(t, cloud.routeCIDRs, test.expectedRouteCIDRs, test.name) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateRouteTable(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	routeTableClient := mockroutetableclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	cloud := &Cloud{ | ||||
| 		RouteTablesClient: routeTableClient, | ||||
| 		Config: Config{ | ||||
| 			RouteTableResourceGroup: "foo", | ||||
| 			RouteTableName:          "bar", | ||||
| 			Location:                "location", | ||||
| 		}, | ||||
| 	} | ||||
| 	cache, _ := cloud.newRouteTableCache() | ||||
| 	cloud.rtCache = cache | ||||
|  | ||||
| 	expectedTable := network.RouteTable{ | ||||
| 		Name:                       &cloud.RouteTableName, | ||||
| 		Location:                   &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{}, | ||||
| 	} | ||||
| 	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, expectedTable, "").Return(nil) | ||||
| 	err := cloud.createRouteTable() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error in creating route table: %v", err) | ||||
| 		t.FailNow() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestProcessRoutes(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		rt            network.RouteTable | ||||
| 		exists        bool | ||||
| 		err           error | ||||
| 		expectErr     bool | ||||
| 		expectedError string | ||||
| 		expectedRoute []cloudprovider.Route | ||||
| 		name          string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			err:           fmt.Errorf("test error"), | ||||
| 			expectErr:     true, | ||||
| 			expectedError: "test error", | ||||
| 		}, | ||||
| 		{ | ||||
| 			exists: false, | ||||
| 			name:   "doesn't exist", | ||||
| 		}, | ||||
| 		{ | ||||
| 			rt:     network.RouteTable{}, | ||||
| 			exists: true, | ||||
| 			name:   "nil routes", | ||||
| 		}, | ||||
| 		{ | ||||
| 			rt: network.RouteTable{ | ||||
| 				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{}, | ||||
| 			}, | ||||
| 			exists: true, | ||||
| 			name:   "no routes", | ||||
| 		}, | ||||
| 		{ | ||||
| 			rt: network.RouteTable{ | ||||
| 				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 					Routes: &[]network.Route{ | ||||
| 						{ | ||||
| 							Name: pointer.String("name"), | ||||
| 							RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 								AddressPrefix: pointer.String("1.2.3.4/16"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			exists: true, | ||||
| 			expectedRoute: []cloudprovider.Route{ | ||||
| 				{ | ||||
| 					Name:            "name", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "name"), | ||||
| 					DestinationCIDR: "1.2.3.4/16", | ||||
| 				}, | ||||
| 			}, | ||||
| 			name: "one route", | ||||
| 		}, | ||||
| 		{ | ||||
| 			rt: network.RouteTable{ | ||||
| 				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 					Routes: &[]network.Route{ | ||||
| 						{ | ||||
| 							Name: pointer.String("name"), | ||||
| 							RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 								AddressPrefix: pointer.String("1.2.3.4/16"), | ||||
| 							}, | ||||
| 						}, | ||||
| 						{ | ||||
| 							Name: pointer.String("name2"), | ||||
| 							RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 								AddressPrefix: pointer.String("5.6.7.8/16"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			exists: true, | ||||
| 			expectedRoute: []cloudprovider.Route{ | ||||
| 				{ | ||||
| 					Name:            "name", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "name"), | ||||
| 					DestinationCIDR: "1.2.3.4/16", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:            "name2", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "name2"), | ||||
| 					DestinationCIDR: "5.6.7.8/16", | ||||
| 				}, | ||||
| 			}, | ||||
| 			name: "more routes", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		routes, err := processRoutes(false, test.rt, test.exists, test.err) | ||||
| 		if test.expectErr { | ||||
| 			if err == nil { | ||||
| 				t.Errorf("%s: unexpected non-error", test.name) | ||||
| 				continue | ||||
| 			} | ||||
| 			if err.Error() != test.expectedError { | ||||
| 				t.Errorf("%s: Expected error: %v, saw error: %v", test.name, test.expectedError, err.Error()) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("%s; unexpected error: %v", test.name, err) | ||||
| 			continue | ||||
| 		} | ||||
| 		if len(routes) != len(test.expectedRoute) { | ||||
| 			t.Errorf("%s: Unexpected difference: %#v vs %#v", test.name, routes, test.expectedRoute) | ||||
| 			continue | ||||
| 		} | ||||
| 		for ix := range test.expectedRoute { | ||||
| 			if !reflect.DeepEqual(test.expectedRoute[ix], *routes[ix]) { | ||||
| 				t.Errorf("%s: Unexpected difference: %#v vs %#v", test.name, test.expectedRoute[ix], *routes[ix]) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFindFirstIPByFamily(t *testing.T) { | ||||
| 	firstIPv4 := "10.0.0.1" | ||||
| 	firstIPv6 := "2001:1234:5678:9abc::9" | ||||
| 	testIPs := []string{ | ||||
| 		firstIPv4, | ||||
| 		"11.0.0.1", | ||||
| 		firstIPv6, | ||||
| 		"fda4:6dee:effc:62a0:0:0:0:0", | ||||
| 	} | ||||
| 	testCases := []struct { | ||||
| 		ipv6           bool | ||||
| 		ips            []string | ||||
| 		expectedIP     string | ||||
| 		expectedErrMsg error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			ipv6:       true, | ||||
| 			ips:        testIPs, | ||||
| 			expectedIP: firstIPv6, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ipv6:       false, | ||||
| 			ips:        testIPs, | ||||
| 			expectedIP: firstIPv4, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ipv6:           true, | ||||
| 			ips:            []string{"10.0.0.1"}, | ||||
| 			expectedErrMsg: fmt.Errorf("no match found matching the ipfamily requested"), | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testCases { | ||||
| 		ip, err := findFirstIPByFamily(test.ips, test.ipv6) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err) | ||||
| 		assert.Equal(t, test.expectedIP, ip) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRouteNameFuncs(t *testing.T) { | ||||
| 	v4CIDR := "10.0.0.1/16" | ||||
| 	v6CIDR := "fd3e:5f02:6ec0:30ba::/64" | ||||
| 	nodeName := "thisNode" | ||||
| 	testCases := []struct { | ||||
| 		ipv6DualStackEnabled bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			ipv6DualStackEnabled: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ipv6DualStackEnabled: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, test := range testCases { | ||||
| 		routeName := mapNodeNameToRouteName(test.ipv6DualStackEnabled, types.NodeName(nodeName), v4CIDR) | ||||
| 		outNodeName := mapRouteNameToNodeName(test.ipv6DualStackEnabled, routeName) | ||||
| 		assert.Equal(t, string(outNodeName), nodeName) | ||||
|  | ||||
| 		routeName = mapNodeNameToRouteName(test.ipv6DualStackEnabled, types.NodeName(nodeName), v6CIDR) | ||||
| 		outNodeName = mapRouteNameToNodeName(test.ipv6DualStackEnabled, routeName) | ||||
| 		assert.Equal(t, string(outNodeName), nodeName) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestListRoutes(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	routeTableClient := mockroutetableclient.NewMockInterface(ctrl) | ||||
| 	mockVMSet := mockvmsets.NewMockVMSet(ctrl) | ||||
|  | ||||
| 	cloud := &Cloud{ | ||||
| 		RouteTablesClient: routeTableClient, | ||||
| 		VMSet:             mockVMSet, | ||||
| 		Config: Config{ | ||||
| 			RouteTableResourceGroup: "foo", | ||||
| 			RouteTableName:          "bar", | ||||
| 			Location:                "location", | ||||
| 		}, | ||||
| 		unmanagedNodes:     sets.NewString(), | ||||
| 		nodeInformerSynced: func() bool { return true }, | ||||
| 	} | ||||
| 	cache, _ := cloud.newRouteTableCache() | ||||
| 	cloud.rtCache = cache | ||||
| 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond) | ||||
| 	go cloud.routeUpdater.run() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name                  string | ||||
| 		routeTableName        string | ||||
| 		routeTable            network.RouteTable | ||||
| 		hasUnmanagedNodes     bool | ||||
| 		nodeInformerNotSynced bool | ||||
| 		unmanagedNodeName     string | ||||
| 		routeCIDRs            map[string]string | ||||
| 		expectedRoutes        []*cloudprovider.Route | ||||
| 		getErr                *retry.Error | ||||
| 		expectedErrMsg        error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "ListRoutes should return correct routes", | ||||
| 			routeTableName: "rt1", | ||||
| 			routeTable: network.RouteTable{ | ||||
| 				Name:     pointer.String("rt1"), | ||||
| 				Location: &cloud.Location, | ||||
| 				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 					Routes: &[]network.Route{ | ||||
| 						{ | ||||
| 							Name: pointer.String("node"), | ||||
| 							RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 								AddressPrefix: pointer.String("1.2.3.4/24"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedRoutes: []*cloudprovider.Route{ | ||||
| 				{ | ||||
| 					Name:            "node", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "node"), | ||||
| 					DestinationCIDR: "1.2.3.4/24", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:              "ListRoutes should return correct routes if there's unmanaged nodes", | ||||
| 			routeTableName:    "rt2", | ||||
| 			hasUnmanagedNodes: true, | ||||
| 			unmanagedNodeName: "unmanaged-node", | ||||
| 			routeCIDRs:        map[string]string{"unmanaged-node": "2.2.3.4/24"}, | ||||
| 			routeTable: network.RouteTable{ | ||||
| 				Name:     pointer.String("rt2"), | ||||
| 				Location: &cloud.Location, | ||||
| 				RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 					Routes: &[]network.Route{ | ||||
| 						{ | ||||
| 							Name: pointer.String("node"), | ||||
| 							RoutePropertiesFormat: &network.RoutePropertiesFormat{ | ||||
| 								AddressPrefix: pointer.String("1.2.3.4/24"), | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedRoutes: []*cloudprovider.Route{ | ||||
| 				{ | ||||
| 					Name:            "node", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "node"), | ||||
| 					DestinationCIDR: "1.2.3.4/24", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:            "unmanaged-node", | ||||
| 					TargetNode:      mapRouteNameToNodeName(false, "unmanaged-node"), | ||||
| 					DestinationCIDR: "2.2.3.4/24", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "ListRoutes should return nil if routeTable don't exist", | ||||
| 			routeTableName: "rt3", | ||||
| 			routeTable:     network.RouteTable{}, | ||||
| 			getErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusNotFound, | ||||
| 				RawError:       cloudprovider.InstanceNotFound, | ||||
| 			}, | ||||
| 			expectedRoutes: []*cloudprovider.Route{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "ListRoutes should report error if error occurs when invoke routeTableClient.Get", | ||||
| 			routeTableName: "rt4", | ||||
| 			routeTable:     network.RouteTable{}, | ||||
| 			getErr: &retry.Error{ | ||||
| 				HTTPStatusCode: http.StatusInternalServerError, | ||||
| 				RawError:       fmt.Errorf("Get error"), | ||||
| 			}, | ||||
| 			expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                  "ListRoutes should report error if node informer is not synced", | ||||
| 			routeTableName:        "rt5", | ||||
| 			nodeInformerNotSynced: true, | ||||
| 			routeTable:            network.RouteTable{}, | ||||
| 			expectedErrMsg:        fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testCases { | ||||
| 		if test.hasUnmanagedNodes { | ||||
| 			cloud.unmanagedNodes.Insert(test.unmanagedNodeName) | ||||
| 			cloud.routeCIDRs = test.routeCIDRs | ||||
| 		} else { | ||||
| 			cloud.unmanagedNodes = sets.NewString() | ||||
| 			cloud.routeCIDRs = nil | ||||
| 		} | ||||
|  | ||||
| 		if test.nodeInformerNotSynced { | ||||
| 			cloud.nodeInformerSynced = func() bool { return false } | ||||
| 		} else { | ||||
| 			cloud.nodeInformerSynced = func() bool { return true } | ||||
| 		} | ||||
|  | ||||
| 		cloud.RouteTableName = test.routeTableName | ||||
| 		routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, test.routeTableName, "").Return(test.routeTable, test.getErr) | ||||
|  | ||||
| 		routes, err := cloud.ListRoutes(context.TODO(), "cluster") | ||||
| 		assert.Equal(t, test.expectedRoutes, routes, test.name) | ||||
| 		assert.Equal(t, test.expectedErrMsg, err, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCleanupOutdatedRoutes(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	for _, testCase := range []struct { | ||||
| 		description                          string | ||||
| 		existingRoutes, expectedRoutes       []network.Route | ||||
| 		existingNodeNames                    sets.String | ||||
| 		expectedChanged, enableIPV6DualStack bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			description: "cleanupOutdatedRoutes should delete outdated non-dualstack routes when dualstack is enabled", | ||||
| 			existingRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			expectedRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 			}, | ||||
| 			existingNodeNames:   sets.NewString("aks-node1-vmss000000"), | ||||
| 			enableIPV6DualStack: true, | ||||
| 			expectedChanged:     true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "cleanupOutdatedRoutes should delete outdated dualstack routes when dualstack is disabled", | ||||
| 			existingRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			expectedRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			existingNodeNames: sets.NewString("aks-node1-vmss000000"), | ||||
| 			expectedChanged:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is enabled", | ||||
| 			existingRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			expectedRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			existingNodeNames:   sets.NewString("aks-node1-vmss000001"), | ||||
| 			enableIPV6DualStack: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is disabled", | ||||
| 			existingRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			expectedRoutes: []network.Route{ | ||||
| 				{Name: pointer.String("aks-node1-vmss000000____xxx")}, | ||||
| 				{Name: pointer.String("aks-node1-vmss000000")}, | ||||
| 			}, | ||||
| 			existingNodeNames: sets.NewString("aks-node1-vmss000001"), | ||||
| 		}, | ||||
| 	} { | ||||
| 		t.Run(testCase.description, func(t *testing.T) { | ||||
| 			cloud := &Cloud{ | ||||
| 				ipv6DualStackEnabled: testCase.enableIPV6DualStack, | ||||
| 				nodeNames:            testCase.existingNodeNames, | ||||
| 			} | ||||
|  | ||||
| 			d := &delayedRouteUpdater{ | ||||
| 				az: cloud, | ||||
| 			} | ||||
|  | ||||
| 			routes, changed := d.cleanupOutdatedRoutes(testCase.existingRoutes) | ||||
| 			assert.Equal(t, testCase.expectedChanged, changed) | ||||
| 			assert.Equal(t, testCase.expectedRoutes, routes) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDeleteRouteDualStack(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	routeTableClient := mockroutetableclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	cloud := &Cloud{ | ||||
| 		RouteTablesClient: routeTableClient, | ||||
| 		Config: Config{ | ||||
| 			RouteTableResourceGroup: "foo", | ||||
| 			RouteTableName:          "bar", | ||||
| 			Location:                "location", | ||||
| 		}, | ||||
| 		unmanagedNodes:       sets.NewString(), | ||||
| 		nodeInformerSynced:   func() bool { return true }, | ||||
| 		ipv6DualStackEnabled: true, | ||||
| 	} | ||||
| 	cache, _ := cloud.newRouteTableCache() | ||||
| 	cloud.rtCache = cache | ||||
| 	cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond) | ||||
| 	go cloud.routeUpdater.run() | ||||
|  | ||||
| 	route := cloudprovider.Route{ | ||||
| 		TargetNode:      "node", | ||||
| 		DestinationCIDR: "1.2.3.4/24", | ||||
| 	} | ||||
| 	routeName := mapNodeNameToRouteName(true, route.TargetNode, route.DestinationCIDR) | ||||
| 	routeNameIPV4 := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR) | ||||
| 	routeTables := network.RouteTable{ | ||||
| 		Name:     &cloud.RouteTableName, | ||||
| 		Location: &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 			Routes: &[]network.Route{ | ||||
| 				{ | ||||
| 					Name: &routeName, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name: &routeNameIPV4, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	routeTablesAfterFirstDeletion := network.RouteTable{ | ||||
| 		Name:     &cloud.RouteTableName, | ||||
| 		Location: &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 			Routes: &[]network.Route{ | ||||
| 				{ | ||||
| 					Name: &routeNameIPV4, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	routeTablesAfterSecondDeletion := network.RouteTable{ | ||||
| 		Name:     &cloud.RouteTableName, | ||||
| 		Location: &cloud.Location, | ||||
| 		RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{ | ||||
| 			Routes: &[]network.Route{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil).AnyTimes() | ||||
| 	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterFirstDeletion, "").Return(nil) | ||||
| 	routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterSecondDeletion, "").Return(nil) | ||||
| 	err := cloud.DeleteRoute(context.TODO(), "cluster", &route) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error deleting route: %v", err) | ||||
| 		t.FailNow() | ||||
| 	} | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,86 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
|  | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/fileclient" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	defaultStorageAccountType      = string(storage.StandardLRS) | ||||
| 	defaultStorageAccountKind      = storage.StorageV2 | ||||
| 	fileShareAccountNamePrefix     = "f" | ||||
| 	sharedDiskAccountNamePrefix    = "ds" | ||||
| 	dedicatedDiskAccountNamePrefix = "dd" | ||||
| ) | ||||
|  | ||||
| // CreateFileShare creates a file share, using a matching storage account type, account kind, etc. | ||||
| // storage account will be created if specified account is not found | ||||
| func (az *Cloud) CreateFileShare(accountOptions *AccountOptions, shareOptions *fileclient.ShareOptions) (string, string, error) { | ||||
| 	if accountOptions == nil { | ||||
| 		return "", "", fmt.Errorf("account options is nil") | ||||
| 	} | ||||
| 	if shareOptions == nil { | ||||
| 		return "", "", fmt.Errorf("share options is nil") | ||||
| 	} | ||||
| 	if accountOptions.ResourceGroup == "" { | ||||
| 		accountOptions.ResourceGroup = az.resourceGroup | ||||
| 	} | ||||
|  | ||||
| 	accountOptions.EnableHTTPSTrafficOnly = true | ||||
| 	if shareOptions.Protocol == storage.NFS { | ||||
| 		accountOptions.EnableHTTPSTrafficOnly = false | ||||
| 	} | ||||
|  | ||||
| 	accountName, accountKey, err := az.EnsureStorageAccount(accountOptions, fileShareAccountNamePrefix) | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("could not get storage key for storage account %s: %v", accountOptions.Name, err) | ||||
| 	} | ||||
|  | ||||
| 	if err := az.createFileShare(accountOptions.ResourceGroup, accountName, shareOptions); err != nil { | ||||
| 		return "", "", fmt.Errorf("failed to create share %s in account %s: %v", shareOptions.Name, accountName, err) | ||||
| 	} | ||||
| 	klog.V(4).Infof("created share %s in account %s", shareOptions.Name, accountOptions.Name) | ||||
| 	return accountName, accountKey, nil | ||||
| } | ||||
|  | ||||
| // DeleteFileShare deletes a file share using storage account name and key | ||||
| func (az *Cloud) DeleteFileShare(resourceGroup, accountName, shareName string) error { | ||||
| 	if err := az.deleteFileShare(resourceGroup, accountName, shareName); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	klog.V(4).Infof("share %s deleted", shareName) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ResizeFileShare resizes a file share | ||||
| func (az *Cloud) ResizeFileShare(resourceGroup, accountName, name string, sizeGiB int) error { | ||||
| 	return az.resizeFileShare(resourceGroup, accountName, name, sizeGiB) | ||||
| } | ||||
|  | ||||
| // GetFileShare gets a file share | ||||
| func (az *Cloud) GetFileShare(resourceGroupName, accountName, name string) (storage.FileShare, error) { | ||||
| 	return az.getFileShare(resourceGroupName, accountName, name) | ||||
| } | ||||
| @@ -1,315 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
| 	"github.com/golang/mock/gomock" | ||||
|  | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/fileclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/fileclient/mockfileclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient" | ||||
| ) | ||||
|  | ||||
| func TestCreateFileShare(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{controllerCommon: &controllerCommon{resourceGroup: "rg"}} | ||||
| 	name := "baz" | ||||
| 	sku := "sku" | ||||
| 	kind := "StorageV2" | ||||
| 	location := "centralus" | ||||
| 	value := "foo key" | ||||
| 	bogus := "bogus" | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		rg       string | ||||
| 		name     string | ||||
| 		acct     string | ||||
| 		acctType string | ||||
| 		acctKind string | ||||
| 		loc      string | ||||
| 		gb       int | ||||
| 		accounts []storage.Account | ||||
| 		keys     storage.AccountListKeysResult | ||||
| 		err      error | ||||
|  | ||||
| 		expectErr  bool | ||||
| 		expectAcct string | ||||
| 		expectKey  string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:      "foo", | ||||
| 			acct:      "bar", | ||||
| 			acctType:  "type", | ||||
| 			acctKind:  "StorageV2", | ||||
| 			loc:       "eastus", | ||||
| 			gb:        10, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:      "foo", | ||||
| 			acct:      "", | ||||
| 			acctType:  "type", | ||||
| 			acctKind:  "StorageV2", | ||||
| 			loc:       "eastus", | ||||
| 			gb:        10, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "foo", | ||||
| 			acct:     "", | ||||
| 			acctType: sku, | ||||
| 			acctKind: kind, | ||||
| 			loc:      location, | ||||
| 			gb:       10, | ||||
| 			accounts: []storage.Account{ | ||||
| 				{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Kind: storage.Kind(kind), Location: &location}, | ||||
| 			}, | ||||
| 			keys: storage.AccountListKeysResult{ | ||||
| 				Keys: &[]storage.AccountKey{ | ||||
| 					{Value: &value}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectAcct: "baz", | ||||
| 			expectKey:  "key", | ||||
| 		}, | ||||
| 		{ | ||||
| 			rg:       "rg", | ||||
| 			name:     "foo", | ||||
| 			acct:     "", | ||||
| 			acctType: sku, | ||||
| 			acctKind: kind, | ||||
| 			loc:      location, | ||||
| 			gb:       10, | ||||
| 			accounts: []storage.Account{ | ||||
| 				{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Kind: storage.Kind(kind), Location: &location}, | ||||
| 			}, | ||||
| 			keys: storage.AccountListKeysResult{ | ||||
| 				Keys: &[]storage.AccountKey{ | ||||
| 					{Value: &value}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			err:       fmt.Errorf("create fileshare error"), | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "foo", | ||||
| 			acct:     "", | ||||
| 			acctType: sku, | ||||
| 			acctKind: kind, | ||||
| 			loc:      location, | ||||
| 			gb:       10, | ||||
| 			accounts: []storage.Account{ | ||||
| 				{Name: &bogus, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Location: &location}, | ||||
| 			}, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "foo", | ||||
| 			acct:     "", | ||||
| 			acctType: sku, | ||||
| 			acctKind: kind, | ||||
| 			loc:      location, | ||||
| 			gb:       10, | ||||
| 			accounts: []storage.Account{ | ||||
| 				{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Location: &bogus}, | ||||
| 			}, | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		mockFileClient := mockfileclient.NewMockInterface(ctrl) | ||||
| 		cloud.FileClient = mockFileClient | ||||
| 		mockFileClient.EXPECT().CreateFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.err).AnyTimes() | ||||
|  | ||||
| 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 		cloud.StorageAccountClient = mockStorageAccountsClient | ||||
| 		mockStorageAccountsClient.EXPECT().ListKeys(gomock.Any(), "rg", gomock.Any()).Return(test.keys, nil).AnyTimes() | ||||
| 		mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(test.accounts, nil).AnyTimes() | ||||
| 		mockStorageAccountsClient.EXPECT().Create(gomock.Any(), "rg", gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
|  | ||||
| 		mockAccount := &AccountOptions{ | ||||
| 			Name:          test.acct, | ||||
| 			Type:          test.acctType, | ||||
| 			Kind:          test.acctKind, | ||||
| 			ResourceGroup: test.rg, | ||||
| 			Location:      test.loc, | ||||
| 		} | ||||
|  | ||||
| 		mockFileShare := &fileclient.ShareOptions{ | ||||
| 			Name:       test.name, | ||||
| 			Protocol:   storage.SMB, | ||||
| 			RequestGiB: test.gb, | ||||
| 		} | ||||
|  | ||||
| 		account, key, err := cloud.CreateFileShare(mockAccount, mockFileShare) | ||||
| 		if test.expectErr && err == nil { | ||||
| 			t.Errorf("unexpected non-error") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		if test.expectAcct != account { | ||||
| 			t.Errorf("Expected: %s, got %s", test.expectAcct, account) | ||||
| 		} | ||||
| 		if test.expectKey != key { | ||||
| 			t.Errorf("Expected: %s, got %s", test.expectKey, key) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDeleteFileShare(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
| 	tests := []struct { | ||||
| 		rg   string | ||||
| 		acct string | ||||
| 		name string | ||||
|  | ||||
| 		err       error | ||||
| 		expectErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			rg:   "rg", | ||||
| 			acct: "bar", | ||||
| 			name: "foo", | ||||
|  | ||||
| 			expectErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			rg:   "rg", | ||||
| 			acct: "bar", | ||||
| 			name: "", | ||||
|  | ||||
| 			err:       fmt.Errorf("delete fileshare error"), | ||||
| 			expectErr: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		mockFileClient := mockfileclient.NewMockInterface(ctrl) | ||||
| 		cloud.FileClient = mockFileClient | ||||
| 		mockFileClient.EXPECT().DeleteFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.err).Times(1) | ||||
|  | ||||
| 		err := cloud.DeleteFileShare(test.rg, test.acct, test.name) | ||||
| 		if test.expectErr && err == nil { | ||||
| 			t.Errorf("unexpected non-error") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestResizeFileShare(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
| 	mockFileClient := mockfileclient.NewMockInterface(ctrl) | ||||
| 	mockFileClient.EXPECT().ResizeFileShare(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | ||||
| 	cloud.FileClient = mockFileClient | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		rg   string | ||||
| 		acct string | ||||
| 		name string | ||||
| 		gb   int | ||||
|  | ||||
| 		expectErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			rg:   "rg", | ||||
| 			acct: "bar", | ||||
| 			name: "foo", | ||||
| 			gb:   10, | ||||
|  | ||||
| 			expectErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 		cloud.StorageAccountClient = mockStorageAccountsClient | ||||
|  | ||||
| 		err := cloud.ResizeFileShare(test.rg, test.acct, test.name, test.gb) | ||||
| 		if test.expectErr && err == nil { | ||||
| 			t.Errorf("unexpected non-error") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetFileShare(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
| 	mockFileClient := mockfileclient.NewMockInterface(ctrl) | ||||
| 	mockFileClient.EXPECT().GetFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(storage.FileShare{}, nil).AnyTimes() | ||||
| 	cloud.FileClient = mockFileClient | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		rg   string | ||||
| 		acct string | ||||
| 		name string | ||||
|  | ||||
| 		expectErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			rg:   "rg", | ||||
| 			acct: "bar", | ||||
| 			name: "foo", | ||||
|  | ||||
| 			expectErr: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 		cloud.StorageAccountClient = mockStorageAccountsClient | ||||
|  | ||||
| 		_, err := cloud.GetFileShare(test.rg, test.acct, test.name) | ||||
| 		if test.expectErr && err == nil { | ||||
| 			t.Errorf("unexpected non-error") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,214 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
|  | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // AccountOptions contains the fields which are used to create storage account. | ||||
| type AccountOptions struct { | ||||
| 	Name, Type, Kind, ResourceGroup, Location string | ||||
| 	EnableHTTPSTrafficOnly                    bool | ||||
| 	Tags                                      map[string]string | ||||
| 	VirtualNetworkResourceIDs                 []string | ||||
| } | ||||
|  | ||||
| type accountWithLocation struct { | ||||
| 	Name, StorageType, Location string | ||||
| } | ||||
|  | ||||
| // getStorageAccounts get matching storage accounts | ||||
| func (az *Cloud) getStorageAccounts(accountOptions *AccountOptions) ([]accountWithLocation, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	result, rerr := az.StorageAccountClient.ListByResourceGroup(ctx, accountOptions.ResourceGroup) | ||||
| 	if rerr != nil { | ||||
| 		return nil, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	accounts := []accountWithLocation{} | ||||
| 	for _, acct := range result { | ||||
| 		if acct.Name != nil && acct.Location != nil && acct.Sku != nil { | ||||
| 			storageType := string((*acct.Sku).Name) | ||||
| 			if accountOptions.Type != "" && !strings.EqualFold(accountOptions.Type, storageType) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if accountOptions.Kind != "" && !strings.EqualFold(accountOptions.Kind, string(acct.Kind)) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			location := *acct.Location | ||||
| 			if accountOptions.Location != "" && !strings.EqualFold(accountOptions.Location, location) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if len(accountOptions.VirtualNetworkResourceIDs) > 0 { | ||||
| 				if acct.AccountProperties == nil || acct.AccountProperties.NetworkRuleSet == nil || | ||||
| 					acct.AccountProperties.NetworkRuleSet.VirtualNetworkRules == nil { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				found := false | ||||
| 				for _, subnetID := range accountOptions.VirtualNetworkResourceIDs { | ||||
| 					for _, rule := range *acct.AccountProperties.NetworkRuleSet.VirtualNetworkRules { | ||||
| 						if strings.EqualFold(pointer.StringDeref(rule.VirtualNetworkResourceID, ""), subnetID) && rule.Action == storage.Allow { | ||||
| 							found = true | ||||
| 							break | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 				if !found { | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			accounts = append(accounts, accountWithLocation{Name: *acct.Name, StorageType: storageType, Location: location}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return accounts, nil | ||||
| } | ||||
|  | ||||
| // GetStorageAccesskey gets the storage account access key | ||||
| func (az *Cloud) GetStorageAccesskey(account, resourceGroup string) (string, error) { | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	result, rerr := az.StorageAccountClient.ListKeys(ctx, resourceGroup, account) | ||||
| 	if rerr != nil { | ||||
| 		return "", rerr.Error() | ||||
| 	} | ||||
| 	if result.Keys == nil { | ||||
| 		return "", fmt.Errorf("empty keys") | ||||
| 	} | ||||
|  | ||||
| 	for _, k := range *result.Keys { | ||||
| 		if k.Value != nil && *k.Value != "" { | ||||
| 			v := *k.Value | ||||
| 			if ind := strings.LastIndex(v, " "); ind >= 0 { | ||||
| 				v = v[(ind + 1):] | ||||
| 			} | ||||
| 			return v, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return "", fmt.Errorf("no valid keys") | ||||
| } | ||||
|  | ||||
| // EnsureStorageAccount search storage account, create one storage account(with genAccountNamePrefix) if not found, return accountName, accountKey | ||||
| func (az *Cloud) EnsureStorageAccount(accountOptions *AccountOptions, genAccountNamePrefix string) (string, string, error) { | ||||
| 	if accountOptions == nil { | ||||
| 		return "", "", fmt.Errorf("account options is nil") | ||||
| 	} | ||||
| 	accountName := accountOptions.Name | ||||
| 	accountType := accountOptions.Type | ||||
| 	accountKind := accountOptions.Kind | ||||
| 	resourceGroup := accountOptions.ResourceGroup | ||||
| 	location := accountOptions.Location | ||||
| 	enableHTTPSTrafficOnly := accountOptions.EnableHTTPSTrafficOnly | ||||
|  | ||||
| 	if len(accountName) == 0 { | ||||
| 		// find a storage account that matches accountType | ||||
| 		accounts, err := az.getStorageAccounts(accountOptions) | ||||
| 		if err != nil { | ||||
| 			return "", "", fmt.Errorf("could not list storage accounts for account type %s: %v", accountType, err) | ||||
| 		} | ||||
|  | ||||
| 		if len(accounts) > 0 { | ||||
| 			accountName = accounts[0].Name | ||||
| 			klog.V(4).Infof("found a matching account %s type %s location %s", accounts[0].Name, accounts[0].StorageType, accounts[0].Location) | ||||
| 		} | ||||
|  | ||||
| 		if len(accountName) == 0 { | ||||
| 			// set network rules for storage account | ||||
| 			var networkRuleSet *storage.NetworkRuleSet | ||||
| 			virtualNetworkRules := []storage.VirtualNetworkRule{} | ||||
| 			for _, subnetID := range accountOptions.VirtualNetworkResourceIDs { | ||||
| 				vnetRule := storage.VirtualNetworkRule{ | ||||
| 					VirtualNetworkResourceID: &subnetID, | ||||
| 					Action:                   storage.Allow, | ||||
| 				} | ||||
| 				virtualNetworkRules = append(virtualNetworkRules, vnetRule) | ||||
| 				klog.V(4).Infof("subnetID(%s) has been set", subnetID) | ||||
| 			} | ||||
| 			if len(virtualNetworkRules) > 0 { | ||||
| 				networkRuleSet = &storage.NetworkRuleSet{ | ||||
| 					VirtualNetworkRules: &virtualNetworkRules, | ||||
| 					DefaultAction:       storage.DefaultActionDeny, | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// not found a matching account, now create a new account in current resource group | ||||
| 			accountName = generateStorageAccountName(genAccountNamePrefix) | ||||
| 			if location == "" { | ||||
| 				location = az.Location | ||||
| 			} | ||||
| 			if accountType == "" { | ||||
| 				accountType = defaultStorageAccountType | ||||
| 			} | ||||
|  | ||||
| 			// use StorageV2 by default per https://docs.microsoft.com/en-us/azure/storage/common/storage-account-options | ||||
| 			kind := defaultStorageAccountKind | ||||
| 			if accountKind != "" { | ||||
| 				kind = storage.Kind(accountKind) | ||||
| 			} | ||||
| 			if len(accountOptions.Tags) == 0 { | ||||
| 				accountOptions.Tags = make(map[string]string) | ||||
| 			} | ||||
| 			accountOptions.Tags["created-by"] = "azure" | ||||
| 			tags := convertMapToMapPointer(accountOptions.Tags) | ||||
|  | ||||
| 			klog.V(2).Infof("azure - no matching account found, begin to create a new account %s in resource group %s, location: %s, accountType: %s, accountKind: %s, tags: %+v", | ||||
| 				accountName, resourceGroup, location, accountType, kind, accountOptions.Tags) | ||||
|  | ||||
| 			cp := storage.AccountCreateParameters{ | ||||
| 				Sku:  &storage.Sku{Name: storage.SkuName(accountType)}, | ||||
| 				Kind: kind, | ||||
| 				AccountPropertiesCreateParameters: &storage.AccountPropertiesCreateParameters{ | ||||
| 					EnableHTTPSTrafficOnly: &enableHTTPSTrafficOnly, | ||||
| 					NetworkRuleSet:         networkRuleSet, | ||||
| 				}, | ||||
| 				Tags:     tags, | ||||
| 				Location: &location} | ||||
|  | ||||
| 			ctx, cancel := getContextWithCancel() | ||||
| 			defer cancel() | ||||
| 			rerr := az.StorageAccountClient.Create(ctx, resourceGroup, accountName, cp) | ||||
| 			if rerr != nil { | ||||
| 				return "", "", fmt.Errorf("failed to create storage account %s, error: %v", accountName, rerr) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// find the access key with this account | ||||
| 	accountKey, err := az.GetStorageAccesskey(accountName, resourceGroup) | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("could not get storage key for storage account %s: %v", accountName, err) | ||||
| 	} | ||||
|  | ||||
| 	return accountName, accountKey, nil | ||||
| } | ||||
| @@ -1,303 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| ) | ||||
|  | ||||
| func TestGetStorageAccessKeys(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
| 	value := "foo bar" | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		results     storage.AccountListKeysResult | ||||
| 		expectedKey string | ||||
| 		expectErr   bool | ||||
| 		err         error | ||||
| 	}{ | ||||
| 		{storage.AccountListKeysResult{}, "", true, nil}, | ||||
| 		{ | ||||
| 			storage.AccountListKeysResult{ | ||||
| 				Keys: &[]storage.AccountKey{ | ||||
| 					{Value: &value}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			"bar", | ||||
| 			false, | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			storage.AccountListKeysResult{ | ||||
| 				Keys: &[]storage.AccountKey{ | ||||
| 					{}, | ||||
| 					{Value: &value}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			"bar", | ||||
| 			false, | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{storage.AccountListKeysResult{}, "", true, fmt.Errorf("test error")}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 		cloud.StorageAccountClient = mockStorageAccountsClient | ||||
| 		mockStorageAccountsClient.EXPECT().ListKeys(gomock.Any(), "rg", gomock.Any()).Return(test.results, nil).AnyTimes() | ||||
| 		key, err := cloud.GetStorageAccesskey("acct", "rg") | ||||
| 		if test.expectErr && err == nil { | ||||
| 			t.Errorf("Unexpected non-error") | ||||
| 			continue | ||||
| 		} | ||||
| 		if !test.expectErr && err != nil { | ||||
| 			t.Errorf("Unexpected error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		if key != test.expectedKey { | ||||
| 			t.Errorf("expected: %s, saw %s", test.expectedKey, key) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetStorageAccount(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
|  | ||||
| 	name := "testAccount" | ||||
| 	location := "testLocation" | ||||
| 	networkID := "networkID" | ||||
| 	accountProperties := storage.AccountProperties{ | ||||
| 		NetworkRuleSet: &storage.NetworkRuleSet{ | ||||
| 			VirtualNetworkRules: &[]storage.VirtualNetworkRule{ | ||||
| 				{ | ||||
| 					VirtualNetworkResourceID: &networkID, | ||||
| 					Action:                   storage.Allow, | ||||
| 					State:                    "state", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}} | ||||
|  | ||||
| 	account := storage.Account{ | ||||
| 		Sku: &storage.Sku{ | ||||
| 			Name: "testSku", | ||||
| 			Tier: "testSkuTier", | ||||
| 		}, | ||||
| 		Kind:              "testKind", | ||||
| 		Location:          &location, | ||||
| 		Name:              &name, | ||||
| 		AccountProperties: &accountProperties, | ||||
| 	} | ||||
|  | ||||
| 	testResourceGroups := []storage.Account{account} | ||||
|  | ||||
| 	accountOptions := &AccountOptions{ | ||||
| 		ResourceGroup:             "rg", | ||||
| 		VirtualNetworkResourceIDs: []string{networkID}, | ||||
| 	} | ||||
|  | ||||
| 	mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 	cloud.StorageAccountClient = mockStorageAccountsClient | ||||
|  | ||||
| 	mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(testResourceGroups, nil).Times(1) | ||||
|  | ||||
| 	accountsWithLocations, err := cloud.getStorageAccounts(accountOptions) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if accountsWithLocations == nil { | ||||
| 		t.Error("unexpected error as returned accounts are nil") | ||||
| 	} | ||||
|  | ||||
| 	if len(accountsWithLocations) == 0 { | ||||
| 		t.Error("unexpected error as returned accounts slice is empty") | ||||
| 	} | ||||
|  | ||||
| 	expectedAccountWithLocation := accountWithLocation{ | ||||
| 		Name:        "testAccount", | ||||
| 		StorageType: "testSku", | ||||
| 		Location:    "testLocation", | ||||
| 	} | ||||
|  | ||||
| 	accountWithLocation := accountsWithLocations[0] | ||||
| 	if accountWithLocation.Name != expectedAccountWithLocation.Name { | ||||
| 		t.Errorf("expected %s, but was %s", accountWithLocation.Name, expectedAccountWithLocation.Name) | ||||
| 	} | ||||
|  | ||||
| 	if accountWithLocation.StorageType != expectedAccountWithLocation.StorageType { | ||||
| 		t.Errorf("expected %s, but was %s", accountWithLocation.StorageType, expectedAccountWithLocation.StorageType) | ||||
| 	} | ||||
|  | ||||
| 	if accountWithLocation.Location != expectedAccountWithLocation.Location { | ||||
| 		t.Errorf("expected %s, but was %s", accountWithLocation.Location, expectedAccountWithLocation.Location) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetStorageAccountEdgeCases(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	cloud := &Cloud{} | ||||
|  | ||||
| 	// default account with name, location, sku, kind | ||||
| 	name := "testAccount" | ||||
| 	location := "testLocation" | ||||
| 	sku := &storage.Sku{ | ||||
| 		Name: "testSku", | ||||
| 		Tier: "testSkuTier", | ||||
| 	} | ||||
| 	account := storage.Account{ | ||||
| 		Sku:      sku, | ||||
| 		Kind:     "testKind", | ||||
| 		Location: &location, | ||||
| 		Name:     &name, | ||||
| 	} | ||||
|  | ||||
| 	accountPropertiesWithoutNetworkRuleSet := storage.AccountProperties{NetworkRuleSet: nil} | ||||
| 	accountPropertiesWithoutVirtualNetworkRules := storage.AccountProperties{ | ||||
| 		NetworkRuleSet: &storage.NetworkRuleSet{ | ||||
| 			VirtualNetworkRules: nil, | ||||
| 		}} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		testCase           string | ||||
| 		testAccountOptions *AccountOptions | ||||
| 		testResourceGroups []storage.Account | ||||
| 		expectedResult     []accountWithLocation | ||||
| 		expectedError      error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			testCase: "account name is nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account location is nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{{Name: &name}}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account sku is nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{{Name: &name, Location: &location}}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options type is not empty and not equal account storage type", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 				Type:          "testAccountOptionsType", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{account}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options kind is not empty and not equal account type", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 				Kind:          "testAccountOptionsKind", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{account}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options location is not empty and not equal account location", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup: "rg", | ||||
| 				Location:      "testAccountOptionsLocation", | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{account}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options account properties are nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup:             "rg", | ||||
| 				VirtualNetworkResourceIDs: []string{"id"}, | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options account properties network rule set is nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup:             "rg", | ||||
| 				VirtualNetworkResourceIDs: []string{"id"}, | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{{Name: &name, Kind: "kind", Location: &location, Sku: sku, AccountProperties: &accountPropertiesWithoutNetworkRuleSet}}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			testCase: "account options account properties virtual network rule is nil", | ||||
| 			testAccountOptions: &AccountOptions{ | ||||
| 				ResourceGroup:             "rg", | ||||
| 				VirtualNetworkResourceIDs: []string{"id"}, | ||||
| 			}, | ||||
| 			testResourceGroups: []storage.Account{{Name: &name, Kind: "kind", Location: &location, Sku: sku, AccountProperties: &accountPropertiesWithoutVirtualNetworkRules}}, | ||||
| 			expectedResult:     []accountWithLocation{}, | ||||
| 			expectedError:      nil, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		t.Logf("running test case: %s", test.testCase) | ||||
| 		mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl) | ||||
| 		cloud.StorageAccountClient = mockStorageAccountsClient | ||||
|  | ||||
| 		mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(test.testResourceGroups, nil).AnyTimes() | ||||
|  | ||||
| 		accountsWithLocations, err := cloud.getStorageAccounts(test.testAccountOptions) | ||||
| 		if err != test.expectedError { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		if len(accountsWithLocations) != len(test.expectedResult) { | ||||
| 			t.Error("unexpected error as returned accounts slice is not empty") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,165 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	tagsDelimiter        = "," | ||||
| 	tagKeyValueDelimiter = "=" | ||||
| ) | ||||
|  | ||||
| // lockMap used to lock on entries | ||||
| type lockMap struct { | ||||
| 	sync.Mutex | ||||
| 	mutexMap map[string]*sync.Mutex | ||||
| } | ||||
|  | ||||
| // NewLockMap returns a new lock map | ||||
| func newLockMap() *lockMap { | ||||
| 	return &lockMap{ | ||||
| 		mutexMap: make(map[string]*sync.Mutex), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // LockEntry acquires a lock associated with the specific entry | ||||
| func (lm *lockMap) LockEntry(entry string) { | ||||
| 	lm.Lock() | ||||
| 	// check if entry does not exists, then add entry | ||||
| 	if _, exists := lm.mutexMap[entry]; !exists { | ||||
| 		lm.addEntry(entry) | ||||
| 	} | ||||
|  | ||||
| 	lm.Unlock() | ||||
| 	lm.lockEntry(entry) | ||||
| } | ||||
|  | ||||
| // UnlockEntry release the lock associated with the specific entry | ||||
| func (lm *lockMap) UnlockEntry(entry string) { | ||||
| 	lm.Lock() | ||||
| 	defer lm.Unlock() | ||||
|  | ||||
| 	if _, exists := lm.mutexMap[entry]; !exists { | ||||
| 		return | ||||
| 	} | ||||
| 	lm.unlockEntry(entry) | ||||
| } | ||||
|  | ||||
| func (lm *lockMap) addEntry(entry string) { | ||||
| 	lm.mutexMap[entry] = &sync.Mutex{} | ||||
| } | ||||
|  | ||||
| func (lm *lockMap) lockEntry(entry string) { | ||||
| 	lm.mutexMap[entry].Lock() | ||||
| } | ||||
|  | ||||
| func (lm *lockMap) unlockEntry(entry string) { | ||||
| 	lm.mutexMap[entry].Unlock() | ||||
| } | ||||
|  | ||||
| func getContextWithCancel() (context.Context, context.CancelFunc) { | ||||
| 	return context.WithCancel(context.Background()) | ||||
| } | ||||
|  | ||||
| // ConvertTagsToMap convert the tags from string to map | ||||
| // the valid tags format is "key1=value1,key2=value2", which could be converted to | ||||
| // {"key1": "value1", "key2": "value2"} | ||||
| func ConvertTagsToMap(tags string) (map[string]string, error) { | ||||
| 	m := make(map[string]string) | ||||
| 	if tags == "" { | ||||
| 		return m, nil | ||||
| 	} | ||||
| 	s := strings.Split(tags, tagsDelimiter) | ||||
| 	for _, tag := range s { | ||||
| 		kv := strings.Split(tag, tagKeyValueDelimiter) | ||||
| 		if len(kv) != 2 { | ||||
| 			return nil, fmt.Errorf("Tags '%s' are invalid, the format should like: 'key1=value1,key2=value2'", tags) | ||||
| 		} | ||||
| 		key := strings.TrimSpace(kv[0]) | ||||
| 		if key == "" { | ||||
| 			return nil, fmt.Errorf("Tags '%s' are invalid, the format should like: 'key1=value1,key2=value2'", tags) | ||||
| 		} | ||||
| 		value := strings.TrimSpace(kv[1]) | ||||
| 		m[key] = value | ||||
| 	} | ||||
|  | ||||
| 	return m, nil | ||||
| } | ||||
|  | ||||
| func convertMapToMapPointer(origin map[string]string) map[string]*string { | ||||
| 	newly := make(map[string]*string) | ||||
| 	for k, v := range origin { | ||||
| 		value := v | ||||
| 		newly[k] = &value | ||||
| 	} | ||||
| 	return newly | ||||
| } | ||||
|  | ||||
| func parseTags(tags string) map[string]*string { | ||||
| 	kvs := strings.Split(tags, ",") | ||||
| 	formatted := make(map[string]*string) | ||||
| 	for _, kv := range kvs { | ||||
| 		res := strings.Split(kv, "=") | ||||
| 		if len(res) != 2 { | ||||
| 			klog.Warningf("parseTags: error when parsing key-value pair %s, would ignore this one", kv) | ||||
| 			continue | ||||
| 		} | ||||
| 		k, v := strings.TrimSpace(res[0]), strings.TrimSpace(res[1]) | ||||
| 		if k == "" || v == "" { | ||||
| 			klog.Warningf("parseTags: error when parsing key-value pair %s-%s, would ignore this one", k, v) | ||||
| 			continue | ||||
| 		} | ||||
| 		formatted[strings.ToLower(k)] = pointer.String(v) | ||||
| 	} | ||||
| 	return formatted | ||||
| } | ||||
|  | ||||
| func findKeyInMapCaseInsensitive(targetMap map[string]*string, key string) (bool, string) { | ||||
| 	for k := range targetMap { | ||||
| 		if strings.EqualFold(k, key) { | ||||
| 			return true, k | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false, "" | ||||
| } | ||||
|  | ||||
| func reconcileTags(currentTagsOnResource, newTags map[string]*string) (reconciledTags map[string]*string, changed bool) { | ||||
| 	for k, v := range newTags { | ||||
| 		found, key := findKeyInMapCaseInsensitive(currentTagsOnResource, k) | ||||
| 		if !found { | ||||
| 			currentTagsOnResource[k] = v | ||||
| 			changed = true | ||||
| 		} else if !strings.EqualFold(pointer.StringDeref(v, ""), pointer.StringDeref(currentTagsOnResource[key], "")) { | ||||
| 			currentTagsOnResource[key] = v | ||||
| 			changed = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return currentTagsOnResource, changed | ||||
| } | ||||
| @@ -1,216 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestSimpleLockEntry(t *testing.T) { | ||||
| 	testLockMap := newLockMap() | ||||
|  | ||||
| 	callbackChan1 := make(chan interface{}) | ||||
| 	go testLockMap.lockAndCallback(t, "entry1", callbackChan1) | ||||
| 	ensureCallbackHappens(t, callbackChan1) | ||||
| } | ||||
|  | ||||
| func TestSimpleLockUnlockEntry(t *testing.T) { | ||||
| 	testLockMap := newLockMap() | ||||
|  | ||||
| 	callbackChan1 := make(chan interface{}) | ||||
| 	go testLockMap.lockAndCallback(t, "entry1", callbackChan1) | ||||
| 	ensureCallbackHappens(t, callbackChan1) | ||||
| 	testLockMap.UnlockEntry("entry1") | ||||
| } | ||||
|  | ||||
| func TestConcurrentLockEntry(t *testing.T) { | ||||
| 	testLockMap := newLockMap() | ||||
|  | ||||
| 	callbackChan1 := make(chan interface{}) | ||||
| 	callbackChan2 := make(chan interface{}) | ||||
|  | ||||
| 	go testLockMap.lockAndCallback(t, "entry1", callbackChan1) | ||||
| 	ensureCallbackHappens(t, callbackChan1) | ||||
|  | ||||
| 	go testLockMap.lockAndCallback(t, "entry1", callbackChan2) | ||||
| 	ensureNoCallback(t, callbackChan2) | ||||
|  | ||||
| 	testLockMap.UnlockEntry("entry1") | ||||
| 	ensureCallbackHappens(t, callbackChan2) | ||||
| 	testLockMap.UnlockEntry("entry1") | ||||
| } | ||||
|  | ||||
| func (lm *lockMap) lockAndCallback(t *testing.T, entry string, callbackChan chan<- interface{}) { | ||||
| 	lm.LockEntry(entry) | ||||
| 	callbackChan <- true | ||||
| } | ||||
|  | ||||
| var callbackTimeout = 2 * time.Second | ||||
|  | ||||
| func ensureCallbackHappens(t *testing.T, callbackChan <-chan interface{}) bool { | ||||
| 	select { | ||||
| 	case <-callbackChan: | ||||
| 		return true | ||||
| 	case <-time.After(callbackTimeout): | ||||
| 		t.Fatalf("timed out waiting for callback") | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ensureNoCallback(t *testing.T, callbackChan <-chan interface{}) bool { | ||||
| 	select { | ||||
| 	case <-callbackChan: | ||||
| 		t.Fatalf("unexpected callback") | ||||
| 		return false | ||||
| 	case <-time.After(callbackTimeout): | ||||
| 		return true | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConvertTagsToMap(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		desc           string | ||||
| 		tags           string | ||||
| 		expectedOutput map[string]string | ||||
| 		expectedError  bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:           "should return empty map when tag is empty", | ||||
| 			tags:           "", | ||||
| 			expectedOutput: map[string]string{}, | ||||
| 			expectedError:  false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "sing valid tag should be converted", | ||||
| 			tags: "key=value", | ||||
| 			expectedOutput: map[string]string{ | ||||
| 				"key": "value", | ||||
| 			}, | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "multiple valid tags should be converted", | ||||
| 			tags: "key1=value1,key2=value2", | ||||
| 			expectedOutput: map[string]string{ | ||||
| 				"key1": "value1", | ||||
| 				"key2": "value2", | ||||
| 			}, | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc: "whitespaces should be trimmed", | ||||
| 			tags: "key1=value1, key2=value2", | ||||
| 			expectedOutput: map[string]string{ | ||||
| 				"key1": "value1", | ||||
| 				"key2": "value2", | ||||
| 			}, | ||||
| 			expectedError: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:           "should return error for invalid format", | ||||
| 			tags:           "foo,bar", | ||||
| 			expectedOutput: nil, | ||||
| 			expectedError:  true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:           "should return error for when key is missed", | ||||
| 			tags:           "key1=value1,=bar", | ||||
| 			expectedOutput: nil, | ||||
| 			expectedError:  true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, c := range testCases { | ||||
| 		m, err := ConvertTagsToMap(c.tags) | ||||
| 		if c.expectedError { | ||||
| 			assert.NotNil(t, err, "TestCase[%d]: %s", i, c.desc) | ||||
| 		} else { | ||||
| 			assert.Nil(t, err, "TestCase[%d]: %s", i, c.desc) | ||||
| 			if !reflect.DeepEqual(m, c.expectedOutput) { | ||||
| 				t.Errorf("got: %v, expected: %v, desc: %v", m, c.expectedOutput, c.desc) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestReconcileTags(t *testing.T) { | ||||
| 	for _, testCase := range []struct { | ||||
| 		description                                  string | ||||
| 		currentTagsOnResource, newTags, expectedTags map[string]*string | ||||
| 		expectedChanged                              bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			description: "reconcileTags should add missing tags and update existing tags", | ||||
| 			currentTagsOnResource: map[string]*string{ | ||||
| 				"a": pointer.String("b"), | ||||
| 			}, | ||||
| 			newTags: map[string]*string{ | ||||
| 				"a": pointer.String("c"), | ||||
| 				"b": pointer.String("d"), | ||||
| 			}, | ||||
| 			expectedTags: map[string]*string{ | ||||
| 				"a": pointer.String("c"), | ||||
| 				"b": pointer.String("d"), | ||||
| 			}, | ||||
| 			expectedChanged: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "reconcileTags should ignore the case of keys when comparing", | ||||
| 			currentTagsOnResource: map[string]*string{ | ||||
| 				"A": pointer.String("b"), | ||||
| 				"c": pointer.String("d"), | ||||
| 			}, | ||||
| 			newTags: map[string]*string{ | ||||
| 				"a": pointer.String("b"), | ||||
| 				"C": pointer.String("d"), | ||||
| 			}, | ||||
| 			expectedTags: map[string]*string{ | ||||
| 				"A": pointer.String("b"), | ||||
| 				"c": pointer.String("d"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "reconcileTags should ignore the case of values when comparing", | ||||
| 			currentTagsOnResource: map[string]*string{ | ||||
| 				"A": pointer.String("b"), | ||||
| 				"c": pointer.String("d"), | ||||
| 			}, | ||||
| 			newTags: map[string]*string{ | ||||
| 				"a": pointer.String("B"), | ||||
| 				"C": pointer.String("D"), | ||||
| 			}, | ||||
| 			expectedTags: map[string]*string{ | ||||
| 				"A": pointer.String("b"), | ||||
| 				"c": pointer.String("d"), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} { | ||||
| 		t.Run(testCase.description, func(t *testing.T) { | ||||
| 			tags, changed := reconcileTags(testCase.currentTagsOnResource, testCase.newTags) | ||||
| 			assert.Equal(t, testCase.expectedChanged, changed) | ||||
| 			assert.Equal(t, testCase.expectedTags, tags) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -1,88 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| //go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=azure_vmsets.go  -destination=mockvmsets/azure_mock_vmsets.go -package=mockvmsets VMSet | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
|  | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| ) | ||||
|  | ||||
| // VMSet defines functions all vmsets (including scale set and availability | ||||
| // set) should be implemented. | ||||
|  | ||||
| type VMSet interface { | ||||
| 	// GetInstanceIDByNodeName gets the cloud provider ID by node name. | ||||
| 	// It must return ("", cloudprovider.InstanceNotFound) if the instance does | ||||
| 	// not exist or is no longer running. | ||||
| 	GetInstanceIDByNodeName(name string) (string, error) | ||||
| 	// GetInstanceTypeByNodeName gets the instance type by node name. | ||||
| 	GetInstanceTypeByNodeName(name string) (string, error) | ||||
| 	// GetIPByNodeName gets machine private IP and public IP by node name. | ||||
| 	GetIPByNodeName(name string) (string, string, error) | ||||
| 	// GetPrimaryInterface gets machine primary network interface by node name. | ||||
| 	GetPrimaryInterface(nodeName string) (network.Interface, error) | ||||
| 	// GetNodeNameByProviderID gets the node name by provider ID. | ||||
| 	GetNodeNameByProviderID(providerID string) (types.NodeName, error) | ||||
|  | ||||
| 	// GetZoneByNodeName gets cloudprovider.Zone by node name. | ||||
| 	GetZoneByNodeName(name string) (cloudprovider.Zone, error) | ||||
|  | ||||
| 	// GetPrimaryVMSetName returns the VM set name depending on the configured vmType. | ||||
| 	// It returns config.PrimaryScaleSetName for vmss and config.PrimaryAvailabilitySetName for standard vmType. | ||||
| 	GetPrimaryVMSetName() string | ||||
| 	// GetVMSetNames selects all possible availability sets or scale sets | ||||
| 	// (depending vmType configured) for service load balancer, if the service has | ||||
| 	// no loadbalancer mode annotation returns the primary VMSet. If service annotation | ||||
| 	// for loadbalancer exists then return the eligible VMSet. | ||||
| 	GetVMSetNames(service *v1.Service, nodes []*v1.Node) (availabilitySetNames *[]string, err error) | ||||
| 	// EnsureHostsInPool ensures the given Node's primary IP configurations are | ||||
| 	// participating in the specified LoadBalancer Backend Pool. | ||||
| 	EnsureHostsInPool(service *v1.Service, nodes []*v1.Node, backendPoolID string, vmSetName string, isInternal bool) error | ||||
| 	// EnsureHostInPool ensures the given VM's Primary NIC's Primary IP Configuration is | ||||
| 	// participating in the specified LoadBalancer Backend Pool. | ||||
| 	EnsureHostInPool(service *v1.Service, nodeName types.NodeName, backendPoolID string, vmSetName string, isInternal bool) (string, string, string, *compute.VirtualMachineScaleSetVM, error) | ||||
| 	// EnsureBackendPoolDeleted ensures the loadBalancer backendAddressPools deleted from the specified nodes. | ||||
| 	EnsureBackendPoolDeleted(service *v1.Service, backendPoolID, vmSetName string, backendAddressPools *[]network.BackendAddressPool, deleteFromVMSet bool) error | ||||
|  | ||||
| 	// AttachDisk attaches a vhd to vm. The vhd must exist, can be identified by diskName, diskURI, and lun. | ||||
| 	AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error | ||||
| 	// DetachDisk detaches a vhd from host. The vhd can be identified by diskName or diskURI. | ||||
| 	DetachDisk(diskName, diskURI string, nodeName types.NodeName) error | ||||
| 	// GetDataDisks gets a list of data disks attached to the node. | ||||
| 	GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) | ||||
|  | ||||
| 	// GetPowerStatusByNodeName returns the power state of the specified node. | ||||
| 	GetPowerStatusByNodeName(name string) (string, error) | ||||
|  | ||||
| 	// GetProvisioningStateByNodeName returns the provisioningState for the specified node. | ||||
| 	GetProvisioningStateByNodeName(name string) (string, error) | ||||
|  | ||||
| 	// GetPrivateIPsByNodeName returns a slice of all private ips assigned to node (ipv6 and ipv4) | ||||
| 	GetPrivateIPsByNodeName(name string) ([]string, error) | ||||
|  | ||||
| 	// GetNodeNameByIPConfigurationID gets the nodeName and vmSetName by IP configuration ID. | ||||
| 	GetNodeNameByIPConfigurationID(ipConfigurationID string) (string, string, error) | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,350 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	vmssNameSeparator = "_" | ||||
|  | ||||
| 	vmssKey                 = "k8svmssKey" | ||||
| 	availabilitySetNodesKey = "k8sAvailabilitySetNodesKey" | ||||
|  | ||||
| 	availabilitySetNodesCacheTTLDefaultInSeconds = 900 | ||||
| 	vmssCacheTTLDefaultInSeconds                 = 600 | ||||
| 	vmssVirtualMachinesCacheTTLDefaultInSeconds  = 600 | ||||
| ) | ||||
|  | ||||
| type vmssVirtualMachinesEntry struct { | ||||
| 	resourceGroup  string | ||||
| 	vmssName       string | ||||
| 	instanceID     string | ||||
| 	virtualMachine *compute.VirtualMachineScaleSetVM | ||||
| 	lastUpdate     time.Time | ||||
| } | ||||
|  | ||||
| type vmssEntry struct { | ||||
| 	vmss          *compute.VirtualMachineScaleSet | ||||
| 	resourceGroup string | ||||
| 	lastUpdate    time.Time | ||||
| } | ||||
|  | ||||
| type availabilitySetEntry struct { | ||||
| 	vmNames   sets.String | ||||
| 	nodeNames sets.String | ||||
| } | ||||
|  | ||||
| func (ss *scaleSet) newVMSSCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		localCache := &sync.Map{} // [vmssName]*vmssEntry | ||||
|  | ||||
| 		allResourceGroups, err := ss.GetResourceGroups() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		for _, resourceGroup := range allResourceGroups.List() { | ||||
| 			allScaleSets, rerr := ss.VirtualMachineScaleSetsClient.List(context.Background(), resourceGroup) | ||||
| 			if rerr != nil { | ||||
| 				klog.Errorf("VirtualMachineScaleSetsClient.List failed: %v", rerr) | ||||
| 				return nil, rerr.Error() | ||||
| 			} | ||||
|  | ||||
| 			for i := range allScaleSets { | ||||
| 				scaleSet := allScaleSets[i] | ||||
| 				if scaleSet.Name == nil || *scaleSet.Name == "" { | ||||
| 					klog.Warning("failed to get the name of VMSS") | ||||
| 					continue | ||||
| 				} | ||||
| 				localCache.Store(*scaleSet.Name, &vmssEntry{ | ||||
| 					vmss:          &scaleSet, | ||||
| 					resourceGroup: resourceGroup, | ||||
| 					lastUpdate:    time.Now().UTC(), | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return localCache, nil | ||||
| 	} | ||||
|  | ||||
| 	if ss.Config.VmssCacheTTLInSeconds == 0 { | ||||
| 		ss.Config.VmssCacheTTLInSeconds = vmssCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(ss.Config.VmssCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func extractVmssVMName(name string) (string, string, error) { | ||||
| 	split := strings.SplitAfter(name, vmssNameSeparator) | ||||
| 	if len(split) < 2 { | ||||
| 		klog.V(3).Infof("Failed to extract vmssVMName %q", name) | ||||
| 		return "", "", ErrorNotVmssInstance | ||||
| 	} | ||||
|  | ||||
| 	ssName := strings.Join(split[0:len(split)-1], "") | ||||
| 	// removing the trailing `vmssNameSeparator` since we used SplitAfter | ||||
| 	ssName = ssName[:len(ssName)-1] | ||||
| 	instanceID := split[len(split)-1] | ||||
| 	return ssName, instanceID, nil | ||||
| } | ||||
|  | ||||
| // getVMSSVMCache returns an *azcache.TimedCache and cache key for a VMSS (creating that cache if new). | ||||
| func (ss *scaleSet) getVMSSVMCache(resourceGroup, vmssName string) (string, *azcache.TimedCache, error) { | ||||
| 	cacheKey := strings.ToLower(fmt.Sprintf("%s/%s", resourceGroup, vmssName)) | ||||
| 	if entry, ok := ss.vmssVMCache.Load(cacheKey); ok { | ||||
| 		cache := entry.(*azcache.TimedCache) | ||||
| 		return cacheKey, cache, nil | ||||
| 	} | ||||
|  | ||||
| 	cache, err := ss.newVMSSVirtualMachinesCache(resourceGroup, vmssName, cacheKey) | ||||
| 	if err != nil { | ||||
| 		return "", nil, err | ||||
| 	} | ||||
| 	ss.vmssVMCache.Store(cacheKey, cache) | ||||
| 	return cacheKey, cache, nil | ||||
| } | ||||
|  | ||||
| // gcVMSSVMCache delete stale VMSS VMs caches from deleted VMSSes. | ||||
| func (ss *scaleSet) gcVMSSVMCache() error { | ||||
| 	cached, err := ss.vmssCache.Get(vmssKey, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	vmsses := cached.(*sync.Map) | ||||
| 	removed := map[string]bool{} | ||||
| 	ss.vmssVMCache.Range(func(key, value interface{}) bool { | ||||
| 		cacheKey := key.(string) | ||||
| 		vlistIdx := cacheKey[strings.LastIndex(cacheKey, "/")+1:] | ||||
| 		if _, ok := vmsses.Load(vlistIdx); !ok { | ||||
| 			removed[cacheKey] = true | ||||
| 		} | ||||
| 		return true | ||||
| 	}) | ||||
|  | ||||
| 	for key := range removed { | ||||
| 		ss.vmssVMCache.Delete(key) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // newVMSSVirtualMachinesCache instantiates a new VMs cache for VMs belonging to the provided VMSS. | ||||
| func (ss *scaleSet) newVMSSVirtualMachinesCache(resourceGroupName, vmssName, cacheKey string) (*azcache.TimedCache, error) { | ||||
| 	vmssVirtualMachinesCacheTTL := time.Duration(ss.Config.VmssVirtualMachinesCacheTTLInSeconds) * time.Second | ||||
|  | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		localCache := &sync.Map{} // [nodeName]*vmssVirtualMachinesEntry | ||||
|  | ||||
| 		oldCache := make(map[string]vmssVirtualMachinesEntry) | ||||
|  | ||||
| 		if vmssCache, ok := ss.vmssVMCache.Load(cacheKey); ok { | ||||
| 			// get old cache before refreshing the cache | ||||
| 			cache := vmssCache.(*azcache.TimedCache) | ||||
| 			entry, exists, err := cache.Store.GetByKey(cacheKey) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			if exists { | ||||
| 				cached := entry.(*azcache.AzureCacheEntry).Data | ||||
| 				if cached != nil { | ||||
| 					virtualMachines := cached.(*sync.Map) | ||||
| 					virtualMachines.Range(func(key, value interface{}) bool { | ||||
| 						oldCache[key.(string)] = *value.(*vmssVirtualMachinesEntry) | ||||
| 						return true | ||||
| 					}) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		vms, err := ss.listScaleSetVMs(vmssName, resourceGroupName) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		for i := range vms { | ||||
| 			vm := vms[i] | ||||
| 			if vm.OsProfile == nil || vm.OsProfile.ComputerName == nil { | ||||
| 				klog.Warningf("failed to get computerName for vmssVM (%q)", vmssName) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			computerName := strings.ToLower(*vm.OsProfile.ComputerName) | ||||
| 			if vm.NetworkProfile == nil || vm.NetworkProfile.NetworkInterfaces == nil { | ||||
| 				klog.Warningf("skip caching vmssVM %s since its network profile hasn't initialized yet (probably still under creating)", computerName) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			vmssVMCacheEntry := &vmssVirtualMachinesEntry{ | ||||
| 				resourceGroup:  resourceGroupName, | ||||
| 				vmssName:       vmssName, | ||||
| 				instanceID:     pointer.StringDeref(vm.InstanceID, ""), | ||||
| 				virtualMachine: &vm, | ||||
| 				lastUpdate:     time.Now().UTC(), | ||||
| 			} | ||||
| 			// set cache entry to nil when the VM is under deleting. | ||||
| 			if vm.VirtualMachineScaleSetVMProperties != nil && | ||||
| 				strings.EqualFold(pointer.StringDeref(vm.VirtualMachineScaleSetVMProperties.ProvisioningState, ""), string(compute.ProvisioningStateDeleting)) { | ||||
| 				klog.V(4).Infof("VMSS virtualMachine %q is under deleting, setting its cache to nil", computerName) | ||||
| 				vmssVMCacheEntry.virtualMachine = nil | ||||
| 			} | ||||
| 			localCache.Store(computerName, vmssVMCacheEntry) | ||||
|  | ||||
| 			delete(oldCache, computerName) | ||||
| 		} | ||||
|  | ||||
| 		// add old missing cache data with nil entries to prevent aggressive | ||||
| 		// ARM calls during cache invalidation | ||||
| 		for name, vmEntry := range oldCache { | ||||
| 			// if the nil cache entry has existed for vmssVirtualMachinesCacheTTL in the cache | ||||
| 			// then it should not be added back to the cache | ||||
| 			if vmEntry.virtualMachine == nil && time.Since(vmEntry.lastUpdate) > vmssVirtualMachinesCacheTTL { | ||||
| 				klog.V(5).Infof("ignoring expired entries from old cache for %s", name) | ||||
| 				continue | ||||
| 			} | ||||
| 			lastUpdate := time.Now().UTC() | ||||
| 			if vmEntry.virtualMachine == nil { | ||||
| 				// if this is already a nil entry then keep the time the nil | ||||
| 				// entry was first created, so we can cleanup unwanted entries | ||||
| 				lastUpdate = vmEntry.lastUpdate | ||||
| 			} | ||||
|  | ||||
| 			klog.V(5).Infof("adding old entries to new cache for %s", name) | ||||
| 			localCache.Store(name, &vmssVirtualMachinesEntry{ | ||||
| 				resourceGroup:  vmEntry.resourceGroup, | ||||
| 				vmssName:       vmEntry.vmssName, | ||||
| 				instanceID:     vmEntry.instanceID, | ||||
| 				virtualMachine: nil, | ||||
| 				lastUpdate:     lastUpdate, | ||||
| 			}) | ||||
| 		} | ||||
|  | ||||
| 		return localCache, nil | ||||
| 	} | ||||
|  | ||||
| 	return azcache.NewTimedcache(vmssVirtualMachinesCacheTTL, getter) | ||||
| } | ||||
|  | ||||
| func (ss *scaleSet) deleteCacheForNode(nodeName string) error { | ||||
| 	node, err := ss.getNodeIdentityByNodeName(nodeName, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	cacheKey, timedcache, err := ss.getVMSSVMCache(node.resourceGroup, node.vmssName) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	vmcache, err := timedcache.Get(cacheKey, azcache.CacheReadTypeUnsafe) | ||||
| 	if err != nil { | ||||
| 		klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err) | ||||
| 		return err | ||||
| 	} | ||||
| 	virtualMachines := vmcache.(*sync.Map) | ||||
| 	virtualMachines.Delete(nodeName) | ||||
|  | ||||
| 	if err := ss.gcVMSSVMCache(); err != nil { | ||||
| 		klog.Errorf("deleteCacheForNode(%s) failed to gc stale vmss caches: %v", nodeName, err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (ss *scaleSet) newAvailabilitySetNodesCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		vmNames := sets.NewString() | ||||
| 		resourceGroups, err := ss.GetResourceGroups() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		for _, resourceGroup := range resourceGroups.List() { | ||||
| 			vmList, err := ss.Cloud.ListVirtualMachines(resourceGroup) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			for _, vm := range vmList { | ||||
| 				if vm.Name != nil { | ||||
| 					vmNames.Insert(*vm.Name) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// store all the node names in the cluster when the cache data was created. | ||||
| 		nodeNames, err := ss.GetNodeNames() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		localCache := availabilitySetEntry{ | ||||
| 			vmNames:   vmNames, | ||||
| 			nodeNames: nodeNames, | ||||
| 		} | ||||
|  | ||||
| 		return localCache, nil | ||||
| 	} | ||||
|  | ||||
| 	if ss.Config.AvailabilitySetNodesCacheTTLInSeconds == 0 { | ||||
| 		ss.Config.AvailabilitySetNodesCacheTTLInSeconds = availabilitySetNodesCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(ss.Config.AvailabilitySetNodesCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func (ss *scaleSet) isNodeManagedByAvailabilitySet(nodeName string, crt azcache.AzureCacheReadType) (bool, error) { | ||||
| 	// Assume all nodes are managed by VMSS when DisableAvailabilitySetNodes is enabled. | ||||
| 	if ss.DisableAvailabilitySetNodes { | ||||
| 		klog.V(2).Infof("Assuming node %q is managed by VMSS since DisableAvailabilitySetNodes is set to true", nodeName) | ||||
| 		return false, nil | ||||
| 	} | ||||
|  | ||||
| 	cached, err := ss.availabilitySetNodesCache.Get(availabilitySetNodesKey, crt) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	cachedNodes := cached.(availabilitySetEntry).nodeNames | ||||
| 	// if the node is not in the cache, assume the node has joined after the last cache refresh and attempt to refresh the cache. | ||||
| 	if !cachedNodes.Has(nodeName) { | ||||
| 		klog.V(2).Infof("Node %s has joined the cluster since the last VM cache refresh, refreshing the cache", nodeName) | ||||
| 		cached, err = ss.availabilitySetNodesCache.Get(availabilitySetNodesKey, azcache.CacheReadTypeForceRefresh) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	cachedVMs := cached.(availabilitySetEntry).vmNames | ||||
| 	return cachedVMs.Has(nodeName), nil | ||||
| } | ||||
| @@ -1,169 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"sync" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestExtractVmssVMName(t *testing.T) { | ||||
| 	cases := []struct { | ||||
| 		description        string | ||||
| 		vmName             string | ||||
| 		expectError        bool | ||||
| 		expectedScaleSet   string | ||||
| 		expectedInstanceID string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			description: "wrong vmss VM name should report error", | ||||
| 			vmName:      "vm1234", | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description: "wrong VM name separator should report error", | ||||
| 			vmName:      "vm-1234", | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			description:        "correct vmss VM name should return correct scaleSet and instanceID", | ||||
| 			vmName:             "vm_1234", | ||||
| 			expectedScaleSet:   "vm", | ||||
| 			expectedInstanceID: "1234", | ||||
| 		}, | ||||
| 		{ | ||||
| 			description:        "correct vmss VM name with Extra Separator should return correct scaleSet and instanceID", | ||||
| 			vmName:             "vm_test_1234", | ||||
| 			expectedScaleSet:   "vm_test", | ||||
| 			expectedInstanceID: "1234", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, c := range cases { | ||||
| 		ssName, instanceID, err := extractVmssVMName(c.vmName) | ||||
| 		if c.expectError { | ||||
| 			assert.Error(t, err, c.description) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		assert.Equal(t, c.expectedScaleSet, ssName, c.description) | ||||
| 		assert.Equal(t, c.expectedInstanceID, instanceID, c.description) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestVMSSVMCache(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	vmssName := "vmss" | ||||
| 	vmList := []string{"vmssee6c2000000", "vmssee6c2000001", "vmssee6c2000002"} | ||||
| 	ss, err := newTestScaleSet(ctrl) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	mockVMSSClient := mockvmssclient.NewMockInterface(ctrl) | ||||
| 	mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl) | ||||
| 	ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient | ||||
| 	ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient | ||||
|  | ||||
| 	expectedScaleSet := buildTestVMSS(vmssName, "vmssee6c2") | ||||
| 	mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, nil).AnyTimes() | ||||
|  | ||||
| 	expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, vmssName, "", 0, vmList, "", false) | ||||
| 	mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, nil).AnyTimes() | ||||
|  | ||||
| 	// validate getting VMSS VM via cache. | ||||
| 	for i := range expectedVMs { | ||||
| 		vm := expectedVMs[i] | ||||
| 		vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "") | ||||
| 		ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "vmss", ssName) | ||||
| 		assert.Equal(t, pointer.StringDeref(vm.InstanceID, ""), instanceID) | ||||
| 		assert.Equal(t, &vm, realVM) | ||||
| 	} | ||||
|  | ||||
| 	// validate deleteCacheForNode(). | ||||
| 	vm := expectedVMs[0] | ||||
| 	vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "") | ||||
| 	err = ss.deleteCacheForNode(vmName) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	// the VM should be removed from cache after deleteCacheForNode(). | ||||
| 	cacheKey, cache, err := ss.getVMSSVMCache("rg", vmssName) | ||||
| 	assert.NoError(t, err) | ||||
| 	cached, err := cache.Get(cacheKey, azcache.CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	cachedVirtualMachines := cached.(*sync.Map) | ||||
| 	_, ok := cachedVirtualMachines.Load(vmName) | ||||
| 	assert.Equal(t, false, ok) | ||||
|  | ||||
| 	// the VM should be back after another cache refresh. | ||||
| 	ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, "vmss", ssName) | ||||
| 	assert.Equal(t, pointer.StringDeref(vm.InstanceID, ""), instanceID) | ||||
| 	assert.Equal(t, &vm, realVM) | ||||
| } | ||||
|  | ||||
| func TestVMSSVMCacheWithDeletingNodes(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	vmssName := "vmss" | ||||
| 	vmList := []string{"vmssee6c2000000", "vmssee6c2000001", "vmssee6c2000002"} | ||||
| 	ss, err := newTestScaleSetWithState(ctrl) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	mockVMSSClient := mockvmssclient.NewMockInterface(ctrl) | ||||
| 	mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl) | ||||
| 	ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient | ||||
| 	ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient | ||||
|  | ||||
| 	expectedScaleSet := compute.VirtualMachineScaleSet{ | ||||
| 		Name:                             &vmssName, | ||||
| 		VirtualMachineScaleSetProperties: &compute.VirtualMachineScaleSetProperties{}, | ||||
| 	} | ||||
| 	mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, nil).AnyTimes() | ||||
|  | ||||
| 	expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, vmssName, "", 0, vmList, string(compute.ProvisioningStateDeleting), false) | ||||
| 	mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, nil).AnyTimes() | ||||
|  | ||||
| 	for i := range expectedVMs { | ||||
| 		vm := expectedVMs[i] | ||||
| 		vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "") | ||||
| 		assert.Equal(t, vm.ProvisioningState, pointer.String(string(compute.ProvisioningStateDeleting))) | ||||
|  | ||||
| 		ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault) | ||||
| 		assert.Nil(t, realVM) | ||||
| 		assert.Equal(t, "", ssName) | ||||
| 		assert.Equal(t, instanceID, ssName) | ||||
| 		assert.Equal(t, cloudprovider.InstanceNotFound, err) | ||||
| 	} | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,356 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	vmCacheTTLDefaultInSeconds           = 60 | ||||
| 	loadBalancerCacheTTLDefaultInSeconds = 120 | ||||
| 	nsgCacheTTLDefaultInSeconds          = 120 | ||||
| 	routeTableCacheTTLDefaultInSeconds   = 120 | ||||
|  | ||||
| 	azureNodeProviderIDRE    = regexp.MustCompile(`^azure:///subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/(?:.*)`) | ||||
| 	azureResourceGroupNameRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(.+)/providers/(?:.*)`) | ||||
| ) | ||||
|  | ||||
| // checkExistsFromError inspects an error and returns a true if err is nil, | ||||
| // false if error is an autorest.Error with StatusCode=404 and will return the | ||||
| // error back if error is another status code or another type of error. | ||||
| func checkResourceExistsFromError(err *retry.Error) (bool, *retry.Error) { | ||||
| 	if err == nil { | ||||
| 		return true, nil | ||||
| 	} | ||||
|  | ||||
| 	if err.HTTPStatusCode == http.StatusNotFound { | ||||
| 		return false, nil | ||||
| 	} | ||||
|  | ||||
| 	return false, err | ||||
| } | ||||
|  | ||||
| // / getVirtualMachine calls 'VirtualMachinesClient.Get' with a timed cache | ||||
| // / The service side has throttling control that delays responses if there are multiple requests onto certain vm | ||||
| // / resource request in short period. | ||||
| func (az *Cloud) getVirtualMachine(nodeName types.NodeName, crt azcache.AzureCacheReadType) (vm compute.VirtualMachine, err error) { | ||||
| 	vmName := string(nodeName) | ||||
| 	cachedVM, err := az.vmCache.Get(vmName, crt) | ||||
| 	if err != nil { | ||||
| 		return vm, err | ||||
| 	} | ||||
|  | ||||
| 	if cachedVM == nil { | ||||
| 		return vm, cloudprovider.InstanceNotFound | ||||
| 	} | ||||
|  | ||||
| 	return *(cachedVM.(*compute.VirtualMachine)), nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getRouteTable(crt azcache.AzureCacheReadType) (routeTable network.RouteTable, exists bool, err error) { | ||||
| 	cachedRt, err := az.rtCache.Get(az.RouteTableName, crt) | ||||
| 	if err != nil { | ||||
| 		return routeTable, false, err | ||||
| 	} | ||||
|  | ||||
| 	if cachedRt == nil { | ||||
| 		return routeTable, false, nil | ||||
| 	} | ||||
|  | ||||
| 	return *(cachedRt.(*network.RouteTable)), true, nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getPublicIPAddress(pipResourceGroup string, pipName string) (network.PublicIPAddress, bool, error) { | ||||
| 	resourceGroup := az.ResourceGroup | ||||
| 	if pipResourceGroup != "" { | ||||
| 		resourceGroup = pipResourceGroup | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	pip, err := az.PublicIPAddressesClient.Get(ctx, resourceGroup, pipName, "") | ||||
| 	exists, rerr := checkResourceExistsFromError(err) | ||||
| 	if rerr != nil { | ||||
| 		return pip, false, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	if !exists { | ||||
| 		klog.V(2).Infof("Public IP %q not found", pipName) | ||||
| 		return pip, false, nil | ||||
| 	} | ||||
|  | ||||
| 	return pip, exists, nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getSubnet(virtualNetworkName string, subnetName string) (network.Subnet, bool, error) { | ||||
| 	var rg string | ||||
| 	if len(az.VnetResourceGroup) > 0 { | ||||
| 		rg = az.VnetResourceGroup | ||||
| 	} else { | ||||
| 		rg = az.ResourceGroup | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := getContextWithCancel() | ||||
| 	defer cancel() | ||||
| 	subnet, err := az.SubnetsClient.Get(ctx, rg, virtualNetworkName, subnetName, "") | ||||
| 	exists, rerr := checkResourceExistsFromError(err) | ||||
| 	if rerr != nil { | ||||
| 		return subnet, false, rerr.Error() | ||||
| 	} | ||||
|  | ||||
| 	if !exists { | ||||
| 		klog.V(2).Infof("Subnet %q not found", subnetName) | ||||
| 		return subnet, false, nil | ||||
| 	} | ||||
|  | ||||
| 	return subnet, exists, nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getAzureLoadBalancer(name string, crt azcache.AzureCacheReadType) (lb network.LoadBalancer, exists bool, err error) { | ||||
| 	cachedLB, err := az.lbCache.Get(name, crt) | ||||
| 	if err != nil { | ||||
| 		return lb, false, err | ||||
| 	} | ||||
|  | ||||
| 	if cachedLB == nil { | ||||
| 		return lb, false, nil | ||||
| 	} | ||||
|  | ||||
| 	return *(cachedLB.(*network.LoadBalancer)), true, nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) getSecurityGroup(crt azcache.AzureCacheReadType) (network.SecurityGroup, error) { | ||||
| 	nsg := network.SecurityGroup{} | ||||
| 	if az.SecurityGroupName == "" { | ||||
| 		return nsg, fmt.Errorf("securityGroupName is not configured") | ||||
| 	} | ||||
|  | ||||
| 	securityGroup, err := az.nsgCache.Get(az.SecurityGroupName, crt) | ||||
| 	if err != nil { | ||||
| 		return nsg, err | ||||
| 	} | ||||
|  | ||||
| 	if securityGroup == nil { | ||||
| 		return nsg, fmt.Errorf("nsg %q not found", az.SecurityGroupName) | ||||
| 	} | ||||
|  | ||||
| 	return *(securityGroup.(*network.SecurityGroup)), nil | ||||
| } | ||||
|  | ||||
| func (az *Cloud) newVMCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		// Currently InstanceView request are used by azure_zones, while the calls come after non-InstanceView | ||||
| 		// request. If we first send an InstanceView request and then a non InstanceView request, the second | ||||
| 		// request will still hit throttling. This is what happens now for cloud controller manager: In this | ||||
| 		// case we do get instance view every time to fulfill the azure_zones requirement without hitting | ||||
| 		// throttling. | ||||
| 		// Consider adding separate parameter for controlling 'InstanceView' once node update issue #56276 is fixed | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
|  | ||||
| 		resourceGroup, err := az.GetNodeResourceGroup(key) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		vm, verr := az.VirtualMachinesClient.Get(ctx, resourceGroup, key, compute.InstanceView) | ||||
| 		exists, rerr := checkResourceExistsFromError(verr) | ||||
| 		if rerr != nil { | ||||
| 			return nil, rerr.Error() | ||||
| 		} | ||||
|  | ||||
| 		if !exists { | ||||
| 			klog.V(2).Infof("Virtual machine %q not found", key) | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		if vm.VirtualMachineProperties != nil && | ||||
| 			strings.EqualFold(pointer.StringDeref(vm.VirtualMachineProperties.ProvisioningState, ""), string(compute.ProvisioningStateDeleting)) { | ||||
| 			klog.V(2).Infof("Virtual machine %q is under deleting", key) | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return &vm, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.VMCacheTTLInSeconds == 0 { | ||||
| 		az.VMCacheTTLInSeconds = vmCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(az.VMCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) newLBCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
|  | ||||
| 		lb, err := az.LoadBalancerClient.Get(ctx, az.getLoadBalancerResourceGroup(), key, "") | ||||
| 		exists, rerr := checkResourceExistsFromError(err) | ||||
| 		if rerr != nil { | ||||
| 			return nil, rerr.Error() | ||||
| 		} | ||||
|  | ||||
| 		if !exists { | ||||
| 			klog.V(2).Infof("Load balancer %q not found", key) | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return &lb, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.LoadBalancerCacheTTLInSeconds == 0 { | ||||
| 		az.LoadBalancerCacheTTLInSeconds = loadBalancerCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(az.LoadBalancerCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) newNSGCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
| 		nsg, err := az.SecurityGroupsClient.Get(ctx, az.SecurityGroupResourceGroup, key, "") | ||||
| 		exists, rerr := checkResourceExistsFromError(err) | ||||
| 		if rerr != nil { | ||||
| 			return nil, rerr.Error() | ||||
| 		} | ||||
|  | ||||
| 		if !exists { | ||||
| 			klog.V(2).Infof("Security group %q not found", key) | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return &nsg, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.NsgCacheTTLInSeconds == 0 { | ||||
| 		az.NsgCacheTTLInSeconds = nsgCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(az.NsgCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) newRouteTableCache() (*azcache.TimedCache, error) { | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		ctx, cancel := getContextWithCancel() | ||||
| 		defer cancel() | ||||
| 		rt, err := az.RouteTablesClient.Get(ctx, az.RouteTableResourceGroup, key, "") | ||||
| 		exists, rerr := checkResourceExistsFromError(err) | ||||
| 		if rerr != nil { | ||||
| 			return nil, rerr.Error() | ||||
| 		} | ||||
|  | ||||
| 		if !exists { | ||||
| 			klog.V(2).Infof("Route table %q not found", key) | ||||
| 			return nil, nil | ||||
| 		} | ||||
|  | ||||
| 		return &rt, nil | ||||
| 	} | ||||
|  | ||||
| 	if az.RouteTableCacheTTLInSeconds == 0 { | ||||
| 		az.RouteTableCacheTTLInSeconds = routeTableCacheTTLDefaultInSeconds | ||||
| 	} | ||||
| 	return azcache.NewTimedcache(time.Duration(az.RouteTableCacheTTLInSeconds)*time.Second, getter) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) useStandardLoadBalancer() bool { | ||||
| 	return strings.EqualFold(az.LoadBalancerSku, loadBalancerSkuStandard) | ||||
| } | ||||
|  | ||||
| func (az *Cloud) excludeMasterNodesFromStandardLB() bool { | ||||
| 	return az.ExcludeMasterFromStandardLB != nil && *az.ExcludeMasterFromStandardLB | ||||
| } | ||||
|  | ||||
| func (az *Cloud) disableLoadBalancerOutboundSNAT() bool { | ||||
| 	if !az.useStandardLoadBalancer() || az.DisableOutboundSNAT == nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return *az.DisableOutboundSNAT | ||||
| } | ||||
|  | ||||
| // IsNodeUnmanaged returns true if the node is not managed by Azure cloud provider. | ||||
| // Those nodes includes on-prem or VMs from other clouds. They will not be added to load balancer | ||||
| // backends. Azure routes and managed disks are also not supported for them. | ||||
| func (az *Cloud) IsNodeUnmanaged(nodeName string) (bool, error) { | ||||
| 	unmanagedNodes, err := az.GetUnmanagedNodes() | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	return unmanagedNodes.Has(nodeName), nil | ||||
| } | ||||
|  | ||||
| // IsNodeUnmanagedByProviderID returns true if the node is not managed by Azure cloud provider. | ||||
| // All managed node's providerIDs are in format 'azure:///subscriptions/<id>/resourceGroups/<rg>/providers/Microsoft.Compute/.*' | ||||
| func (az *Cloud) IsNodeUnmanagedByProviderID(providerID string) bool { | ||||
| 	return !azureNodeProviderIDRE.Match([]byte(providerID)) | ||||
| } | ||||
|  | ||||
| // convertResourceGroupNameToLower converts the resource group name in the resource ID to be lowered. | ||||
| func convertResourceGroupNameToLower(resourceID string) (string, error) { | ||||
| 	matches := azureResourceGroupNameRE.FindStringSubmatch(resourceID) | ||||
| 	if len(matches) != 2 { | ||||
| 		return "", fmt.Errorf("%q isn't in Azure resource ID format %q", resourceID, azureResourceGroupNameRE.String()) | ||||
| 	} | ||||
|  | ||||
| 	resourceGroup := matches[1] | ||||
| 	return strings.Replace(resourceID, resourceGroup, strings.ToLower(resourceGroup), 1), nil | ||||
| } | ||||
|  | ||||
| // isBackendPoolOnSameLB checks whether newBackendPoolID is on the same load balancer as existingBackendPools. | ||||
| // Since both public and internal LBs are supported, lbName and lbName-internal are treated as same. | ||||
| // If not same, the lbName for existingBackendPools would also be returned. | ||||
| func isBackendPoolOnSameLB(newBackendPoolID string, existingBackendPools []string) (bool, string, error) { | ||||
| 	matches := backendPoolIDRE.FindStringSubmatch(newBackendPoolID) | ||||
| 	if len(matches) != 2 { | ||||
| 		return false, "", fmt.Errorf("new backendPoolID %q is in wrong format", newBackendPoolID) | ||||
| 	} | ||||
|  | ||||
| 	newLBName := matches[1] | ||||
| 	newLBNameTrimmed := strings.TrimSuffix(newLBName, InternalLoadBalancerNameSuffix) | ||||
| 	for _, backendPool := range existingBackendPools { | ||||
| 		matches := backendPoolIDRE.FindStringSubmatch(backendPool) | ||||
| 		if len(matches) != 2 { | ||||
| 			return false, "", fmt.Errorf("existing backendPoolID %q is in wrong format", backendPool) | ||||
| 		} | ||||
|  | ||||
| 		lbName := matches[1] | ||||
| 		if !strings.EqualFold(strings.TrimSuffix(lbName, InternalLoadBalancerNameSuffix), newLBNameTrimmed) { | ||||
| 			return false, lbName, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return true, "", nil | ||||
| } | ||||
| @@ -1,292 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| func TestExtractNotFound(t *testing.T) { | ||||
| 	notFound := &retry.Error{HTTPStatusCode: http.StatusNotFound} | ||||
| 	otherHTTP := &retry.Error{HTTPStatusCode: http.StatusForbidden} | ||||
| 	otherErr := &retry.Error{HTTPStatusCode: http.StatusTooManyRequests} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		err         *retry.Error | ||||
| 		expectedErr *retry.Error | ||||
| 		exists      bool | ||||
| 	}{ | ||||
| 		{nil, nil, true}, | ||||
| 		{otherErr, otherErr, false}, | ||||
| 		{notFound, nil, false}, | ||||
| 		{otherHTTP, otherHTTP, false}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		exists, err := checkResourceExistsFromError(test.err) | ||||
| 		if test.exists != exists { | ||||
| 			t.Errorf("expected: %v, saw: %v", test.exists, exists) | ||||
| 		} | ||||
| 		if !reflect.DeepEqual(test.expectedErr, err) { | ||||
| 			t.Errorf("expected err: %v, saw: %v", test.expectedErr, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestIsNodeUnmanaged(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		unmanagedNodes sets.String | ||||
| 		node           string | ||||
| 		expected       bool | ||||
| 		expectErr      bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "unmanaged node should return true", | ||||
| 			unmanagedNodes: sets.NewString("node1", "node2"), | ||||
| 			node:           "node1", | ||||
| 			expected:       true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "managed node should return false", | ||||
| 			unmanagedNodes: sets.NewString("node1", "node2"), | ||||
| 			node:           "node3", | ||||
| 			expected:       false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "empty unmanagedNodes should return true", | ||||
| 			unmanagedNodes: sets.NewString(), | ||||
| 			node:           "node3", | ||||
| 			expected:       false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "no synced informer should report error", | ||||
| 			unmanagedNodes: sets.NewString(), | ||||
| 			node:           "node1", | ||||
| 			expectErr:      true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	for _, test := range tests { | ||||
| 		az.unmanagedNodes = test.unmanagedNodes | ||||
| 		if test.expectErr { | ||||
| 			az.nodeInformerSynced = func() bool { | ||||
| 				return false | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		real, err := az.IsNodeUnmanaged(test.node) | ||||
| 		if test.expectErr { | ||||
| 			assert.Error(t, err, test.name) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		assert.NoError(t, err, test.name) | ||||
| 		assert.Equal(t, test.expected, real, test.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestIsNodeUnmanagedByProviderID(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		providerID string | ||||
| 		expected   bool | ||||
| 		name       string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			providerID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			providerID: CloudProviderName + "://", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			providerID: ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			providerID: "aws:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			providerID: "k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
| 	for _, test := range tests { | ||||
| 		isUnmanagedNode := az.IsNodeUnmanagedByProviderID(test.providerID) | ||||
| 		assert.Equal(t, test.expected, isUnmanagedNode, test.providerID) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConvertResourceGroupNameToLower(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		desc        string | ||||
| 		resourceID  string | ||||
| 		expected    string | ||||
| 		expectError bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			desc:        "empty string should report error", | ||||
| 			resourceID:  "", | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "resourceID not in Azure format should report error", | ||||
| 			resourceID:  "invalid-id", | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:        "providerID not in Azure format should report error", | ||||
| 			resourceID:  "azure://invalid-id", | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:       "resource group name in VM providerID should be converted", | ||||
| 			resourceID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:       "resource group name in VM resourceID should be converted", | ||||
| 			resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 			expected:   "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:       "resource group name in VMSS providerID should be converted", | ||||
| 			resourceID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156", | ||||
| 			expected:   CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156", | ||||
| 		}, | ||||
| 		{ | ||||
| 			desc:       "resource group name in VMSS resourceID should be converted", | ||||
| 			resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156", | ||||
| 			expected:   "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		real, err := convertResourceGroupNameToLower(test.resourceID) | ||||
| 		if test.expectError { | ||||
| 			assert.NotNil(t, err, test.desc) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		assert.Nil(t, err, test.desc) | ||||
| 		assert.Equal(t, test.expected, real, test.desc) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestIsBackendPoolOnSameLB(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		backendPoolID        string | ||||
| 		existingBackendPools []string | ||||
| 		expected             bool | ||||
| 		expectedLBName       string | ||||
| 		expectError          bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expected:       true, | ||||
| 			expectedLBName: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1-internal/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expected:       true, | ||||
| 			expectedLBName: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1-internal/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expected:       true, | ||||
| 			expectedLBName: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb2/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expected:       false, | ||||
| 			expectedLBName: "lb2", | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "wrong-backendpool-id", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"wrong-existing-backendpool-id", | ||||
| 			}, | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "wrong-backendpool-id", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"wrong-existing-backendpool-id", | ||||
| 			}, | ||||
| 			expectError: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/malformed-lb1-internal/backendAddressPools/pool1", | ||||
| 			existingBackendPools: []string{ | ||||
| 				"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/malformed-lb1-lanretni/backendAddressPools/pool2", | ||||
| 			}, | ||||
| 			expected:       false, | ||||
| 			expectedLBName: "malformed-lb1-lanretni", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		isSameLB, lbName, err := isBackendPoolOnSameLB(test.backendPoolID, test.existingBackendPools) | ||||
| 		if test.expectError { | ||||
| 			assert.Error(t, err) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		assert.Equal(t, test.expected, isSameLB) | ||||
| 		assert.Equal(t, test.expectedLBName, lbName) | ||||
| 	} | ||||
| } | ||||
| @@ -1,131 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2016 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azcache "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| ) | ||||
|  | ||||
| // makeZone returns the zone value in format of <region>-<zone-id>. | ||||
| func (az *Cloud) makeZone(location string, zoneID int) string { | ||||
| 	return fmt.Sprintf("%s-%d", strings.ToLower(location), zoneID) | ||||
| } | ||||
|  | ||||
| // isAvailabilityZone returns true if the zone is in format of <region>-<zone-id>. | ||||
| func (az *Cloud) isAvailabilityZone(zone string) bool { | ||||
| 	return strings.HasPrefix(zone, fmt.Sprintf("%s-", az.Location)) | ||||
| } | ||||
|  | ||||
| // GetZoneID returns the ID of zone from node's zone label. | ||||
| func (az *Cloud) GetZoneID(zoneLabel string) string { | ||||
| 	if !az.isAvailabilityZone(zoneLabel) { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	return strings.TrimPrefix(zoneLabel, fmt.Sprintf("%s-", az.Location)) | ||||
| } | ||||
|  | ||||
| // GetZone returns the Zone containing the current availability zone and locality region that the program is running in. | ||||
| // If the node is not running with availability zones, then it will fall back to fault domain. | ||||
| func (az *Cloud) GetZone(ctx context.Context) (cloudprovider.Zone, error) { | ||||
| 	if az.UseInstanceMetadata { | ||||
| 		metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeUnsafe) | ||||
| 		if err != nil { | ||||
| 			return cloudprovider.Zone{}, err | ||||
| 		} | ||||
|  | ||||
| 		if metadata.Compute == nil { | ||||
| 			az.metadata.imsCache.Delete(metadataCacheKey) | ||||
| 			return cloudprovider.Zone{}, fmt.Errorf("failure of getting compute information from instance metadata") | ||||
| 		} | ||||
|  | ||||
| 		zone := "" | ||||
| 		location := metadata.Compute.Location | ||||
| 		if metadata.Compute.Zone != "" { | ||||
| 			zoneID, err := strconv.Atoi(metadata.Compute.Zone) | ||||
| 			if err != nil { | ||||
| 				return cloudprovider.Zone{}, fmt.Errorf("failed to parse zone ID %q: %v", metadata.Compute.Zone, err) | ||||
| 			} | ||||
| 			zone = az.makeZone(location, zoneID) | ||||
| 		} else { | ||||
| 			klog.V(3).Infof("Availability zone is not enabled for the node, falling back to fault domain") | ||||
| 			zone = metadata.Compute.FaultDomain | ||||
| 		} | ||||
|  | ||||
| 		return cloudprovider.Zone{ | ||||
| 			FailureDomain: strings.ToLower(zone), | ||||
| 			Region:        strings.ToLower(location), | ||||
| 		}, nil | ||||
| 	} | ||||
| 	// if UseInstanceMetadata is false, get Zone name by calling ARM | ||||
| 	hostname, err := os.Hostname() | ||||
| 	if err != nil { | ||||
| 		return cloudprovider.Zone{}, fmt.Errorf("failure getting hostname from kernel") | ||||
| 	} | ||||
| 	return az.VMSet.GetZoneByNodeName(strings.ToLower(hostname)) | ||||
| } | ||||
|  | ||||
| // GetZoneByProviderID implements Zones.GetZoneByProviderID | ||||
| // This is particularly useful in external cloud providers where the kubelet | ||||
| // does not initialize node data. | ||||
| func (az *Cloud) GetZoneByProviderID(ctx context.Context, providerID string) (cloudprovider.Zone, error) { | ||||
| 	if providerID == "" { | ||||
| 		return cloudprovider.Zone{}, errNodeNotInitialized | ||||
| 	} | ||||
|  | ||||
| 	// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	if az.IsNodeUnmanagedByProviderID(providerID) { | ||||
| 		klog.V(2).Infof("GetZoneByProviderID: omitting unmanaged node %q", providerID) | ||||
| 		return cloudprovider.Zone{}, nil | ||||
| 	} | ||||
|  | ||||
| 	nodeName, err := az.VMSet.GetNodeNameByProviderID(providerID) | ||||
| 	if err != nil { | ||||
| 		return cloudprovider.Zone{}, err | ||||
| 	} | ||||
|  | ||||
| 	return az.GetZoneByNodeName(ctx, nodeName) | ||||
| } | ||||
|  | ||||
| // GetZoneByNodeName implements Zones.GetZoneByNodeName | ||||
| // This is particularly useful in external cloud providers where the kubelet | ||||
| // does not initialize node data. | ||||
| func (az *Cloud) GetZoneByNodeName(ctx context.Context, nodeName types.NodeName) (cloudprovider.Zone, error) { | ||||
| 	// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them. | ||||
| 	unmanaged, err := az.IsNodeUnmanaged(string(nodeName)) | ||||
| 	if err != nil { | ||||
| 		return cloudprovider.Zone{}, err | ||||
| 	} | ||||
| 	if unmanaged { | ||||
| 		klog.V(2).Infof("GetZoneByNodeName: omitting unmanaged node %q", nodeName) | ||||
| 		return cloudprovider.Zone{}, nil | ||||
| 	} | ||||
|  | ||||
| 	return az.VMSet.GetZoneByNodeName(string(nodeName)) | ||||
| } | ||||
| @@ -1,245 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2018 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package azure | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	cloudprovider "k8s.io/cloud-provider" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient" | ||||
| 	"k8s.io/utils/pointer" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	testAvailabilitySetNodeProviderID = "azure:///subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm-0" | ||||
| ) | ||||
|  | ||||
| func TestIsAvailabilityZone(t *testing.T) { | ||||
| 	location := "eastus" | ||||
| 	az := &Cloud{ | ||||
| 		Config: Config{ | ||||
| 			Location: location, | ||||
| 		}, | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		desc     string | ||||
| 		zone     string | ||||
| 		expected bool | ||||
| 	}{ | ||||
| 		{"empty string should return false", "", false}, | ||||
| 		{"wrong format should return false", "123", false}, | ||||
| 		{"wrong location should return false", "chinanorth-1", false}, | ||||
| 		{"correct zone should return true", "eastus-1", true}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		actual := az.isAvailabilityZone(test.zone) | ||||
| 		if actual != test.expected { | ||||
| 			t.Errorf("test [%q] get unexpected result: %v != %v", test.desc, actual, test.expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetZoneID(t *testing.T) { | ||||
| 	location := "eastus" | ||||
| 	az := &Cloud{ | ||||
| 		Config: Config{ | ||||
| 			Location: location, | ||||
| 		}, | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		desc     string | ||||
| 		zone     string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{"empty string should return empty string", "", ""}, | ||||
| 		{"wrong format should return empty string", "123", ""}, | ||||
| 		{"wrong location should return empty string", "chinanorth-1", ""}, | ||||
| 		{"correct zone should return zone ID", "eastus-1", "1"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		actual := az.GetZoneID(test.zone) | ||||
| 		if actual != test.expected { | ||||
| 			t.Errorf("test [%q] get unexpected result: %q != %q", test.desc, actual, test.expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetZone(t *testing.T) { | ||||
| 	cloud := &Cloud{ | ||||
| 		Config: Config{ | ||||
| 			Location:            "eastus", | ||||
| 			UseInstanceMetadata: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	testcases := []struct { | ||||
| 		name        string | ||||
| 		zone        string | ||||
| 		location    string | ||||
| 		faultDomain string | ||||
| 		expected    string | ||||
| 		isNilResp   bool | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "GetZone should get real zone if only node's zone is set", | ||||
| 			zone:     "1", | ||||
| 			location: "eastus", | ||||
| 			expected: "eastus-1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "GetZone should get real zone if both node's zone and FD are set", | ||||
| 			zone:        "1", | ||||
| 			location:    "eastus", | ||||
| 			faultDomain: "99", | ||||
| 			expected:    "eastus-1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "GetZone should get faultDomain if node's zone isn't set", | ||||
| 			location:    "eastus", | ||||
| 			faultDomain: "99", | ||||
| 			expected:    "99", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "GetZone should get availability zone in lower cases", | ||||
| 			location: "EastUS", | ||||
| 			zone:     "1", | ||||
| 			expected: "eastus-1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "GetZone should report an error if there is no `Compute` in the response", | ||||
| 			isNilResp:   true, | ||||
| 			expectedErr: fmt.Errorf("failure of getting compute information from instance metadata"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "GetZone should report an error if the zone is invalid", | ||||
| 			zone:        "a", | ||||
| 			location:    "eastus", | ||||
| 			faultDomain: "99", | ||||
| 			expected:    "", | ||||
| 			expectedErr: fmt.Errorf("failed to parse zone ID \"a\": strconv.Atoi: parsing \"a\": invalid syntax"), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		listener, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		respString := fmt.Sprintf(`{"compute":{"zone":"%s", "platformFaultDomain":"%s", "location":"%s"}}`, test.zone, test.faultDomain, test.location) | ||||
| 		if test.isNilResp { | ||||
| 			respString = "{}" | ||||
| 		} | ||||
| 		mux := http.NewServeMux() | ||||
| 		mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 			fmt.Fprint(w, respString) | ||||
| 		})) | ||||
| 		go func() { | ||||
| 			http.Serve(listener, mux) | ||||
| 		}() | ||||
| 		defer listener.Close() | ||||
|  | ||||
| 		cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/") | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 		} | ||||
|  | ||||
| 		zone, err := cloud.GetZone(context.Background()) | ||||
| 		if err != nil { | ||||
| 			if test.expectedErr == nil { | ||||
| 				t.Errorf("Test [%s] unexpected error: %v", test.name, err) | ||||
| 			} else { | ||||
| 				assert.Equal(t, test.expectedErr, err) | ||||
| 			} | ||||
| 		} | ||||
| 		if zone.FailureDomain != test.expected { | ||||
| 			t.Errorf("Test [%s] unexpected zone: %s, expected %q", test.name, zone.FailureDomain, test.expected) | ||||
| 		} | ||||
| 		if err == nil && zone.Region != cloud.Location { | ||||
| 			t.Errorf("Test [%s] unexpected region: %s, expected: %s", test.name, zone.Region, cloud.Location) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMakeZone(t *testing.T) { | ||||
| 	az := &Cloud{} | ||||
| 	zone := az.makeZone("EASTUS", 2) | ||||
| 	assert.Equal(t, "eastus-2", zone) | ||||
| } | ||||
|  | ||||
| func TestGetZoneByProviderID(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	az := GetTestCloud(ctrl) | ||||
|  | ||||
| 	zone, err := az.GetZoneByProviderID(context.Background(), "") | ||||
| 	assert.Equal(t, errNodeNotInitialized, err) | ||||
| 	assert.Equal(t, cloudprovider.Zone{}, zone) | ||||
|  | ||||
| 	zone, err = az.GetZoneByProviderID(context.Background(), "invalid/id") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, cloudprovider.Zone{}, zone) | ||||
|  | ||||
| 	mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface) | ||||
| 	mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm-0", gomock.Any()).Return(compute.VirtualMachine{ | ||||
| 		Zones:    &[]string{"1"}, | ||||
| 		Location: pointer.String("eastus"), | ||||
| 	}, nil) | ||||
| 	zone, err = az.GetZoneByProviderID(context.Background(), testAvailabilitySetNodeProviderID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, cloudprovider.Zone{ | ||||
| 		FailureDomain: "eastus-1", | ||||
| 		Region:        "eastus", | ||||
| 	}, zone) | ||||
| } | ||||
|  | ||||
| func TestAvailabilitySetGetZoneByNodeName(t *testing.T) { | ||||
| 	az := &Cloud{ | ||||
| 		unmanagedNodes: sets.String{"vm-0": sets.Empty{}}, | ||||
| 		nodeInformerSynced: func() bool { | ||||
| 			return true | ||||
| 		}, | ||||
| 	} | ||||
| 	zone, err := az.GetZoneByNodeName(context.Background(), "vm-0") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, cloudprovider.Zone{}, zone) | ||||
|  | ||||
| 	az = &Cloud{ | ||||
| 		unmanagedNodes: sets.String{"vm-0": sets.Empty{}}, | ||||
| 		nodeInformerSynced: func() bool { | ||||
| 			return false | ||||
| 		}, | ||||
| 	} | ||||
| 	zone, err = az.GetZoneByNodeName(context.Background(), "vm-0") | ||||
| 	assert.Equal(t, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"), err) | ||||
| 	assert.Equal(t, cloudprovider.Zone{}, zone) | ||||
| } | ||||
| @@ -1,178 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cache | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"k8s.io/client-go/tools/cache" | ||||
| ) | ||||
|  | ||||
| // AzureCacheReadType defines the read type for cache data | ||||
| type AzureCacheReadType int | ||||
|  | ||||
| const ( | ||||
| 	// CacheReadTypeDefault returns data from cache if cache entry not expired | ||||
| 	// if cache entry expired, then it will refetch the data using getter | ||||
| 	// save the entry in cache and then return | ||||
| 	CacheReadTypeDefault AzureCacheReadType = iota | ||||
| 	// CacheReadTypeUnsafe returns data from cache even if the cache entry is | ||||
| 	// active/expired. If entry doesn't exist in cache, then data is fetched | ||||
| 	// using getter, saved in cache and returned | ||||
| 	CacheReadTypeUnsafe | ||||
| 	// CacheReadTypeForceRefresh force refreshes the cache even if the cache entry | ||||
| 	// is not expired | ||||
| 	CacheReadTypeForceRefresh | ||||
| ) | ||||
|  | ||||
| // GetFunc defines a getter function for timedCache. | ||||
| type GetFunc func(key string) (interface{}, error) | ||||
|  | ||||
| // AzureCacheEntry is the internal structure stores inside TTLStore. | ||||
| type AzureCacheEntry struct { | ||||
| 	Key  string | ||||
| 	Data interface{} | ||||
|  | ||||
| 	// The lock to ensure not updating same entry simultaneously. | ||||
| 	Lock sync.Mutex | ||||
| 	// time when entry was fetched and created | ||||
| 	CreatedOn time.Time | ||||
| } | ||||
|  | ||||
| // cacheKeyFunc defines the key function required in TTLStore. | ||||
| func cacheKeyFunc(obj interface{}) (string, error) { | ||||
| 	return obj.(*AzureCacheEntry).Key, nil | ||||
| } | ||||
|  | ||||
| // TimedCache is a cache with TTL. | ||||
| type TimedCache struct { | ||||
| 	Store  cache.Store | ||||
| 	Lock   sync.Mutex | ||||
| 	Getter GetFunc | ||||
| 	TTL    time.Duration | ||||
| } | ||||
|  | ||||
| // NewTimedcache creates a new TimedCache. | ||||
| func NewTimedcache(ttl time.Duration, getter GetFunc) (*TimedCache, error) { | ||||
| 	if getter == nil { | ||||
| 		return nil, fmt.Errorf("getter is not provided") | ||||
| 	} | ||||
|  | ||||
| 	return &TimedCache{ | ||||
| 		Getter: getter, | ||||
| 		// switch to using NewStore instead of NewTTLStore so that we can | ||||
| 		// reuse entries for calls that are fine with reading expired/stalled data. | ||||
| 		// with NewTTLStore, entries are not returned if they have already expired. | ||||
| 		Store: cache.NewStore(cacheKeyFunc), | ||||
| 		TTL:   ttl, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // getInternal returns AzureCacheEntry by key. If the key is not cached yet, | ||||
| // it returns a AzureCacheEntry with nil data. | ||||
| func (t *TimedCache) getInternal(key string) (*AzureCacheEntry, error) { | ||||
| 	entry, exists, err := t.Store.GetByKey(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// if entry exists, return the entry | ||||
| 	if exists { | ||||
| 		return entry.(*AzureCacheEntry), nil | ||||
| 	} | ||||
|  | ||||
| 	// lock here to ensure if entry doesn't exist, we add a new entry | ||||
| 	// avoiding overwrites | ||||
| 	t.Lock.Lock() | ||||
| 	defer t.Lock.Unlock() | ||||
|  | ||||
| 	// Another goroutine might have written the same key. | ||||
| 	entry, exists, err = t.Store.GetByKey(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if exists { | ||||
| 		return entry.(*AzureCacheEntry), nil | ||||
| 	} | ||||
|  | ||||
| 	// Still not found, add new entry with nil data. | ||||
| 	// Note the data will be filled later by getter. | ||||
| 	newEntry := &AzureCacheEntry{ | ||||
| 		Key:  key, | ||||
| 		Data: nil, | ||||
| 	} | ||||
| 	t.Store.Add(newEntry) | ||||
| 	return newEntry, nil | ||||
| } | ||||
|  | ||||
| // Get returns the requested item by key. | ||||
| func (t *TimedCache) Get(key string, crt AzureCacheReadType) (interface{}, error) { | ||||
| 	entry, err := t.getInternal(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	entry.Lock.Lock() | ||||
| 	defer entry.Lock.Unlock() | ||||
|  | ||||
| 	// entry exists and if cache is not force refreshed | ||||
| 	if entry.Data != nil && crt != CacheReadTypeForceRefresh { | ||||
| 		// allow unsafe read, so return data even if expired | ||||
| 		if crt == CacheReadTypeUnsafe { | ||||
| 			return entry.Data, nil | ||||
| 		} | ||||
| 		// if cached data is not expired, return cached data | ||||
| 		if crt == CacheReadTypeDefault && time.Since(entry.CreatedOn) < t.TTL { | ||||
| 			return entry.Data, nil | ||||
| 		} | ||||
| 	} | ||||
| 	// Data is not cached yet, cache data is expired or requested force refresh | ||||
| 	// cache it by getter. entry is locked before getting to ensure concurrent | ||||
| 	// gets don't result in multiple ARM calls. | ||||
| 	data, err := t.Getter(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// set the data in cache and also set the last update time | ||||
| 	// to now as the data was recently fetched | ||||
| 	entry.Data = data | ||||
| 	entry.CreatedOn = time.Now().UTC() | ||||
|  | ||||
| 	return entry.Data, nil | ||||
| } | ||||
|  | ||||
| // Delete removes an item from the cache. | ||||
| func (t *TimedCache) Delete(key string) error { | ||||
| 	return t.Store.Delete(&AzureCacheEntry{ | ||||
| 		Key: key, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // Set sets the data cache for the key. | ||||
| // It is only used for testing. | ||||
| func (t *TimedCache) Set(key string, data interface{}) { | ||||
| 	t.Store.Add(&AzureCacheEntry{ | ||||
| 		Key:       key, | ||||
| 		Data:      data, | ||||
| 		CreatedOn: time.Now().UTC(), | ||||
| 	}) | ||||
| } | ||||
| @@ -1,229 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2017 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package cache | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	fakeCacheTTL = 2 * time.Second | ||||
| ) | ||||
|  | ||||
| type fakeDataObj struct{} | ||||
|  | ||||
| type fakeDataSource struct { | ||||
| 	called int | ||||
| 	data   map[string]*fakeDataObj | ||||
| 	lock   sync.Mutex | ||||
| } | ||||
|  | ||||
| func (fake *fakeDataSource) get(key string) (interface{}, error) { | ||||
| 	fake.lock.Lock() | ||||
| 	defer fake.lock.Unlock() | ||||
|  | ||||
| 	fake.called = fake.called + 1 | ||||
| 	if v, ok := fake.data[key]; ok { | ||||
| 		return v, nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (fake *fakeDataSource) set(data map[string]*fakeDataObj) { | ||||
| 	fake.lock.Lock() | ||||
| 	defer fake.lock.Unlock() | ||||
|  | ||||
| 	fake.data = data | ||||
| 	fake.called = 0 | ||||
| } | ||||
|  | ||||
| func newFakeCache(t *testing.T) (*fakeDataSource, *TimedCache) { | ||||
| 	dataSource := &fakeDataSource{ | ||||
| 		data: make(map[string]*fakeDataObj), | ||||
| 	} | ||||
| 	getter := dataSource.get | ||||
| 	cache, err := NewTimedcache(fakeCacheTTL, getter) | ||||
| 	assert.NoError(t, err) | ||||
| 	return dataSource, cache | ||||
| } | ||||
|  | ||||
| func TestCacheGet(t *testing.T) { | ||||
| 	val := &fakeDataObj{} | ||||
| 	cases := []struct { | ||||
| 		name     string | ||||
| 		data     map[string]*fakeDataObj | ||||
| 		key      string | ||||
| 		expected interface{} | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "cache should return nil for empty data source", | ||||
| 			key:      "key1", | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "cache should return nil for non exist key", | ||||
| 			data:     map[string]*fakeDataObj{"key2": val}, | ||||
| 			key:      "key1", | ||||
| 			expected: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "cache should return data for existing key", | ||||
| 			data:     map[string]*fakeDataObj{"key1": val}, | ||||
| 			key:      "key1", | ||||
| 			expected: val, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, c := range cases { | ||||
| 		dataSource, cache := newFakeCache(t) | ||||
| 		dataSource.set(c.data) | ||||
| 		val, err := cache.Get(c.key, CacheReadTypeDefault) | ||||
| 		assert.NoError(t, err, c.name) | ||||
| 		assert.Equal(t, c.expected, val, c.name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCacheGetError(t *testing.T) { | ||||
| 	getError := fmt.Errorf("getError") | ||||
| 	getter := func(key string) (interface{}, error) { | ||||
| 		return nil, getError | ||||
| 	} | ||||
| 	cache, err := NewTimedcache(fakeCacheTTL, getter) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	val, err := cache.Get("key", CacheReadTypeDefault) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Equal(t, getError, err) | ||||
| 	assert.Nil(t, val) | ||||
| } | ||||
|  | ||||
| func TestCacheDelete(t *testing.T) { | ||||
| 	key := "key1" | ||||
| 	val := &fakeDataObj{} | ||||
| 	data := map[string]*fakeDataObj{ | ||||
| 		key: val, | ||||
| 	} | ||||
| 	dataSource, cache := newFakeCache(t) | ||||
| 	dataSource.set(data) | ||||
|  | ||||
| 	v, err := cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data") | ||||
|  | ||||
| 	dataSource.set(nil) | ||||
| 	cache.Delete(key) | ||||
| 	v, err = cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, nil, v, "cache should get nil after data is removed") | ||||
| } | ||||
|  | ||||
| func TestCacheExpired(t *testing.T) { | ||||
| 	key := "key1" | ||||
| 	val := &fakeDataObj{} | ||||
| 	data := map[string]*fakeDataObj{ | ||||
| 		key: val, | ||||
| 	} | ||||
| 	dataSource, cache := newFakeCache(t) | ||||
| 	dataSource.set(data) | ||||
|  | ||||
| 	v, err := cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data") | ||||
|  | ||||
| 	time.Sleep(fakeCacheTTL) | ||||
| 	v, err = cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 2, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data even after expired") | ||||
| } | ||||
|  | ||||
| func TestCacheAllowUnsafeRead(t *testing.T) { | ||||
| 	key := "key1" | ||||
| 	val := &fakeDataObj{} | ||||
| 	data := map[string]*fakeDataObj{ | ||||
| 		key: val, | ||||
| 	} | ||||
| 	dataSource, cache := newFakeCache(t) | ||||
| 	dataSource.set(data) | ||||
|  | ||||
| 	v, err := cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data") | ||||
|  | ||||
| 	time.Sleep(fakeCacheTTL) | ||||
| 	v, err = cache.Get(key, CacheReadTypeUnsafe) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should return expired as allow unsafe read is allowed") | ||||
| } | ||||
|  | ||||
| func TestCacheNoConcurrentGet(t *testing.T) { | ||||
| 	key := "key1" | ||||
| 	val := &fakeDataObj{} | ||||
| 	data := map[string]*fakeDataObj{ | ||||
| 		key: val, | ||||
| 	} | ||||
| 	dataSource, cache := newFakeCache(t) | ||||
| 	dataSource.set(data) | ||||
|  | ||||
| 	time.Sleep(fakeCacheTTL) | ||||
| 	var wg sync.WaitGroup | ||||
| 	for i := 0; i < 5; i++ { | ||||
| 		wg.Add(1) | ||||
| 		go func() { | ||||
| 			defer wg.Done() | ||||
| 			_, _ = cache.Get(key, CacheReadTypeDefault) | ||||
| 		}() | ||||
| 	} | ||||
| 	v, err := cache.Get(key, CacheReadTypeDefault) | ||||
| 	wg.Wait() | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data") | ||||
| } | ||||
|  | ||||
| func TestCacheForceRefresh(t *testing.T) { | ||||
| 	key := "key1" | ||||
| 	val := &fakeDataObj{} | ||||
| 	data := map[string]*fakeDataObj{ | ||||
| 		key: val, | ||||
| 	} | ||||
| 	dataSource, cache := newFakeCache(t) | ||||
| 	dataSource.set(data) | ||||
|  | ||||
| 	v, err := cache.Get(key, CacheReadTypeDefault) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 1, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "cache should get correct data") | ||||
|  | ||||
| 	v, err = cache.Get(key, CacheReadTypeForceRefresh) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 2, dataSource.called) | ||||
| 	assert.Equal(t, val, v, "should refetch unexpired data as forced refresh") | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package cache is an implementation of Azure caches. | ||||
| package cache // import "k8s.io/legacy-cloud-providers/azure/cache" | ||||
| @@ -1,701 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package armclient | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
|  | ||||
| 	"k8s.io/client-go/pkg/version" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| var _ Interface = &Client{} | ||||
|  | ||||
| // Client implements ARM client Interface. | ||||
| type Client struct { | ||||
| 	client  autorest.Client | ||||
| 	backoff *retry.Backoff | ||||
|  | ||||
| 	baseURI      string | ||||
| 	apiVersion   string | ||||
| 	clientRegion string | ||||
| } | ||||
|  | ||||
| // New creates a ARM client | ||||
| func New(authorizer autorest.Authorizer, baseURI, userAgent, apiVersion, clientRegion string, clientBackoff *retry.Backoff) *Client { | ||||
| 	restClient := autorest.NewClientWithUserAgent(userAgent) | ||||
| 	restClient.PollingDelay = 5 * time.Second | ||||
| 	restClient.RetryAttempts = 3 | ||||
| 	restClient.RetryDuration = time.Second * 1 | ||||
| 	restClient.Authorizer = authorizer | ||||
|  | ||||
| 	if userAgent == "" { | ||||
| 		restClient.UserAgent = GetUserAgent(restClient) | ||||
| 	} | ||||
|  | ||||
| 	backoff := clientBackoff | ||||
| 	if backoff == nil { | ||||
| 		backoff = &retry.Backoff{} | ||||
| 	} | ||||
| 	if backoff.Steps == 0 { | ||||
| 		// 1 steps means no retry. | ||||
| 		backoff.Steps = 1 | ||||
| 	} | ||||
|  | ||||
| 	return &Client{ | ||||
| 		client:       restClient, | ||||
| 		baseURI:      baseURI, | ||||
| 		backoff:      backoff, | ||||
| 		apiVersion:   apiVersion, | ||||
| 		clientRegion: NormalizeAzureRegion(clientRegion), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetUserAgent gets the autorest client with a user agent that | ||||
| // includes "kubernetes" and the full kubernetes git version string | ||||
| // example: | ||||
| // Azure-SDK-for-Go/7.0.1 arm-network/2016-09-01; kubernetes-cloudprovider/v1.17.0; | ||||
| func GetUserAgent(client autorest.Client) string { | ||||
| 	k8sVersion := version.Get().GitVersion | ||||
| 	return fmt.Sprintf("%s; kubernetes-cloudprovider/%s", client.UserAgent, k8sVersion) | ||||
| } | ||||
|  | ||||
| // NormalizeAzureRegion returns a normalized Azure region with white spaces removed and converted to lower case | ||||
| func NormalizeAzureRegion(name string) string { | ||||
| 	region := "" | ||||
| 	for _, runeValue := range name { | ||||
| 		if !unicode.IsSpace(runeValue) { | ||||
| 			region += string(runeValue) | ||||
| 		} | ||||
| 	} | ||||
| 	return strings.ToLower(region) | ||||
| } | ||||
|  | ||||
| // sendRequest sends a http request to ARM service. | ||||
| // Although Azure SDK supports retries per https://github.com/azure/azure-sdk-for-go#request-retry-policy, we | ||||
| // disable it since we want to fully control the retry policies. | ||||
| func (c *Client) sendRequest(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) { | ||||
| 	sendBackoff := *c.backoff | ||||
| 	response, err := autorest.SendWithSender( | ||||
| 		c.client, | ||||
| 		request, | ||||
| 		retry.DoExponentialBackoffRetry(&sendBackoff), | ||||
| 	) | ||||
|  | ||||
| 	if response == nil && err == nil { | ||||
| 		return response, retry.NewError(false, fmt.Errorf("Empty response and no HTTP code")) | ||||
| 	} | ||||
|  | ||||
| 	return response, retry.GetError(response, err) | ||||
| } | ||||
|  | ||||
| // Send sends a http request to ARM service with possible retry to regional ARM endpoint. | ||||
| func (c *Client) Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) { | ||||
| 	response, rerr := c.sendRequest(ctx, request) | ||||
| 	if rerr != nil { | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusNotFound || c.clientRegion == "" { | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	bodyBytes, _ := ioutil.ReadAll(response.Body) | ||||
| 	defer func() { | ||||
| 		response.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) | ||||
| 	}() | ||||
|  | ||||
| 	bodyString := string(bodyBytes) | ||||
| 	klog.V(5).Infof("Send.sendRequest original error message: %s", bodyString) | ||||
|  | ||||
| 	// Hack: retry the regional ARM endpoint in case of ARM traffic split and arm resource group replication is too slow | ||||
| 	var body map[string]interface{} | ||||
| 	if e := json.Unmarshal(bodyBytes, &body); e != nil { | ||||
| 		klog.V(5).Infof("Send.sendRequest: error in parsing response body string: %s, Skip retrying regional host", e) | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	if err, ok := body["error"].(map[string]interface{}); !ok || | ||||
| 		err["code"] == nil || | ||||
| 		!strings.EqualFold(err["code"].(string), "ResourceGroupNotFound") { | ||||
| 		klog.V(5).Infof("Send.sendRequest: response body does not contain ResourceGroupNotFound error code. Skip retrying regional host") | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	currentHost := request.URL.Host | ||||
| 	if request.Host != "" { | ||||
| 		currentHost = request.Host | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasPrefix(strings.ToLower(currentHost), c.clientRegion) { | ||||
| 		klog.V(5).Infof("Send.sendRequest: current host %s is regional host. Skip retrying regional host.", currentHost) | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	request.Host = fmt.Sprintf("%s.%s", c.clientRegion, strings.ToLower(currentHost)) | ||||
| 	klog.V(5).Infof("Send.sendRegionalRequest on ResourceGroupNotFound error. Retrying regional host: %s", request.Host) | ||||
| 	regionalResponse, regionalError := c.sendRequest(ctx, request) | ||||
|  | ||||
| 	// only use the result if the regional request actually goes through and returns 2xx status code, for two reasons: | ||||
| 	// 1. the retry on regional ARM host approach is a hack. | ||||
| 	// 2. the concatenated regional uri could be wrong as the rule is not officially declared by ARM. | ||||
| 	if regionalResponse == nil || regionalResponse.StatusCode > 299 { | ||||
| 		regionalErrStr := "" | ||||
| 		if regionalError != nil { | ||||
| 			regionalErrStr = regionalError.Error().Error() | ||||
| 		} | ||||
|  | ||||
| 		klog.V(5).Infof("Send.sendRegionalRequest failed to get response from regional host, error: '%s'. Ignoring the result.", regionalErrStr) | ||||
| 		return response, rerr | ||||
| 	} | ||||
|  | ||||
| 	return regionalResponse, regionalError | ||||
| } | ||||
|  | ||||
| // PreparePutRequest prepares put request | ||||
| func (c *Client) PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsContentType("application/json; charset=utf-8"), | ||||
| 			autorest.AsPut(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // PreparePatchRequest prepares patch request | ||||
| func (c *Client) PreparePatchRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsContentType("application/json; charset=utf-8"), | ||||
| 			autorest.AsPatch(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // PreparePostRequest prepares post request | ||||
| func (c *Client) PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsContentType("application/json; charset=utf-8"), | ||||
| 			autorest.AsPost(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // PrepareGetRequest prepares get request | ||||
| func (c *Client) PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsGet(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // PrepareDeleteRequest preparse delete request | ||||
| func (c *Client) PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsDelete(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // PrepareHeadRequest prepares head request | ||||
| func (c *Client) PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		[]autorest.PrepareDecorator{ | ||||
| 			autorest.AsHead(), | ||||
| 			autorest.WithBaseURL(c.baseURI)}, | ||||
| 		decorators...) | ||||
| 	return c.prepareRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationCompletion waits for an operation completion | ||||
| func (c *Client) WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error { | ||||
| 	err := future.WaitForCompletionRef(ctx, c.client) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in WaitForCompletionRef: '%v'", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var done bool | ||||
| 	done, err = future.DoneWithContext(ctx, c.client) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in DoneWithContext: '%v'", err) | ||||
| 		return autorest.NewErrorWithError(err, asyncOperationName, "Result", future.Response(), "Polling failure") | ||||
| 	} | ||||
| 	if !done { | ||||
| 		return azure.NewAsyncOpIncompleteError(asyncOperationName) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationResult waits for an operation result. | ||||
| func (c *Client) WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error) { | ||||
| 	err := c.WaitForAsyncOperationCompletion(ctx, future, asyncOperationName) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in WaitForAsyncOperationCompletion: '%v'", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sendBackoff := *c.backoff | ||||
| 	sender := autorest.DecorateSender( | ||||
| 		c.client, | ||||
| 		retry.DoExponentialBackoffRetry(&sendBackoff), | ||||
| 	) | ||||
| 	return future.GetResult(sender) | ||||
| } | ||||
|  | ||||
| // SendAsync send a request and return a future object representing the async result as well as the origin http response | ||||
| func (c *Client) SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error) { | ||||
| 	asyncResponse, rerr := c.Send(ctx, request) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.send", request.URL.String(), rerr.Error()) | ||||
| 		return nil, nil, rerr | ||||
| 	} | ||||
|  | ||||
| 	future, err := azure.NewFutureFromResponse(asyncResponse) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.respond", request.URL.String(), err) | ||||
| 		return nil, asyncResponse, retry.GetError(asyncResponse, err) | ||||
| 	} | ||||
|  | ||||
| 	return &future, asyncResponse, nil | ||||
| } | ||||
|  | ||||
| // GetResource get a resource by resource ID | ||||
| func (c *Client) GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error) { | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 	} | ||||
| 	if expand != "" { | ||||
| 		queryParameters := map[string]interface{}{ | ||||
| 			"$expand": autorest.Encode("query", expand), | ||||
| 		} | ||||
| 		decorators = append(decorators, autorest.WithQueryParameters(queryParameters)) | ||||
| 	} | ||||
| 	request, err := c.PrepareGetRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "get.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	return c.Send(ctx, request) | ||||
| } | ||||
|  | ||||
| // GetResourceWithDecorators get a resource with decorators by resource ID | ||||
| func (c *Client) GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) { | ||||
| 	getDecorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 	} | ||||
| 	getDecorators = append(getDecorators, decorators...) | ||||
| 	request, err := c.PrepareGetRequest(ctx, getDecorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "get.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	return c.Send(ctx, request) | ||||
| } | ||||
|  | ||||
| // PutResource puts a resource by resource ID | ||||
| func (c *Client) PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	putDecorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
| 	return c.PutResourceWithDecorators(ctx, resourceID, parameters, putDecorators) | ||||
| } | ||||
|  | ||||
| // PutResources puts a list of resources from resources map[resourceID]parameters. | ||||
| // Those resources sync requests are sequential while async requests are concurrent. It's especially | ||||
| // useful when the ARM API doesn't support concurrent requests. | ||||
| func (c *Client) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*PutResourcesResponse { | ||||
| 	if len(resources) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Sequential sync requests. | ||||
| 	futures := make(map[string]*azure.Future) | ||||
| 	responses := make(map[string]*PutResourcesResponse) | ||||
| 	for resourceID, parameters := range resources { | ||||
| 		decorators := []autorest.PrepareDecorator{ | ||||
| 			autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 			autorest.WithJSON(parameters), | ||||
| 		} | ||||
| 		request, err := c.PreparePutRequest(ctx, decorators...) | ||||
| 		if err != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err) | ||||
| 			responses[resourceID] = &PutResourcesResponse{ | ||||
| 				Error: retry.NewError(false, err), | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		future, resp, clientErr := c.SendAsync(ctx, request) | ||||
| 		defer c.CloseResponse(ctx, resp) | ||||
| 		if clientErr != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, clientErr.Error()) | ||||
| 			responses[resourceID] = &PutResourcesResponse{ | ||||
| 				Error: clientErr, | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		futures[resourceID] = future | ||||
| 	} | ||||
|  | ||||
| 	// Concurrent async requests. | ||||
| 	wg := sync.WaitGroup{} | ||||
| 	var responseLock sync.Mutex | ||||
| 	for resourceID, future := range futures { | ||||
| 		wg.Add(1) | ||||
| 		go func(resourceID string, future *azure.Future) { | ||||
| 			defer wg.Done() | ||||
| 			response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PutResource") | ||||
| 			if err != nil { | ||||
| 				if response != nil { | ||||
| 					klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode) | ||||
| 				} else { | ||||
| 					klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error()) | ||||
| 				} | ||||
|  | ||||
| 				retriableErr := retry.GetError(response, err) | ||||
| 				if !retriableErr.Retriable && | ||||
| 					strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) { | ||||
| 					klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error()) | ||||
| 					retriableErr.Retriable = true | ||||
| 				} | ||||
|  | ||||
| 				responseLock.Lock() | ||||
| 				responses[resourceID] = &PutResourcesResponse{ | ||||
| 					Error: retriableErr, | ||||
| 				} | ||||
| 				responseLock.Unlock() | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			responseLock.Lock() | ||||
| 			responses[resourceID] = &PutResourcesResponse{ | ||||
| 				Response: response, | ||||
| 			} | ||||
| 			responseLock.Unlock() | ||||
| 		}(resourceID, future) | ||||
| 	} | ||||
|  | ||||
| 	wg.Wait() | ||||
| 	return responses | ||||
| } | ||||
|  | ||||
| // PutResourceWithDecorators puts a resource by resource ID | ||||
| func (c *Client) PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) { | ||||
| 	request, err := c.PreparePutRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	future, resp, clientErr := c.SendAsync(ctx, request) | ||||
| 	defer c.CloseResponse(ctx, resp) | ||||
| 	if clientErr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, clientErr.Error()) | ||||
| 		return nil, clientErr | ||||
| 	} | ||||
|  | ||||
| 	response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PutResource") | ||||
| 	if err != nil { | ||||
| 		if response != nil { | ||||
| 			klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode) | ||||
| 		} else { | ||||
| 			klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		retriableErr := retry.GetError(response, err) | ||||
| 		if !retriableErr.Retriable && | ||||
| 			strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) { | ||||
| 			klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error()) | ||||
| 			retriableErr.Retriable = true | ||||
| 		} | ||||
| 		return nil, retriableErr | ||||
| 	} | ||||
|  | ||||
| 	return response, nil | ||||
| } | ||||
|  | ||||
| // PatchResource patches a resource by resource ID | ||||
| func (c *Client) PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
|  | ||||
| 	request, err := c.PreparePatchRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "patch.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	future, resp, clientErr := c.SendAsync(ctx, request) | ||||
| 	defer c.CloseResponse(ctx, resp) | ||||
| 	if clientErr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "patch.send", resourceID, clientErr.Error()) | ||||
| 		return nil, clientErr | ||||
| 	} | ||||
|  | ||||
| 	response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PatchResource") | ||||
| 	if err != nil { | ||||
| 		if response != nil { | ||||
| 			klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode) | ||||
| 		} else { | ||||
| 			klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		retriableErr := retry.GetError(response, err) | ||||
| 		if !retriableErr.Retriable && | ||||
| 			strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) { | ||||
| 			klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error()) | ||||
| 			retriableErr.Retriable = true | ||||
| 		} | ||||
| 		return nil, retriableErr | ||||
| 	} | ||||
|  | ||||
| 	return response, nil | ||||
| } | ||||
|  | ||||
| // PutResourceAsync puts a resource by resource ID in async mode | ||||
| func (c *Client) PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error) { | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
|  | ||||
| 	request, err := c.PreparePutRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	future, resp, rErr := c.SendAsync(ctx, request) | ||||
| 	defer c.CloseResponse(ctx, resp) | ||||
| 	if rErr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, err) | ||||
| 		return nil, rErr | ||||
| 	} | ||||
|  | ||||
| 	return future, nil | ||||
| } | ||||
|  | ||||
| // PostResource posts a resource by resource ID | ||||
| func (c *Client) PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceID": resourceID, | ||||
| 		"action":     action, | ||||
| 	} | ||||
|  | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}/{action}", pathParameters), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
| 	request, err := c.PreparePostRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "post.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	return c.sendRequest(ctx, request) | ||||
| } | ||||
|  | ||||
| // DeleteResource deletes a resource by resource ID | ||||
| func (c *Client) DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error { | ||||
| 	future, clientErr := c.DeleteResourceAsync(ctx, resourceID, ifMatch) | ||||
| 	if clientErr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "delete.request", resourceID, clientErr.Error()) | ||||
| 		return clientErr | ||||
| 	} | ||||
|  | ||||
| 	if future == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := c.WaitForAsyncOperationCompletion(ctx, future, "armclient.DeleteResource"); err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "delete.wait", resourceID, clientErr.Error()) | ||||
| 		return retry.NewError(true, err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // HeadResource heads a resource by resource ID | ||||
| func (c *Client) HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error) { | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 	} | ||||
| 	request, err := c.PrepareHeadRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "head.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	return c.sendRequest(ctx, request) | ||||
| } | ||||
|  | ||||
| // DeleteResourceAsync delete a resource by resource ID and returns a future representing the async result | ||||
| func (c *Client) DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error) { | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 	} | ||||
| 	if len(ifMatch) > 0 { | ||||
| 		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(ifMatch))) | ||||
| 	} | ||||
|  | ||||
| 	deleteRequest, err := c.PrepareDeleteRequest(ctx, decorators...) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.prepare", resourceID, err) | ||||
| 		return nil, retry.NewError(false, err) | ||||
| 	} | ||||
|  | ||||
| 	resp, rerr := c.sendRequest(ctx, deleteRequest) | ||||
| 	defer c.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.send", resourceID, rerr.Error()) | ||||
| 		return nil, rerr | ||||
| 	} | ||||
|  | ||||
| 	err = autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent, http.StatusNotFound)) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.respond", resourceID, err) | ||||
| 		return nil, retry.GetError(resp, err) | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode == http.StatusNotFound { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	future, err := azure.NewFutureFromResponse(resp) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.future", resourceID, err) | ||||
| 		return nil, retry.GetError(resp, err) | ||||
| 	} | ||||
|  | ||||
| 	return &future, nil | ||||
| } | ||||
|  | ||||
| // CloseResponse closes a response | ||||
| func (c *Client) CloseResponse(ctx context.Context, response *http.Response) { | ||||
| 	if response != nil && response.Body != nil { | ||||
| 		if err := response.Body.Close(); err != nil { | ||||
| 			klog.Errorf("Error closing the response body: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *Client) prepareRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	decorators = append( | ||||
| 		decorators, | ||||
| 		withAPIVersion(c.apiVersion)) | ||||
| 	preparer := autorest.CreatePreparer(decorators...) | ||||
| 	return preparer.Prepare((&http.Request{}).WithContext(ctx)) | ||||
| } | ||||
|  | ||||
| func withAPIVersion(apiVersion string) autorest.PrepareDecorator { | ||||
| 	const apiVersionKey = "api-version" | ||||
| 	return func(p autorest.Preparer) autorest.Preparer { | ||||
| 		return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { | ||||
| 			r, err := p.Prepare(r) | ||||
| 			if err == nil { | ||||
| 				if r.URL == nil { | ||||
| 					return r, fmt.Errorf("Error in withAPIVersion: Invoked with a nil URL") | ||||
| 				} | ||||
|  | ||||
| 				v := r.URL.Query() | ||||
| 				if len(v.Get(apiVersionKey)) > 0 { | ||||
| 					return r, nil | ||||
| 				} | ||||
|  | ||||
| 				v.Add(apiVersionKey, apiVersion) | ||||
| 				r.URL.RawQuery = v.Encode() | ||||
| 			} | ||||
| 			return r, err | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetResourceID gets Azure resource ID | ||||
| func GetResourceID(subscriptionID, resourceGroupName, resourceType, resourceName string) string { | ||||
| 	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s", | ||||
| 		autorest.Encode("path", subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName), | ||||
| 		resourceType, | ||||
| 		autorest.Encode("path", resourceName)) | ||||
| } | ||||
|  | ||||
| // GetChildResourceID gets Azure child resource ID | ||||
| func GetChildResourceID(subscriptionID, resourceGroupName, resourceType, resourceName, childResourceType, childResourceName string) string { | ||||
| 	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s/%s", | ||||
| 		autorest.Encode("path", subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName), | ||||
| 		resourceType, | ||||
| 		autorest.Encode("path", resourceName), | ||||
| 		childResourceType, | ||||
| 		autorest.Encode("path", childResourceName)) | ||||
| } | ||||
|  | ||||
| // GetChildResourcesListID gets Azure child resources list ID | ||||
| func GetChildResourcesListID(subscriptionID, resourceGroupName, resourceType, resourceName, childResourceType string) string { | ||||
| 	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s", | ||||
| 		autorest.Encode("path", subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName), | ||||
| 		resourceType, | ||||
| 		autorest.Encode("path", resourceName), | ||||
| 		childResourceType) | ||||
| } | ||||
|  | ||||
| // GetProviderResourceID gets Azure RP resource ID | ||||
| func GetProviderResourceID(subscriptionID, providerNamespace string) string { | ||||
| 	return fmt.Sprintf("/subscriptions/%s/providers/%s", | ||||
| 		autorest.Encode("path", subscriptionID), | ||||
| 		providerNamespace) | ||||
| } | ||||
|  | ||||
| // GetProviderResourcesListID gets Azure RP resources list ID | ||||
| func GetProviderResourcesListID(subscriptionID string) string { | ||||
| 	return fmt.Sprintf("/subscriptions/%s/providers", autorest.Encode("path", subscriptionID)) | ||||
| } | ||||
| @@ -1,573 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package armclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	testResourceID = "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP" | ||||
| ) | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, "", "test", "2019-01-01", "eastus", backoff) | ||||
| 	assert.NotNil(t, armClient.backoff) | ||||
| 	assert.Equal(t, 3, armClient.backoff.Steps, "Backoff steps should be same as the value passed in") | ||||
|  | ||||
| 	backoff = &retry.Backoff{Steps: 0} | ||||
| 	armClient = New(nil, "", "test", "2019-01-01", "eastus", backoff) | ||||
| 	assert.NotNil(t, armClient.backoff) | ||||
| 	assert.Equal(t, 1, armClient.backoff.Steps, "Backoff steps should be default to 1 if it is 0") | ||||
|  | ||||
| 	armClient = New(nil, "", "test", "2019-01-01", "eastus", nil) | ||||
| 	assert.NotNil(t, armClient.backoff) | ||||
| 	assert.Equal(t, 1, armClient.backoff.Steps, "Backoff steps should be default to 1 if it is not set") | ||||
| } | ||||
|  | ||||
| func TestSend(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if count <= 1 { | ||||
| 			http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 			count++ | ||||
| 		} | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceGroupName": autorest.Encode("path", "testgroup"), | ||||
| 		"subscriptionId":    autorest.Encode("path", "testid"), | ||||
| 		"resourceName":      autorest.Encode("path", "testname"), | ||||
| 	} | ||||
|  | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters( | ||||
| 			"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	request, err := armClient.PrepareGetRequest(ctx, decorators...) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	response, rerr := armClient.Send(ctx, request) | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, 2, count) | ||||
| 	assert.Equal(t, http.StatusOK, response.StatusCode) | ||||
| } | ||||
|  | ||||
| func TestSendFailure(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 		count++ | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceGroupName": autorest.Encode("path", "testgroup"), | ||||
| 		"subscriptionId":    autorest.Encode("path", "testid"), | ||||
| 		"resourceName":      autorest.Encode("path", "testname"), | ||||
| 	} | ||||
|  | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters( | ||||
| 			"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	request, err := armClient.PreparePatchRequest(ctx, decorators...) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	response, rerr := armClient.Send(ctx, request) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.Equal(t, http.StatusInternalServerError, response.StatusCode) | ||||
| } | ||||
|  | ||||
| func TestSendThrottled(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Header().Set(retry.RetryAfterHeaderKey, "30") | ||||
| 		http.Error(w, "failed", http.StatusTooManyRequests) | ||||
| 		count++ | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceGroupName": autorest.Encode("path", "testgroup"), | ||||
| 		"subscriptionId":    autorest.Encode("path", "testid"), | ||||
| 		"resourceName":      autorest.Encode("path", "testname"), | ||||
| 	} | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters( | ||||
| 			"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	request, err := armClient.PrepareGetRequest(ctx, decorators...) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	response, rerr := armClient.Send(ctx, request) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, 1, count) | ||||
| 	assert.Equal(t, http.StatusTooManyRequests, response.StatusCode) | ||||
| } | ||||
|  | ||||
| func TestSendAsync(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusForbidden) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceGroupName": autorest.Encode("path", "testgroup"), | ||||
| 		"subscriptionId":    autorest.Encode("path", "testid"), | ||||
| 		"resourceName":      autorest.Encode("path", "testname"), | ||||
| 	} | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters( | ||||
| 			"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	request, err := armClient.PreparePutRequest(ctx, decorators...) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	future, response, rerr := armClient.SendAsync(ctx, request) | ||||
| 	assert.Nil(t, future) | ||||
| 	assert.Nil(t, response) | ||||
| 	assert.Equal(t, 1, count) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, false, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestSendAsyncSuccess(t *testing.T) { | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.WriteHeader(http.StatusOK) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	pathParameters := map[string]interface{}{ | ||||
| 		"resourceGroupName": autorest.Encode("path", "testgroup"), | ||||
| 		"subscriptionId":    autorest.Encode("path", "testid"), | ||||
| 		"resourceName":      autorest.Encode("path", "testname"), | ||||
| 	} | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters( | ||||
| 			"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	request, err := armClient.PreparePostRequest(ctx, decorators...) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	future, response, rerr := armClient.SendAsync(ctx, request) | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.NotNil(t, response) | ||||
| 	assert.NotNil(t, future) | ||||
| } | ||||
|  | ||||
| func TestNormalizeAzureRegion(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		region   string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			region:   "eastus", | ||||
| 			expected: "eastus", | ||||
| 		}, | ||||
| 		{ | ||||
| 			region:   " eastus ", | ||||
| 			expected: "eastus", | ||||
| 		}, | ||||
| 		{ | ||||
| 			region:   " eastus\t", | ||||
| 			expected: "eastus", | ||||
| 		}, | ||||
| 		{ | ||||
| 			region:   " eastus\v", | ||||
| 			expected: "eastus", | ||||
| 		}, | ||||
| 		{ | ||||
| 			region:   " eastus\v\r\f\n", | ||||
| 			expected: "eastus", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range tests { | ||||
| 		real := NormalizeAzureRegion(test.region) | ||||
| 		assert.Equal(t, test.expected, real, "test[%d]: NormalizeAzureRegion(%q) != %q", i, test.region, test.expected) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetResource(t *testing.T) { | ||||
| 	expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?%24expand=data&api-version=2019-01-01" | ||||
|  | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		assert.Equal(t, "GET", r.Method) | ||||
| 		assert.Equal(t, expectedURI, r.URL.String()) | ||||
| 		w.WriteHeader(http.StatusOK) | ||||
| 		w.Write([]byte("{data: testPIP}")) | ||||
| 		count++ | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	response, rerr := armClient.GetResource(ctx, testResourceID, "data") | ||||
| 	byteResponseBody, _ := ioutil.ReadAll(response.Body) | ||||
| 	stringResponseBody := string(byteResponseBody) | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, "{data: testPIP}", stringResponseBody) | ||||
| 	assert.Equal(t, 1, count) | ||||
| } | ||||
|  | ||||
| func TestGetResourceWithDecorators(t *testing.T) { | ||||
| 	expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01¶m1=value1¶m2=value2" | ||||
|  | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		assert.Equal(t, "GET", r.Method) | ||||
| 		assert.Equal(t, expectedURI, r.URL.String()) | ||||
| 		w.WriteHeader(http.StatusOK) | ||||
| 		w.Write([]byte("{data: testPIP}")) | ||||
| 		count++ | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	params := map[string]interface{}{ | ||||
| 		"param1": "value1", | ||||
| 		"param2": "value2", | ||||
| 	} | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithQueryParameters(params), | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	response, rerr := armClient.GetResourceWithDecorators(ctx, testResourceID, decorators) | ||||
| 	byteResponseBody, _ := ioutil.ReadAll(response.Body) | ||||
| 	stringResponseBody := string(byteResponseBody) | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, "{data: testPIP}", stringResponseBody) | ||||
| 	assert.Equal(t, 1, count) | ||||
| } | ||||
|  | ||||
| func TestPutResource(t *testing.T) { | ||||
| 	expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01" | ||||
| 	operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01" | ||||
| 	handlers := []func(http.ResponseWriter, *http.Request){ | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "PUT", req.Method) | ||||
| 			assert.Equal(t, expectedURI, req.URL.String()) | ||||
| 			rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), | ||||
| 				fmt.Sprintf("http://%s%s", req.Host, operationURI)) | ||||
| 			rw.WriteHeader(http.StatusCreated) | ||||
| 		}, | ||||
|  | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "GET", req.Method) | ||||
| 			assert.Equal(t, operationURI, req.URL.String()) | ||||
|  | ||||
| 			rw.WriteHeader(http.StatusOK) | ||||
| 			rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		handlers[count](w, r) | ||||
| 		count++ | ||||
| 		if count > 1 { | ||||
| 			count = 1 | ||||
| 		} | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	response, rerr := armClient.PutResource(ctx, testResourceID, nil) | ||||
| 	assert.Equal(t, 1, count) | ||||
| 	assert.Nil(t, response) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestPutResources(t *testing.T) { | ||||
| 	serverFuncs := []func(rw http.ResponseWriter, req *http.Request){ | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "PUT", req.Method) | ||||
|  | ||||
| 			rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), | ||||
| 				fmt.Sprintf("http://%s%s", req.Host, "/id/1?api-version=2019-01-01")) | ||||
| 			rw.WriteHeader(http.StatusCreated) | ||||
| 		}, | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "PUT", req.Method) | ||||
|  | ||||
| 			rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), | ||||
| 				fmt.Sprintf("http://%s%s", req.Host, "/id/2?api-version=2019-01-01")) | ||||
| 			rw.WriteHeader(http.StatusInternalServerError) | ||||
| 		}, | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "GET", req.Method) | ||||
|  | ||||
| 			rw.WriteHeader(http.StatusOK) | ||||
| 			rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) | ||||
| 		}, | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "GET", req.Method) | ||||
|  | ||||
| 			rw.WriteHeader(http.StatusOK) | ||||
| 			rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	i, total := 0, 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		serverFuncs[i](w, r) | ||||
| 		i++ | ||||
| 		if i > 3 { | ||||
| 			i = 3 | ||||
| 		} | ||||
| 		total++ | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resources := map[string]interface{}{ | ||||
| 		"/id/1": nil, | ||||
| 		"/id/2": nil, | ||||
| 	} | ||||
| 	responses := armClient.PutResources(ctx, nil) | ||||
| 	assert.Nil(t, responses) | ||||
| 	responses = armClient.PutResources(ctx, resources) | ||||
| 	assert.NotNil(t, responses) | ||||
| 	assert.Equal(t, 3, total) | ||||
| } | ||||
|  | ||||
| func TestPutResourceAsync(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resourceID := testResourceID | ||||
| 	future, rerr := armClient.PutResourceAsync(ctx, resourceID, "") | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.Nil(t, future) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestDeleteResourceAsync(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resourceID := testResourceID | ||||
| 	future, rerr := armClient.DeleteResourceAsync(ctx, resourceID, "") | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.Nil(t, future) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestPatchResource(t *testing.T) { | ||||
| 	expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01" | ||||
| 	operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01" | ||||
| 	handlers := []func(http.ResponseWriter, *http.Request){ | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "PATCH", req.Method) | ||||
| 			assert.Equal(t, expectedURI, req.URL.String()) | ||||
| 			rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"), | ||||
| 				fmt.Sprintf("http://%s%s", req.Host, operationURI)) | ||||
| 			rw.WriteHeader(http.StatusCreated) | ||||
| 		}, | ||||
|  | ||||
| 		func(rw http.ResponseWriter, req *http.Request) { | ||||
| 			assert.Equal(t, "GET", req.Method) | ||||
| 			assert.Equal(t, operationURI, req.URL.String()) | ||||
|  | ||||
| 			rw.WriteHeader(http.StatusOK) | ||||
| 			rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`)) | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		handlers[count](w, r) | ||||
| 		count++ | ||||
| 		if count > 1 { | ||||
| 			count = 1 | ||||
| 		} | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 1} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	response, rerr := armClient.PatchResource(ctx, testResourceID, nil) | ||||
| 	assert.Equal(t, 1, count) | ||||
| 	assert.Nil(t, response) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestPostResource(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resourceID := testResourceID | ||||
| 	future, rerr := armClient.PostResource(ctx, resourceID, "post", "") | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.NotNil(t, future) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestDeleteResource(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resourceID := testResourceID | ||||
| 	rerr := armClient.DeleteResource(ctx, resourceID, "") | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestHeadResource(t *testing.T) { | ||||
| 	count := 0 | ||||
| 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		count++ | ||||
| 		http.Error(w, "failed", http.StatusInternalServerError) | ||||
| 	})) | ||||
|  | ||||
| 	backoff := &retry.Backoff{Steps: 3} | ||||
| 	armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff) | ||||
| 	armClient.client.RetryDuration = time.Millisecond * 1 | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	resourceID := testResourceID | ||||
| 	response, rerr := armClient.HeadResource(ctx, resourceID) | ||||
| 	assert.Equal(t, 3, count) | ||||
| 	assert.NotNil(t, response) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, true, rerr.Retriable) | ||||
| } | ||||
|  | ||||
| func TestGetResourceID(t *testing.T) { | ||||
| 	expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name" | ||||
|  | ||||
| 	resourceID := GetResourceID("sub", "rg", "type", "name") | ||||
| 	assert.Equal(t, expectedResourceID, resourceID) | ||||
| } | ||||
|  | ||||
| func TestGetChildResourceID(t *testing.T) { | ||||
| 	expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2/name-3" | ||||
|  | ||||
| 	resourceID := GetChildResourceID("sub", "rg", "type", "name-1", "name-2", "name-3") | ||||
| 	assert.Equal(t, expectedResourceID, resourceID) | ||||
| } | ||||
|  | ||||
| func TestGetChildResourcesListID(t *testing.T) { | ||||
| 	expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2" | ||||
|  | ||||
| 	resourceID := GetChildResourcesListID("sub", "rg", "type", "name-1", "name-2") | ||||
| 	assert.Equal(t, expectedResourceID, resourceID) | ||||
| } | ||||
|  | ||||
| func TestGetProviderResourceID(t *testing.T) { | ||||
| 	expectedResourceID := "/subscriptions/sub/providers/namespace" | ||||
|  | ||||
| 	resourceID := GetProviderResourceID("sub", "namespace") | ||||
| 	assert.Equal(t, expectedResourceID, resourceID) | ||||
| } | ||||
|  | ||||
| func TestGetProviderResourcesListID(t *testing.T) { | ||||
| 	expectedResourceID := "/subscriptions/sub/providers" | ||||
|  | ||||
| 	resourceID := GetProviderResourcesListID("sub") | ||||
| 	assert.Equal(t, expectedResourceID, resourceID) | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package armclient implements the client for ARM. | ||||
| package armclient // import "k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| @@ -1,104 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| //go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go  -destination=mockarmclient/interface.go -package=mockarmclient Interface | ||||
| package armclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| // PutResourcesResponse defines the response for PutResources. | ||||
| type PutResourcesResponse struct { | ||||
| 	Response *http.Response | ||||
| 	Error    *retry.Error | ||||
| } | ||||
|  | ||||
| // Interface is the client interface for ARM. | ||||
| type Interface interface { | ||||
| 	// Send sends a http request to ARM service with possible retry to regional ARM endpoint. | ||||
| 	Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// PreparePutRequest prepares put request | ||||
| 	PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) | ||||
|  | ||||
| 	// PreparePostRequest prepares post request | ||||
| 	PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) | ||||
|  | ||||
| 	// PrepareGetRequest prepares get request | ||||
| 	PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) | ||||
|  | ||||
| 	// PrepareDeleteRequest preparse delete request | ||||
| 	PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) | ||||
|  | ||||
| 	// PrepareHeadRequest prepares head request | ||||
| 	PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) | ||||
|  | ||||
| 	// WaitForAsyncOperationCompletion waits for an operation completion | ||||
| 	WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error | ||||
|  | ||||
| 	// WaitForAsyncOperationResult waits for an operation result. | ||||
| 	WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error) | ||||
|  | ||||
| 	// SendAsync send a request and return a future object representing the async result as well as the origin http response | ||||
| 	SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error) | ||||
|  | ||||
| 	// PutResource puts a resource by resource ID | ||||
| 	PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// PutResources puts a list of resources from resources map[resourceID]parameters. | ||||
| 	// Those resources sync requests are sequential while async requests are concurrent. It 's especially | ||||
| 	// useful when the ARM API doesn't support concurrent requests. | ||||
| 	PutResources(ctx context.Context, resources map[string]interface{}) map[string]*PutResourcesResponse | ||||
|  | ||||
| 	// PutResourceWithDecorators puts a resource with decorators by resource ID | ||||
| 	PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// PatchResource patches a resource by resource ID | ||||
| 	PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// PutResourceAsync puts a resource by resource ID in async mode | ||||
| 	PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error) | ||||
|  | ||||
| 	// HeadResource heads a resource by resource ID | ||||
| 	HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// GetResource get a resource by resource ID | ||||
| 	GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	//GetResourceWithDecorators get a resource with decorators by resource ID | ||||
| 	GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// PostResource posts a resource by resource ID | ||||
| 	PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error) | ||||
|  | ||||
| 	// DeleteResource deletes a resource by resource ID | ||||
| 	DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error | ||||
|  | ||||
| 	// DeleteResourceAsync delete a resource by resource ID and returns a future representing the async result | ||||
| 	DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error) | ||||
|  | ||||
| 	// CloseResponse closes a response | ||||
| 	CloseResponse(ctx context.Context, response *http.Response) | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package mockarmclient implements the mock client for ARM. | ||||
| package mockarmclient // import "k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" | ||||
| @@ -1,394 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Code generated by MockGen. DO NOT EDIT. | ||||
| // Source: interface.go | ||||
|  | ||||
| // Package mockarmclient is a generated GoMock package. | ||||
| package mockarmclient | ||||
|  | ||||
| import ( | ||||
| 	context "context" | ||||
| 	http "net/http" | ||||
| 	reflect "reflect" | ||||
|  | ||||
| 	autorest "github.com/Azure/go-autorest/autorest" | ||||
| 	azure "github.com/Azure/go-autorest/autorest/azure" | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| 	armclient "k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	retry "k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| // MockInterface is a mock of Interface interface. | ||||
| type MockInterface struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockInterfaceMockRecorder | ||||
| } | ||||
|  | ||||
| // MockInterfaceMockRecorder is the mock recorder for MockInterface. | ||||
| type MockInterfaceMockRecorder struct { | ||||
| 	mock *MockInterface | ||||
| } | ||||
|  | ||||
| // NewMockInterface creates a new mock instance. | ||||
| func NewMockInterface(ctrl *gomock.Controller) *MockInterface { | ||||
| 	mock := &MockInterface{ctrl: ctrl} | ||||
| 	mock.recorder = &MockInterfaceMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // CloseResponse mocks base method. | ||||
| func (m *MockInterface) CloseResponse(ctx context.Context, response *http.Response) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	m.ctrl.Call(m, "CloseResponse", ctx, response) | ||||
| } | ||||
|  | ||||
| // CloseResponse indicates an expected call of CloseResponse. | ||||
| func (mr *MockInterfaceMockRecorder) CloseResponse(ctx, response interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseResponse", reflect.TypeOf((*MockInterface)(nil).CloseResponse), ctx, response) | ||||
| } | ||||
|  | ||||
| // DeleteResource mocks base method. | ||||
| func (m *MockInterface) DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "DeleteResource", ctx, resourceID, ifMatch) | ||||
| 	ret0, _ := ret[0].(*retry.Error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // DeleteResource indicates an expected call of DeleteResource. | ||||
| func (mr *MockInterfaceMockRecorder) DeleteResource(ctx, resourceID, ifMatch interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResource", reflect.TypeOf((*MockInterface)(nil).DeleteResource), ctx, resourceID, ifMatch) | ||||
| } | ||||
|  | ||||
| // DeleteResourceAsync mocks base method. | ||||
| func (m *MockInterface) DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "DeleteResourceAsync", ctx, resourceID, ifMatch) | ||||
| 	ret0, _ := ret[0].(*azure.Future) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // DeleteResourceAsync indicates an expected call of DeleteResourceAsync. | ||||
| func (mr *MockInterfaceMockRecorder) DeleteResourceAsync(ctx, resourceID, ifMatch interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResourceAsync", reflect.TypeOf((*MockInterface)(nil).DeleteResourceAsync), ctx, resourceID, ifMatch) | ||||
| } | ||||
|  | ||||
| // GetResource mocks base method. | ||||
| func (m *MockInterface) GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetResource", ctx, resourceID, expand) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetResource indicates an expected call of GetResource. | ||||
| func (mr *MockInterfaceMockRecorder) GetResource(ctx, resourceID, expand interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResource", reflect.TypeOf((*MockInterface)(nil).GetResource), ctx, resourceID, expand) | ||||
| } | ||||
|  | ||||
| // GetResourceWithDecorators mocks base method. | ||||
| func (m *MockInterface) GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "GetResourceWithDecorators", ctx, resourceID, decorators) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // GetResourceWithDecorators indicates an expected call of GetResourceWithDecorators. | ||||
| func (mr *MockInterfaceMockRecorder) GetResourceWithDecorators(ctx, resourceID, decorators interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceWithDecorators", reflect.TypeOf((*MockInterface)(nil).GetResourceWithDecorators), ctx, resourceID, decorators) | ||||
| } | ||||
|  | ||||
| // HeadResource mocks base method. | ||||
| func (m *MockInterface) HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "HeadResource", ctx, resourceID) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // HeadResource indicates an expected call of HeadResource. | ||||
| func (mr *MockInterfaceMockRecorder) HeadResource(ctx, resourceID interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadResource", reflect.TypeOf((*MockInterface)(nil).HeadResource), ctx, resourceID) | ||||
| } | ||||
|  | ||||
| // PatchResource mocks base method. | ||||
| func (m *MockInterface) PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PatchResource", ctx, resourceID, parameters) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PatchResource indicates an expected call of PatchResource. | ||||
| func (mr *MockInterfaceMockRecorder) PatchResource(ctx, resourceID, parameters interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchResource", reflect.TypeOf((*MockInterface)(nil).PatchResource), ctx, resourceID, parameters) | ||||
| } | ||||
|  | ||||
| // PostResource mocks base method. | ||||
| func (m *MockInterface) PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PostResource", ctx, resourceID, action, parameters) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PostResource indicates an expected call of PostResource. | ||||
| func (mr *MockInterfaceMockRecorder) PostResource(ctx, resourceID, action, parameters interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostResource", reflect.TypeOf((*MockInterface)(nil).PostResource), ctx, resourceID, action, parameters) | ||||
| } | ||||
|  | ||||
| // PrepareDeleteRequest mocks base method. | ||||
| func (m *MockInterface) PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	varargs := []interface{}{ctx} | ||||
| 	for _, a := range decorators { | ||||
| 		varargs = append(varargs, a) | ||||
| 	} | ||||
| 	ret := m.ctrl.Call(m, "PrepareDeleteRequest", varargs...) | ||||
| 	ret0, _ := ret[0].(*http.Request) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PrepareDeleteRequest indicates an expected call of PrepareDeleteRequest. | ||||
| func (mr *MockInterfaceMockRecorder) PrepareDeleteRequest(ctx interface{}, decorators ...interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	varargs := append([]interface{}{ctx}, decorators...) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareDeleteRequest", reflect.TypeOf((*MockInterface)(nil).PrepareDeleteRequest), varargs...) | ||||
| } | ||||
|  | ||||
| // PrepareGetRequest mocks base method. | ||||
| func (m *MockInterface) PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	varargs := []interface{}{ctx} | ||||
| 	for _, a := range decorators { | ||||
| 		varargs = append(varargs, a) | ||||
| 	} | ||||
| 	ret := m.ctrl.Call(m, "PrepareGetRequest", varargs...) | ||||
| 	ret0, _ := ret[0].(*http.Request) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PrepareGetRequest indicates an expected call of PrepareGetRequest. | ||||
| func (mr *MockInterfaceMockRecorder) PrepareGetRequest(ctx interface{}, decorators ...interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	varargs := append([]interface{}{ctx}, decorators...) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareGetRequest", reflect.TypeOf((*MockInterface)(nil).PrepareGetRequest), varargs...) | ||||
| } | ||||
|  | ||||
| // PrepareHeadRequest mocks base method. | ||||
| func (m *MockInterface) PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	varargs := []interface{}{ctx} | ||||
| 	for _, a := range decorators { | ||||
| 		varargs = append(varargs, a) | ||||
| 	} | ||||
| 	ret := m.ctrl.Call(m, "PrepareHeadRequest", varargs...) | ||||
| 	ret0, _ := ret[0].(*http.Request) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PrepareHeadRequest indicates an expected call of PrepareHeadRequest. | ||||
| func (mr *MockInterfaceMockRecorder) PrepareHeadRequest(ctx interface{}, decorators ...interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	varargs := append([]interface{}{ctx}, decorators...) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareHeadRequest", reflect.TypeOf((*MockInterface)(nil).PrepareHeadRequest), varargs...) | ||||
| } | ||||
|  | ||||
| // PreparePostRequest mocks base method. | ||||
| func (m *MockInterface) PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	varargs := []interface{}{ctx} | ||||
| 	for _, a := range decorators { | ||||
| 		varargs = append(varargs, a) | ||||
| 	} | ||||
| 	ret := m.ctrl.Call(m, "PreparePostRequest", varargs...) | ||||
| 	ret0, _ := ret[0].(*http.Request) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PreparePostRequest indicates an expected call of PreparePostRequest. | ||||
| func (mr *MockInterfaceMockRecorder) PreparePostRequest(ctx interface{}, decorators ...interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	varargs := append([]interface{}{ctx}, decorators...) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePostRequest", reflect.TypeOf((*MockInterface)(nil).PreparePostRequest), varargs...) | ||||
| } | ||||
|  | ||||
| // PreparePutRequest mocks base method. | ||||
| func (m *MockInterface) PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	varargs := []interface{}{ctx} | ||||
| 	for _, a := range decorators { | ||||
| 		varargs = append(varargs, a) | ||||
| 	} | ||||
| 	ret := m.ctrl.Call(m, "PreparePutRequest", varargs...) | ||||
| 	ret0, _ := ret[0].(*http.Request) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PreparePutRequest indicates an expected call of PreparePutRequest. | ||||
| func (mr *MockInterfaceMockRecorder) PreparePutRequest(ctx interface{}, decorators ...interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	varargs := append([]interface{}{ctx}, decorators...) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePutRequest", reflect.TypeOf((*MockInterface)(nil).PreparePutRequest), varargs...) | ||||
| } | ||||
|  | ||||
| // PutResource mocks base method. | ||||
| func (m *MockInterface) PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PutResource", ctx, resourceID, parameters) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PutResource indicates an expected call of PutResource. | ||||
| func (mr *MockInterfaceMockRecorder) PutResource(ctx, resourceID, parameters interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResource", reflect.TypeOf((*MockInterface)(nil).PutResource), ctx, resourceID, parameters) | ||||
| } | ||||
|  | ||||
| // PutResourceAsync mocks base method. | ||||
| func (m *MockInterface) PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PutResourceAsync", ctx, resourceID, parameters) | ||||
| 	ret0, _ := ret[0].(*azure.Future) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PutResourceAsync indicates an expected call of PutResourceAsync. | ||||
| func (mr *MockInterfaceMockRecorder) PutResourceAsync(ctx, resourceID, parameters interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceAsync", reflect.TypeOf((*MockInterface)(nil).PutResourceAsync), ctx, resourceID, parameters) | ||||
| } | ||||
|  | ||||
| // PutResourceWithDecorators mocks base method. | ||||
| func (m *MockInterface) PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PutResourceWithDecorators", ctx, resourceID, parameters, decorators) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // PutResourceWithDecorators indicates an expected call of PutResourceWithDecorators. | ||||
| func (mr *MockInterfaceMockRecorder) PutResourceWithDecorators(ctx, resourceID, parameters, decorators interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceWithDecorators", reflect.TypeOf((*MockInterface)(nil).PutResourceWithDecorators), ctx, resourceID, parameters, decorators) | ||||
| } | ||||
|  | ||||
| // PutResources mocks base method. | ||||
| func (m *MockInterface) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*armclient.PutResourcesResponse { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "PutResources", ctx, resources) | ||||
| 	ret0, _ := ret[0].(map[string]*armclient.PutResourcesResponse) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // PutResources indicates an expected call of PutResources. | ||||
| func (mr *MockInterfaceMockRecorder) PutResources(ctx, resources interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResources", reflect.TypeOf((*MockInterface)(nil).PutResources), ctx, resources) | ||||
| } | ||||
|  | ||||
| // Send mocks base method. | ||||
| func (m *MockInterface) Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Send", ctx, request) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // Send indicates an expected call of Send. | ||||
| func (mr *MockInterfaceMockRecorder) Send(ctx, request interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockInterface)(nil).Send), ctx, request) | ||||
| } | ||||
|  | ||||
| // SendAsync mocks base method. | ||||
| func (m *MockInterface) SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "SendAsync", ctx, request) | ||||
| 	ret0, _ := ret[0].(*azure.Future) | ||||
| 	ret1, _ := ret[1].(*http.Response) | ||||
| 	ret2, _ := ret[2].(*retry.Error) | ||||
| 	return ret0, ret1, ret2 | ||||
| } | ||||
|  | ||||
| // SendAsync indicates an expected call of SendAsync. | ||||
| func (mr *MockInterfaceMockRecorder) SendAsync(ctx, request interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendAsync", reflect.TypeOf((*MockInterface)(nil).SendAsync), ctx, request) | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationCompletion mocks base method. | ||||
| func (m *MockInterface) WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "WaitForAsyncOperationCompletion", ctx, future, asyncOperationName) | ||||
| 	ret0, _ := ret[0].(error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationCompletion indicates an expected call of WaitForAsyncOperationCompletion. | ||||
| func (mr *MockInterfaceMockRecorder) WaitForAsyncOperationCompletion(ctx, future, asyncOperationName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForAsyncOperationCompletion", reflect.TypeOf((*MockInterface)(nil).WaitForAsyncOperationCompletion), ctx, future, asyncOperationName) | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationResult mocks base method. | ||||
| func (m *MockInterface) WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "WaitForAsyncOperationResult", ctx, future, asyncOperationName) | ||||
| 	ret0, _ := ret[0].(*http.Response) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // WaitForAsyncOperationResult indicates an expected call of WaitForAsyncOperationResult. | ||||
| func (mr *MockInterfaceMockRecorder) WaitForAsyncOperationResult(ctx, future, asyncOperationName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForAsyncOperationResult", reflect.TypeOf((*MockInterface)(nil).WaitForAsyncOperationResult), ctx, future, asyncOperationName) | ||||
| } | ||||
| @@ -1,82 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package clients | ||||
|  | ||||
| import ( | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| // ClientConfig contains all essential information to create an Azure client. | ||||
| type ClientConfig struct { | ||||
| 	CloudName               string | ||||
| 	Location                string | ||||
| 	SubscriptionID          string | ||||
| 	ResourceManagerEndpoint string | ||||
| 	Authorizer              autorest.Authorizer | ||||
| 	RateLimitConfig         *RateLimitConfig | ||||
| 	Backoff                 *retry.Backoff | ||||
| 	UserAgent               string | ||||
| } | ||||
|  | ||||
| // WithRateLimiter returns a new ClientConfig with rateLimitConfig set. | ||||
| func (cfg *ClientConfig) WithRateLimiter(rl *RateLimitConfig) *ClientConfig { | ||||
| 	newClientConfig := *cfg | ||||
| 	newClientConfig.RateLimitConfig = rl | ||||
| 	return &newClientConfig | ||||
| } | ||||
|  | ||||
| // RateLimitConfig indicates the rate limit config options. | ||||
| type RateLimitConfig struct { | ||||
| 	// Enable rate limiting | ||||
| 	CloudProviderRateLimit bool `json:"cloudProviderRateLimit,omitempty" yaml:"cloudProviderRateLimit,omitempty"` | ||||
| 	// Rate limit QPS (Read) | ||||
| 	CloudProviderRateLimitQPS float32 `json:"cloudProviderRateLimitQPS,omitempty" yaml:"cloudProviderRateLimitQPS,omitempty"` | ||||
| 	// Rate limit Bucket Size | ||||
| 	CloudProviderRateLimitBucket int `json:"cloudProviderRateLimitBucket,omitempty" yaml:"cloudProviderRateLimitBucket,omitempty"` | ||||
| 	// Rate limit QPS (Write) | ||||
| 	CloudProviderRateLimitQPSWrite float32 `json:"cloudProviderRateLimitQPSWrite,omitempty" yaml:"cloudProviderRateLimitQPSWrite,omitempty"` | ||||
| 	// Rate limit Bucket Size | ||||
| 	CloudProviderRateLimitBucketWrite int `json:"cloudProviderRateLimitBucketWrite,omitempty" yaml:"cloudProviderRateLimitBucketWrite,omitempty"` | ||||
| } | ||||
|  | ||||
| // RateLimitEnabled returns true if CloudProviderRateLimit is set to true. | ||||
| func RateLimitEnabled(config *RateLimitConfig) bool { | ||||
| 	return config != nil && config.CloudProviderRateLimit | ||||
| } | ||||
|  | ||||
| // NewRateLimiter creates new read and write flowcontrol.RateLimiter from RateLimitConfig. | ||||
| func NewRateLimiter(config *RateLimitConfig) (flowcontrol.RateLimiter, flowcontrol.RateLimiter) { | ||||
| 	readLimiter := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	writeLimiter := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
|  | ||||
| 	if config != nil && config.CloudProviderRateLimit { | ||||
| 		readLimiter = flowcontrol.NewTokenBucketRateLimiter( | ||||
| 			config.CloudProviderRateLimitQPS, | ||||
| 			config.CloudProviderRateLimitBucket) | ||||
|  | ||||
| 		writeLimiter = flowcontrol.NewTokenBucketRateLimiter( | ||||
| 			config.CloudProviderRateLimitQPSWrite, | ||||
| 			config.CloudProviderRateLimitBucketWrite) | ||||
| 	} | ||||
|  | ||||
| 	return readLimiter, writeLimiter | ||||
| } | ||||
| @@ -1,69 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2019 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package clients | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| ) | ||||
|  | ||||
| func TestWithRateLimiter(t *testing.T) { | ||||
| 	config := &ClientConfig{} | ||||
| 	assert.Nil(t, config.RateLimitConfig) | ||||
| 	c := config.WithRateLimiter(&RateLimitConfig{CloudProviderRateLimit: true}) | ||||
| 	assert.Equal(t, &RateLimitConfig{CloudProviderRateLimit: true}, c.RateLimitConfig) | ||||
| 	config.WithRateLimiter(nil) | ||||
| 	assert.Nil(t, config.RateLimitConfig) | ||||
| } | ||||
|  | ||||
| func TestRateLimitEnabled(t *testing.T) { | ||||
| 	assert.Equal(t, false, RateLimitEnabled(nil)) | ||||
| 	config := &RateLimitConfig{} | ||||
| 	assert.Equal(t, false, RateLimitEnabled(config)) | ||||
| 	config.CloudProviderRateLimit = true | ||||
| 	assert.Equal(t, true, RateLimitEnabled(config)) | ||||
| } | ||||
|  | ||||
| func TestNewRateLimiter(t *testing.T) { | ||||
| 	fakeRateLimiter := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	readLimiter, writeLimiter := NewRateLimiter(nil) | ||||
| 	assert.Equal(t, readLimiter, fakeRateLimiter) | ||||
| 	assert.Equal(t, writeLimiter, fakeRateLimiter) | ||||
|  | ||||
| 	rateLimitConfig := &RateLimitConfig{ | ||||
| 		CloudProviderRateLimit: false, | ||||
| 	} | ||||
| 	readLimiter, writeLimiter = NewRateLimiter(rateLimitConfig) | ||||
| 	assert.Equal(t, readLimiter, fakeRateLimiter) | ||||
| 	assert.Equal(t, writeLimiter, fakeRateLimiter) | ||||
|  | ||||
| 	rateLimitConfig = &RateLimitConfig{ | ||||
| 		CloudProviderRateLimit:            true, | ||||
| 		CloudProviderRateLimitQPS:         3, | ||||
| 		CloudProviderRateLimitBucket:      10, | ||||
| 		CloudProviderRateLimitQPSWrite:    1, | ||||
| 		CloudProviderRateLimitBucketWrite: 3, | ||||
| 	} | ||||
| 	readLimiter, writeLimiter = NewRateLimiter(rateLimitConfig) | ||||
| 	assert.Equal(t, flowcontrol.NewTokenBucketRateLimiter(3, 10), readLimiter) | ||||
| 	assert.Equal(t, flowcontrol.NewTokenBucketRateLimiter(1, 3), writeLimiter) | ||||
| } | ||||
| @@ -1,419 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package containerserviceclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
|  | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/metrics" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var _ Interface = &Client{} | ||||
|  | ||||
| // Client implements ContainerService client Interface. | ||||
| type Client struct { | ||||
| 	armClient      armclient.Interface | ||||
| 	subscriptionID string | ||||
|  | ||||
| 	// Rate limiting configures. | ||||
| 	rateLimiterReader flowcontrol.RateLimiter | ||||
| 	rateLimiterWriter flowcontrol.RateLimiter | ||||
|  | ||||
| 	// ARM throttling configures. | ||||
| 	RetryAfterReader time.Time | ||||
| 	RetryAfterWriter time.Time | ||||
| } | ||||
|  | ||||
| // New creates a new ContainerServiceClient client with ratelimiting. | ||||
| func New(config *azclients.ClientConfig) *Client { | ||||
| 	baseURI := config.ResourceManagerEndpoint | ||||
| 	authorizer := config.Authorizer | ||||
| 	armClient := armclient.New(authorizer, baseURI, config.UserAgent, APIVersion, config.Location, config.Backoff) | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig) | ||||
|  | ||||
| 	klog.V(2).Infof("Azure ContainerServiceClient (read ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPS, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucket) | ||||
| 	klog.V(2).Infof("Azure ContainerServiceClient (write ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPSWrite, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucketWrite) | ||||
|  | ||||
| 	client := &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 		subscriptionID:    config.SubscriptionID, | ||||
| 	} | ||||
|  | ||||
| 	return client | ||||
| } | ||||
|  | ||||
| // Get gets a ManagedCluster. | ||||
| func (c *Client) Get(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("managed_clusters", "get", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterReader.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return containerservice.ManagedCluster{}, retry.GetRateLimitError(false, "GetManagedCluster") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterReader.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("GetManagedCluster", "client throttled", c.RetryAfterReader) | ||||
| 		return containerservice.ManagedCluster{}, rerr | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.getManagedCluster(ctx, resourceGroupName, managedClusterName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterReader = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // getManagedCluster gets a ManagedCluster. | ||||
| func (c *Client) getManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.ContainerService/managedClusters", | ||||
| 		managedClusterName, | ||||
| 	) | ||||
| 	result := containerservice.ManagedCluster{} | ||||
|  | ||||
| 	response, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.get.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	err := autorest.Respond( | ||||
| 		response, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.get.respond", resourceID, err) | ||||
| 		return result, retry.GetError(response, err) | ||||
| 	} | ||||
|  | ||||
| 	result.Response = autorest.Response{Response: response} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // List gets a list of ManagedClusters in the resource group. | ||||
| func (c *Client) List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("managed_clusters", "list", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterReader.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return nil, retry.GetRateLimitError(false, "ListManagedCluster") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterReader.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("ListManagedCluster", "client throttled", c.RetryAfterReader) | ||||
| 		return nil, rerr | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.listManagedCluster(ctx, resourceGroupName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterReader = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // listManagedCluster gets a list of ManagedClusters in the resource group. | ||||
| func (c *Client) listManagedCluster(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) { | ||||
| 	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters", | ||||
| 		autorest.Encode("path", c.subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName)) | ||||
| 	result := make([]containerservice.ManagedCluster, 0) | ||||
| 	page := &ManagedClusterResultPage{} | ||||
| 	page.fn = c.listNextResults | ||||
|  | ||||
| 	resp, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	page.mclr, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.respond", resourceID, err) | ||||
| 		return result, retry.GetError(resp, err) | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		result = append(result, page.Values()...) | ||||
|  | ||||
| 		// Abort the loop when there's no nextLink in the response. | ||||
| 		if pointer.StringDeref(page.Response().NextLink, "") == "" { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if err = page.NextWithContext(ctx); err != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.next", resourceID, err) | ||||
| 			return result, retry.GetError(page.Response().Response.Response, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (c *Client) listResponder(resp *http.Response) (result containerservice.ManagedClusterListResult, err error) { | ||||
| 	err = autorest.Respond( | ||||
| 		resp, | ||||
| 		autorest.ByIgnoring(), | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // managedClusterListResultPreparer prepares a request to retrieve the next set of results. | ||||
| // It returns nil if no more results exist. | ||||
| func (c *Client) managedClusterListResultPreparer(ctx context.Context, mclr containerservice.ManagedClusterListResult) (*http.Request, error) { | ||||
| 	if mclr.NextLink == nil || len(pointer.StringDeref(mclr.NextLink, "")) < 1 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithBaseURL(pointer.StringDeref(mclr.NextLink, "")), | ||||
| 	} | ||||
| 	return c.armClient.PrepareGetRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // listNextResults retrieves the next set of results, if any. | ||||
| func (c *Client) listNextResults(ctx context.Context, lastResults containerservice.ManagedClusterListResult) (result containerservice.ManagedClusterListResult, err error) { | ||||
| 	req, err := c.managedClusterListResultPreparer(ctx, lastResults) | ||||
| 	if err != nil { | ||||
| 		return result, autorest.NewErrorWithError(err, "managedclusterclient", "listNextResults", nil, "Failure preparing next results request") | ||||
| 	} | ||||
| 	if req == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp, rerr := c.armClient.Send(ctx, req) | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		result.Response = autorest.Response{Response: resp} | ||||
| 		return result, autorest.NewErrorWithError(rerr.Error(), "managedclusterclient", "listNextResults", resp, "Failure sending next results request") | ||||
| 	} | ||||
|  | ||||
| 	result, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		err = autorest.NewErrorWithError(err, "managedclusterclient", "listNextResults", resp, "Failure responding to next results request") | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // ManagedClusterResultPage contains a page of ManagedCluster values. | ||||
| type ManagedClusterResultPage struct { | ||||
| 	fn   func(context.Context, containerservice.ManagedClusterListResult) (containerservice.ManagedClusterListResult, error) | ||||
| 	mclr containerservice.ManagedClusterListResult | ||||
| } | ||||
|  | ||||
| // NextWithContext advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| func (page *ManagedClusterResultPage) NextWithContext(ctx context.Context) (err error) { | ||||
| 	next, err := page.fn(ctx, page.mclr) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	page.mclr = next | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Next advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| // Deprecated: Use NextWithContext() instead. | ||||
| func (page *ManagedClusterResultPage) Next() error { | ||||
| 	return page.NextWithContext(context.Background()) | ||||
| } | ||||
|  | ||||
| // NotDone returns true if the page enumeration should be started or is not yet complete. | ||||
| func (page ManagedClusterResultPage) NotDone() bool { | ||||
| 	return !page.mclr.IsEmpty() | ||||
| } | ||||
|  | ||||
| // Response returns the raw server response from the last page request. | ||||
| func (page ManagedClusterResultPage) Response() containerservice.ManagedClusterListResult { | ||||
| 	return page.mclr | ||||
| } | ||||
|  | ||||
| // Values returns the slice of values for the current page or nil if there are no values. | ||||
| func (page ManagedClusterResultPage) Values() []containerservice.ManagedCluster { | ||||
| 	if page.mclr.IsEmpty() { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return *page.mclr.Value | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate creates or updates a ManagedCluster. | ||||
| func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("managed_clusters", "create_or_update", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "CreateOrUpdateManagedCluster") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("CreateOrUpdateManagedCluster", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.createOrUpdateManagedCluster(ctx, resourceGroupName, managedClusterName, parameters, etag) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // createOrUpdateManagedCluster creates or updates a ManagedCluster. | ||||
| func (c *Client) createOrUpdateManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.ContainerService/managedClusters", | ||||
| 		managedClusterName, | ||||
| 	) | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
| 	if etag != "" { | ||||
| 		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag))) | ||||
| 	} | ||||
|  | ||||
| 	response, rerr := c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators) | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedCluster.put.request", resourceID, rerr.Error()) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	if response != nil && response.StatusCode != http.StatusNoContent { | ||||
| 		_, rerr = c.createOrUpdateResponder(response) | ||||
| 		if rerr != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedCluster.put.respond", resourceID, rerr.Error()) | ||||
| 			return rerr | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) createOrUpdateResponder(resp *http.Response) (*containerservice.ManagedCluster, *retry.Error) { | ||||
| 	result := &containerservice.ManagedCluster{} | ||||
| 	err := autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return result, retry.GetError(resp, err) | ||||
| } | ||||
|  | ||||
| // Delete deletes a ManagedCluster by name. | ||||
| func (c *Client) Delete(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("managed_clusters", "delete", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "DeleteManagedCluster") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("DeleteManagedCluster", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.deleteManagedCluster(ctx, resourceGroupName, managedClusterName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // deleteManagedCluster deletes a ManagedCluster by name. | ||||
| func (c *Client) deleteManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.ContainerService/managedClusters", | ||||
| 		managedClusterName, | ||||
| 	) | ||||
|  | ||||
| 	return c.armClient.DeleteResource(ctx, resourceID, "") | ||||
| } | ||||
| @@ -1,634 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package containerserviceclient | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // 2065-01-24 05:20:00 +0000 UTC | ||||
| func getFutureTime() time.Time { | ||||
| 	return time.Unix(3000000000, 0) | ||||
| } | ||||
|  | ||||
| func getTestManagedClusterClient(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{}) | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestManagedClusterClientWithNeverRateLimiter(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter() | ||||
| 	rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter() | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestManagedClusterClientWithRetryAfterReader(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 		RetryAfterReader:  getFutureTime(), | ||||
| 		RetryAfterWriter:  getFutureTime(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestManagedCluster(name string) containerservice.ManagedCluster { | ||||
| 	return containerservice.ManagedCluster{ | ||||
| 		ID:       pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/%s", name)), | ||||
| 		Name:     pointer.String(name), | ||||
| 		Location: pointer.String("eastus"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	config := &azclients.ClientConfig{ | ||||
| 		SubscriptionID:          "sub", | ||||
| 		ResourceManagerEndpoint: "endpoint", | ||||
| 		Location:                "eastus", | ||||
| 		RateLimitConfig: &azclients.RateLimitConfig{ | ||||
| 			CloudProviderRateLimit:            true, | ||||
| 			CloudProviderRateLimitQPS:         0.5, | ||||
| 			CloudProviderRateLimitBucket:      1, | ||||
| 			CloudProviderRateLimitQPSWrite:    0.5, | ||||
| 			CloudProviderRateLimitBucketWrite: 1, | ||||
| 		}, | ||||
| 		Backoff: &retry.Backoff{Steps: 1}, | ||||
| 	} | ||||
|  | ||||
| 	mcClient := New(config) | ||||
| 	assert.Equal(t, "sub", mcClient.subscriptionID) | ||||
| 	assert.NotEmpty(t, mcClient.rateLimiterReader) | ||||
| 	assert.NotEmpty(t, mcClient.rateLimiterWriter) | ||||
| } | ||||
|  | ||||
| func TestGet(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	expected := containerservice.ManagedCluster{} | ||||
| 	expected.Response = autorest.Response{Response: response} | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcGetErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetManagedCluster"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient) | ||||
| 	expected := containerservice.ManagedCluster{} | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Equal(t, mcGetErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcGetErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetManagedCluster", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient) | ||||
| 	expected := containerservice.ManagedCluster{} | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Equal(t, mcGetErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Empty(t, result) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetNotFound(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	expected := containerservice.ManagedCluster{Response: autorest.Response{}} | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestGetInternalError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusInternalServerError, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	expected := containerservice.ManagedCluster{Response: autorest.Response{}} | ||||
| 	result, rerr := mcClient.Get(context.TODO(), "rg", "cluster") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestList(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")} | ||||
| 	responseBody, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(responseBody)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, 3, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListNextResultsMultiPages(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		prepareErr error | ||||
| 		sendErr    *retry.Error | ||||
| 		statusCode int | ||||
| 	}{ | ||||
| 		{ | ||||
| 			prepareErr: nil, | ||||
| 			sendErr:    nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			prepareErr: fmt.Errorf("error"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			sendErr: &retry.Error{RawError: fmt.Errorf("error")}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	lastResult := containerservice.ManagedClusterListResult{ | ||||
| 		NextLink: pointer.String("next"), | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 		req := &http.Request{ | ||||
| 			Method: "GET", | ||||
| 		} | ||||
| 		armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr) | ||||
| 		if test.prepareErr == nil { | ||||
| 			armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{ | ||||
| 				StatusCode: http.StatusOK, | ||||
| 				Body:       ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))), | ||||
| 			}, test.sendErr) | ||||
| 			armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()) | ||||
| 		} | ||||
|  | ||||
| 		mcClient := getTestManagedClusterClient(armClient) | ||||
| 		result, err := mcClient.listNextResults(context.TODO(), lastResult) | ||||
| 		if test.prepareErr != nil || test.sendErr != nil { | ||||
| 			assert.Error(t, err) | ||||
| 		} else { | ||||
| 			assert.NoError(t, err) | ||||
| 		} | ||||
| 		if test.prepareErr != nil { | ||||
| 			assert.Empty(t, result) | ||||
| 		} else { | ||||
| 			assert.NotEmpty(t, result) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestListNextResultsMultiPagesWithListResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	test := struct { | ||||
| 		prepareErr error | ||||
| 		sendErr    *retry.Error | ||||
| 	}{ | ||||
| 		prepareErr: nil, | ||||
| 		sendErr:    nil, | ||||
| 	} | ||||
|  | ||||
| 	lastResult := containerservice.ManagedClusterListResult{ | ||||
| 		NextLink: pointer.String("next"), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	req := &http.Request{ | ||||
| 		Method: "GET", | ||||
| 	} | ||||
| 	armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr) | ||||
| 	if test.prepareErr == nil { | ||||
| 		armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{ | ||||
| 			StatusCode: http.StatusNotFound, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))), | ||||
| 		}, test.sendErr) | ||||
| 		armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()) | ||||
| 	} | ||||
|  | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewBuffer([]byte(`{"foo":"bar"}`))), | ||||
| 	} | ||||
| 	expected := containerservice.ManagedClusterListResult{} | ||||
| 	expected.Response = autorest.Response{Response: response} | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, err := mcClient.listNextResults(context.TODO(), lastResult) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Equal(t, expected, result) | ||||
| } | ||||
|  | ||||
| func TestListWithListResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")} | ||||
| 	responseBody, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusNotFound, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(responseBody)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListWithNextPage(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")} | ||||
| 	// ManagedClusterListResult.MarshalJson() doesn't include "nextLink" in its result, hence responseBody is composed manually below. | ||||
| 	responseBody, err := json.Marshal(map[string]interface{}{"value": mcList, "nextLink": "nextLink"}) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	pagedResponse, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(&http.Request{}, nil) | ||||
| 	armClient.EXPECT().Send(gomock.Any(), gomock.Any()).Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(pagedResponse)), | ||||
| 		}, nil) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(responseBody)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(2) | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, 6, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcListErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "ListManagedCluster"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcListErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestListRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcListErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "ListManagedCluster", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcListErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestListThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	result, rerr := mcClient.List(context.TODO(), "rg") | ||||
| 	assert.Empty(t, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdate(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "*") | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcCreateOrUpdateErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "CreateOrUpdateManagedCluster"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient) | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcCreateOrUpdateErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcCreateOrUpdateErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "CreateOrUpdateManagedCluster", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient) | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcCreateOrUpdateErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDelete(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(mc.ID, ""), "").Return(nil).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	rerr := mcClient.Delete(context.TODO(), "rg", "cluster") | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcDeleteErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "DeleteManagedCluster"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient) | ||||
| 	rerr := mcClient.Delete(context.TODO(), "rg", "cluster") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcDeleteErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	mcDeleteErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "DeleteManagedCluster", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient) | ||||
| 	rerr := mcClient.Delete(context.TODO(), "rg", "cluster") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, mcDeleteErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	mc := getTestManagedCluster("cluster") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(mc.ID, ""), "").Return(throttleErr).Times(1) | ||||
|  | ||||
| 	mcClient := getTestManagedClusterClient(armClient) | ||||
| 	rerr := mcClient.Delete(context.TODO(), "rg", "cluster") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package containerserviceclient implements the client for azure container service. | ||||
| package containerserviceclient // import "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient" | ||||
| @@ -1,42 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| //go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go  -destination=mockcontainerserviceclient/interface.go -package=mockcontainerserviceclient Interface | ||||
| package containerserviceclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// APIVersion is the API version for containerservice. | ||||
| 	APIVersion = "2020-04-01" | ||||
| ) | ||||
|  | ||||
| // Interface is the client interface for ContainerService. | ||||
| type Interface interface { | ||||
| 	CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error | ||||
| 	Delete(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error | ||||
| 	Get(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) | ||||
| 	List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package mockcontainerserviceclient implements the mock client for azure container service. | ||||
| package mockcontainerserviceclient // import "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient/mockcontainerserviceclient" | ||||
| @@ -1,114 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Code generated by MockGen. DO NOT EDIT. | ||||
| // Source: interface.go | ||||
|  | ||||
| // Package mockcontainerserviceclient is a generated GoMock package. | ||||
| package mockcontainerserviceclient | ||||
|  | ||||
| import ( | ||||
| 	context "context" | ||||
| 	reflect "reflect" | ||||
|  | ||||
| 	containerservice "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice" | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| 	retry "k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| // MockInterface is a mock of Interface interface. | ||||
| type MockInterface struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockInterfaceMockRecorder | ||||
| } | ||||
|  | ||||
| // MockInterfaceMockRecorder is the mock recorder for MockInterface. | ||||
| type MockInterfaceMockRecorder struct { | ||||
| 	mock *MockInterface | ||||
| } | ||||
|  | ||||
| // NewMockInterface creates a new mock instance. | ||||
| func NewMockInterface(ctrl *gomock.Controller) *MockInterface { | ||||
| 	mock := &MockInterface{ctrl: ctrl} | ||||
| 	mock.recorder = &MockInterfaceMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate mocks base method. | ||||
| func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, managedClusterName, parameters, etag) | ||||
| 	ret0, _ := ret[0].(*retry.Error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate indicates an expected call of CreateOrUpdate. | ||||
| func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, managedClusterName, parameters, etag interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, managedClusterName, parameters, etag) | ||||
| } | ||||
|  | ||||
| // Delete mocks base method. | ||||
| func (m *MockInterface) Delete(ctx context.Context, resourceGroupName, managedClusterName string) *retry.Error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Delete", ctx, resourceGroupName, managedClusterName) | ||||
| 	ret0, _ := ret[0].(*retry.Error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // Delete indicates an expected call of Delete. | ||||
| func (mr *MockInterfaceMockRecorder) Delete(ctx, resourceGroupName, managedClusterName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), ctx, resourceGroupName, managedClusterName) | ||||
| } | ||||
|  | ||||
| // Get mocks base method. | ||||
| func (m *MockInterface) Get(ctx context.Context, resourceGroupName, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, managedClusterName) | ||||
| 	ret0, _ := ret[0].(containerservice.ManagedCluster) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // Get indicates an expected call of Get. | ||||
| func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, managedClusterName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, managedClusterName) | ||||
| } | ||||
|  | ||||
| // List mocks base method. | ||||
| func (m *MockInterface) List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "List", ctx, resourceGroupName) | ||||
| 	ret0, _ := ret[0].([]containerservice.ManagedCluster) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // List indicates an expected call of List. | ||||
| func (mr *MockInterfaceMockRecorder) List(ctx, resourceGroupName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), ctx, resourceGroupName) | ||||
| } | ||||
| @@ -1,459 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package deploymentclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
|  | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/metrics" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var _ Interface = &Client{} | ||||
|  | ||||
| // Client implements ContainerService client Interface. | ||||
| type Client struct { | ||||
| 	armClient      armclient.Interface | ||||
| 	subscriptionID string | ||||
|  | ||||
| 	// Rate limiting configures. | ||||
| 	rateLimiterReader flowcontrol.RateLimiter | ||||
| 	rateLimiterWriter flowcontrol.RateLimiter | ||||
|  | ||||
| 	// ARM throttling configures. | ||||
| 	RetryAfterReader time.Time | ||||
| 	RetryAfterWriter time.Time | ||||
| } | ||||
|  | ||||
| // New creates a new ContainerServiceClient client with ratelimiting. | ||||
| func New(config *azclients.ClientConfig) *Client { | ||||
| 	baseURI := config.ResourceManagerEndpoint | ||||
| 	authorizer := config.Authorizer | ||||
| 	armClient := armclient.New(authorizer, baseURI, config.UserAgent, APIVersion, config.Location, config.Backoff) | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig) | ||||
|  | ||||
| 	klog.V(2).Infof("Azure DeploymentClient (read ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPS, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucket) | ||||
| 	klog.V(2).Infof("Azure DeploymentClient (write ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPSWrite, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucketWrite) | ||||
|  | ||||
| 	client := &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 		subscriptionID:    config.SubscriptionID, | ||||
| 	} | ||||
|  | ||||
| 	return client | ||||
| } | ||||
|  | ||||
| // Get gets a deployment | ||||
| func (c *Client) Get(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("deployments", "get", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterReader.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return resources.DeploymentExtended{}, retry.GetRateLimitError(false, "GetDeployment") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterReader.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("GetDeployment", "client throttled", c.RetryAfterReader) | ||||
| 		return resources.DeploymentExtended{}, rerr | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.getDeployment(ctx, resourceGroupName, deploymentName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterReader = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // getDeployment gets a deployment. | ||||
| func (c *Client) getDeployment(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error) { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Resources/deployments", | ||||
| 		deploymentName, | ||||
| 	) | ||||
| 	result := resources.DeploymentExtended{} | ||||
|  | ||||
| 	response, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.get.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	err := autorest.Respond( | ||||
| 		response, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.get.respond", resourceID, err) | ||||
| 		return result, retry.GetError(response, err) | ||||
| 	} | ||||
|  | ||||
| 	result.Response = autorest.Response{Response: response} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // List gets a list of deployments in the resource group. | ||||
| func (c *Client) List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("deployments", "list", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterReader.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return nil, retry.GetRateLimitError(false, "ListDeployment") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterReader.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("ListDeployment", "client throttled", c.RetryAfterReader) | ||||
| 		return nil, rerr | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.listDeployment(ctx, resourceGroupName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterReader = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // listDeployment gets a list of deployments in the resource group. | ||||
| func (c *Client) listDeployment(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) { | ||||
| 	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments", | ||||
| 		autorest.Encode("path", c.subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName)) | ||||
| 	result := make([]resources.DeploymentExtended, 0) | ||||
| 	page := &DeploymentResultPage{} | ||||
| 	page.fn = c.listNextResults | ||||
|  | ||||
| 	resp, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	page.dplr, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.respond", resourceID, err) | ||||
| 		return result, retry.GetError(resp, err) | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		result = append(result, page.Values()...) | ||||
|  | ||||
| 		// Abort the loop when there's no nextLink in the response. | ||||
| 		if pointer.StringDeref(page.Response().NextLink, "") == "" { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if err = page.NextWithContext(ctx); err != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.next", resourceID, err) | ||||
| 			return result, retry.GetError(page.Response().Response.Response, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (c *Client) listResponder(resp *http.Response) (result resources.DeploymentListResult, err error) { | ||||
| 	err = autorest.Respond( | ||||
| 		resp, | ||||
| 		autorest.ByIgnoring(), | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // deploymentListResultPreparer prepares a request to retrieve the next set of results. | ||||
| // It returns nil if no more results exist. | ||||
| func (c *Client) deploymentListResultPreparer(ctx context.Context, dplr resources.DeploymentListResult) (*http.Request, error) { | ||||
| 	if dplr.NextLink == nil || len(pointer.StringDeref(dplr.NextLink, "")) < 1 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithBaseURL(pointer.StringDeref(dplr.NextLink, "")), | ||||
| 	} | ||||
| 	return c.armClient.PrepareGetRequest(ctx, decorators...) | ||||
| } | ||||
|  | ||||
| // listNextResults retrieves the next set of results, if any. | ||||
| func (c *Client) listNextResults(ctx context.Context, lastResults resources.DeploymentListResult) (result resources.DeploymentListResult, err error) { | ||||
| 	req, err := c.deploymentListResultPreparer(ctx, lastResults) | ||||
| 	if err != nil { | ||||
| 		return result, autorest.NewErrorWithError(err, "deploymentclient", "listNextResults", nil, "Failure preparing next results request") | ||||
| 	} | ||||
| 	if req == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp, rerr := c.armClient.Send(ctx, req) | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		result.Response = autorest.Response{Response: resp} | ||||
| 		return result, autorest.NewErrorWithError(rerr.Error(), "deploymentclient", "listNextResults", resp, "Failure sending next results request") | ||||
| 	} | ||||
|  | ||||
| 	result, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		err = autorest.NewErrorWithError(err, "deploymentclient", "listNextResults", resp, "Failure responding to next results request") | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // DeploymentResultPage contains a page of deployments values. | ||||
| type DeploymentResultPage struct { | ||||
| 	fn   func(context.Context, resources.DeploymentListResult) (resources.DeploymentListResult, error) | ||||
| 	dplr resources.DeploymentListResult | ||||
| } | ||||
|  | ||||
| // NextWithContext advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| func (page *DeploymentResultPage) NextWithContext(ctx context.Context) (err error) { | ||||
| 	next, err := page.fn(ctx, page.dplr) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	page.dplr = next | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Next advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| // Deprecated: Use NextWithContext() instead. | ||||
| func (page *DeploymentResultPage) Next() error { | ||||
| 	return page.NextWithContext(context.Background()) | ||||
| } | ||||
|  | ||||
| // NotDone returns true if the page enumeration should be started or is not yet complete. | ||||
| func (page DeploymentResultPage) NotDone() bool { | ||||
| 	return !page.dplr.IsEmpty() | ||||
| } | ||||
|  | ||||
| // Response returns the raw server response from the last page request. | ||||
| func (page DeploymentResultPage) Response() resources.DeploymentListResult { | ||||
| 	return page.dplr | ||||
| } | ||||
|  | ||||
| // Values returns the slice of values for the current page or nil if there are no values. | ||||
| func (page DeploymentResultPage) Values() []resources.DeploymentExtended { | ||||
| 	if page.dplr.IsEmpty() { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return *page.dplr.Value | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate creates or updates a deployment. | ||||
| func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, deploymentName string, parameters resources.Deployment, etag string) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("deployments", "create_or_update", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "CreateOrUpdateDeployment") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("CreateOrUpdateDeployment", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.createOrUpdateDeployment(ctx, resourceGroupName, deploymentName, parameters, etag) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) createOrUpdateDeployment(ctx context.Context, resourceGroupName string, deploymentName string, parameters resources.Deployment, etag string) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Resources/deployments", | ||||
| 		deploymentName, | ||||
| 	) | ||||
| 	decorators := []autorest.PrepareDecorator{ | ||||
| 		autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}), | ||||
| 		autorest.WithJSON(parameters), | ||||
| 	} | ||||
| 	if etag != "" { | ||||
| 		decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag))) | ||||
| 	} | ||||
|  | ||||
| 	response, rerr := c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators) | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.put.request", resourceID, rerr.Error()) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	if response != nil && response.StatusCode != http.StatusNoContent { | ||||
| 		_, rerr = c.createOrUpdateResponder(response) | ||||
| 		if rerr != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.put.respond", resourceID, rerr.Error()) | ||||
| 			return rerr | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) createOrUpdateResponder(resp *http.Response) (*resources.DeploymentExtended, *retry.Error) { | ||||
| 	result := &resources.DeploymentExtended{} | ||||
| 	err := autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return result, retry.GetError(resp, err) | ||||
| } | ||||
|  | ||||
| // Delete deletes a deployment by name. | ||||
| func (c *Client) Delete(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("deployments", "delete", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "DeleteDeployment") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("DeleteDeployment", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.deleteDeployment(ctx, resourceGroupName, deploymentName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // deleteDeployment deletes a deployment by name. | ||||
| func (c *Client) deleteDeployment(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Resources/deployments", | ||||
| 		deploymentName, | ||||
| 	) | ||||
|  | ||||
| 	return c.armClient.DeleteResource(ctx, resourceID, "") | ||||
| } | ||||
|  | ||||
| // ExportTemplate exports the template used for specified deployment | ||||
| func (c *Client) ExportTemplate(ctx context.Context, resourceGroupName string, deploymentName string) (result resources.DeploymentExportResult, rerr *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("deployments", "export_template", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return resources.DeploymentExportResult{}, retry.GetRateLimitError(true, "ExportTemplateDeployment") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("CreateOrUpdateDeployment", "client throttled", c.RetryAfterWriter) | ||||
| 		return resources.DeploymentExportResult{}, rerr | ||||
| 	} | ||||
|  | ||||
| 	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments/%s/exportTemplate", | ||||
| 		autorest.Encode("path", c.subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName), | ||||
| 		autorest.Encode("path", deploymentName)) | ||||
| 	response, rerr := c.armClient.PostResource(ctx, resourceID, "exportTemplate", struct{}{}) | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.exportTemplate.request", resourceID, rerr.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := autorest.Respond( | ||||
| 		response, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.exportTemplate.respond", resourceID, err) | ||||
| 		return result, retry.GetError(response, err) | ||||
| 	} | ||||
|  | ||||
| 	result.Response = autorest.Response{Response: response} | ||||
| 	return | ||||
| } | ||||
| @@ -1,635 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package deploymentclient | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| // 2065-01-24 05:20:00 +0000 UTC | ||||
| func getFutureTime() time.Time { | ||||
| 	return time.Unix(3000000000, 0) | ||||
| } | ||||
|  | ||||
| func getTestDeploymentClient(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{}) | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestDeploymentClientWithNeverRateLimiter(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter() | ||||
| 	rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter() | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestDeploymentClientWithRetryAfterReader(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter() | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 		RetryAfterReader:  getFutureTime(), | ||||
| 		RetryAfterWriter:  getFutureTime(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestDeploymentExtended(name string) resources.DeploymentExtended { | ||||
| 	return resources.DeploymentExtended{ | ||||
| 		ID:   pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/%s", name)), | ||||
| 		Name: pointer.String(name), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	config := &azclients.ClientConfig{ | ||||
| 		SubscriptionID:          "sub", | ||||
| 		ResourceManagerEndpoint: "endpoint", | ||||
| 		Location:                "eastus", | ||||
| 		RateLimitConfig: &azclients.RateLimitConfig{ | ||||
| 			CloudProviderRateLimit:            true, | ||||
| 			CloudProviderRateLimitQPS:         0.5, | ||||
| 			CloudProviderRateLimitBucket:      1, | ||||
| 			CloudProviderRateLimitQPSWrite:    0.5, | ||||
| 			CloudProviderRateLimitBucketWrite: 1, | ||||
| 		}, | ||||
| 		Backoff: &retry.Backoff{Steps: 1}, | ||||
| 	} | ||||
| 	dpClient := New(config) | ||||
| 	assert.Equal(t, "sub", dpClient.subscriptionID) | ||||
| 	assert.NotEmpty(t, dpClient.rateLimiterReader) | ||||
| 	assert.NotEmpty(t, dpClient.rateLimiterWriter) | ||||
| } | ||||
|  | ||||
| func TestGet(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	expected := resources.DeploymentExtended{} | ||||
| 	expected.Response = autorest.Response{Response: response} | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpGetErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetDeployment"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient) | ||||
| 	expected := resources.DeploymentExtended{} | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Equal(t, dpGetErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpGetErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetDeployment", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClientWithRetryAfterReader(armClient) | ||||
| 	expected := resources.DeploymentExtended{} | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.Equal(t, dpGetErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Empty(t, result) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestGetNotFound(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	expected := resources.DeploymentExtended{Response: autorest.Response{}} | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestGetInternalError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusInternalServerError, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	expected := resources.DeploymentExtended{Response: autorest.Response{}} | ||||
| 	result, rerr := dpClient.Get(context.TODO(), "rg", "dep") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestList(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")} | ||||
| 	responseBody, err := json.Marshal(resources.DeploymentListResult{Value: &dpList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(responseBody)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, 3, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListNextResultsMultiPages(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		prepareErr error | ||||
| 		sendErr    *retry.Error | ||||
| 		statusCode int | ||||
| 	}{ | ||||
| 		{ | ||||
| 			prepareErr: nil, | ||||
| 			sendErr:    nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			prepareErr: fmt.Errorf("error"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			sendErr: &retry.Error{RawError: fmt.Errorf("error")}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	lastResult := resources.DeploymentListResult{ | ||||
| 		NextLink: pointer.String("next"), | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 		req := &http.Request{ | ||||
| 			Method: "GET", | ||||
| 		} | ||||
| 		armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr) | ||||
| 		if test.prepareErr == nil { | ||||
| 			armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{ | ||||
| 				StatusCode: http.StatusOK, | ||||
| 				Body:       ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))), | ||||
| 			}, test.sendErr) | ||||
| 			armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()) | ||||
| 		} | ||||
|  | ||||
| 		dpClient := getTestDeploymentClient(armClient) | ||||
| 		result, err := dpClient.listNextResults(context.TODO(), lastResult) | ||||
| 		if test.prepareErr != nil || test.sendErr != nil { | ||||
| 			assert.Error(t, err) | ||||
| 		} else { | ||||
| 			assert.NoError(t, err) | ||||
| 		} | ||||
| 		if test.prepareErr != nil { | ||||
| 			assert.Empty(t, result) | ||||
| 		} else { | ||||
| 			assert.NotEmpty(t, result) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestListNextResultsMultiPagesWithListResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	test := struct { | ||||
| 		prepareErr error | ||||
| 		sendErr    *retry.Error | ||||
| 	}{ | ||||
| 		prepareErr: nil, | ||||
| 		sendErr:    nil, | ||||
| 	} | ||||
|  | ||||
| 	lastResult := resources.DeploymentListResult{ | ||||
| 		NextLink: pointer.String("next"), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	req := &http.Request{ | ||||
| 		Method: "GET", | ||||
| 	} | ||||
| 	armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr) | ||||
| 	if test.prepareErr == nil { | ||||
| 		armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{ | ||||
| 			StatusCode: http.StatusNotFound, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))), | ||||
| 		}, test.sendErr) | ||||
| 		armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()) | ||||
| 	} | ||||
|  | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewBuffer([]byte(`{"foo":"bar"}`))), | ||||
| 	} | ||||
| 	expected := resources.DeploymentListResult{} | ||||
| 	expected.Response = autorest.Response{Response: response} | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, err := dpClient.listNextResults(context.TODO(), lastResult) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Equal(t, expected, result) | ||||
| } | ||||
|  | ||||
| func TestListWithListResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")} | ||||
| 	responseBody, err := json.Marshal(resources.DeploymentListResult{Value: &dpList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusNotFound, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(responseBody)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListWithNextPage(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments" | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")} | ||||
| 	// DeploymentListResult.MarshalJson() doesn't include "nextLink" in its result, hence partialResponse is composed manually below. | ||||
| 	partialResponse, err := json.Marshal(map[string]interface{}{"value": dpList, "nextLink": "nextLink"}) | ||||
| 	assert.NoError(t, err) | ||||
| 	pagedResponse, err := json.Marshal(resources.DeploymentListResult{Value: &dpList}) | ||||
| 	assert.NoError(t, err) | ||||
| 	armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(&http.Request{}, nil) | ||||
| 	armClient.EXPECT().Send(gomock.Any(), gomock.Any()).Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(pagedResponse)), | ||||
| 		}, nil) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return( | ||||
| 		&http.Response{ | ||||
| 			StatusCode: http.StatusOK, | ||||
| 			Body:       ioutil.NopCloser(bytes.NewReader(partialResponse)), | ||||
| 		}, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(2) | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.Nil(t, rerr) | ||||
| 	assert.Equal(t, 6, len(result)) | ||||
| } | ||||
|  | ||||
| func TestListNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpListErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "ListDeployment"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpListErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestListRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpListErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "ListDeployment", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	dpClient := getTestDeploymentClientWithRetryAfterReader(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.Equal(t, 0, len(result)) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpListErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestListThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	result, rerr := dpClient.List(context.TODO(), "rg") | ||||
| 	assert.Empty(t, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdate(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dp := resources.Deployment{} | ||||
| 	dpExtended := getTestDeploymentExtended("dep") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "*") | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dp := resources.Deployment{} | ||||
| 	dpExtended := getTestDeploymentExtended("dep") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpCreateOrUpdateErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "CreateOrUpdateDeployment"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient) | ||||
| 	dp := resources.Deployment{} | ||||
| 	rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpCreateOrUpdateErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpCreateOrUpdateErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "CreateOrUpdateDeployment", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	dp := resources.Deployment{} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	mcClient := getTestDeploymentClientWithRetryAfterReader(armClient) | ||||
| 	rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpCreateOrUpdateErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdateThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	dp := resources.Deployment{} | ||||
| 	dpExtended := getTestDeploymentExtended("dep") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDelete(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dp := getTestDeploymentExtended("dep") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(dp.ID, ""), "").Return(nil).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	rerr := dpClient.Delete(context.TODO(), "rg", "dep") | ||||
| 	assert.Nil(t, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteNeverRateLimiter(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpDeleteErr := &retry.Error{ | ||||
| 		RawError:  fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "DeleteDeployment"), | ||||
| 		Retriable: true, | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient) | ||||
| 	rerr := dpClient.Delete(context.TODO(), "rg", "dep") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpDeleteErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteRetryAfterReader(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	dpDeleteErr := &retry.Error{ | ||||
| 		RawError:   fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "DeleteDeployment", "client throttled"), | ||||
| 		Retriable:  true, | ||||
| 		RetryAfter: getFutureTime(), | ||||
| 	} | ||||
|  | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClientWithRetryAfterReader(armClient) | ||||
| 	rerr := dpClient.Delete(context.TODO(), "rg", "dep") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, dpDeleteErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestDeleteThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	dp := getTestDeploymentExtended("dep") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(dp.ID, ""), "").Return(throttleErr).Times(1) | ||||
|  | ||||
| 	dpClient := getTestDeploymentClient(armClient) | ||||
| 	rerr := dpClient.Delete(context.TODO(), "rg", "dep") | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package deploymentclient implements the client for azure deployments. | ||||
| package deploymentclient // import "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient" | ||||
| @@ -1,42 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| //go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go  -destination=mockdeploymentclient/interface.go -package=mockdeploymentclient Interface | ||||
| package deploymentclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// APIVersion is the API version for resources. | ||||
| 	APIVersion = "2017-05-10" | ||||
| ) | ||||
|  | ||||
| // Interface is the client interface for Deployments. | ||||
| type Interface interface { | ||||
| 	Get(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error) | ||||
| 	List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) | ||||
| 	ExportTemplate(ctx context.Context, resourceGroupName string, deploymentName string) (result resources.DeploymentExportResult, rerr *retry.Error) | ||||
| 	CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters resources.Deployment, etag string) *retry.Error | ||||
| 	Delete(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package mockdeploymentclient implements the mock client for azure deployments. | ||||
| package mockdeploymentclient // import "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient/mockdeploymentclient" | ||||
| @@ -1,129 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Code generated by MockGen. DO NOT EDIT. | ||||
| // Source: interface.go | ||||
|  | ||||
| // Package mockdeploymentclient is a generated GoMock package. | ||||
| package mockdeploymentclient | ||||
|  | ||||
| import ( | ||||
| 	context "context" | ||||
| 	reflect "reflect" | ||||
|  | ||||
| 	resources "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| 	retry "k8s.io/legacy-cloud-providers/azure/retry" | ||||
| ) | ||||
|  | ||||
| // MockInterface is a mock of Interface interface. | ||||
| type MockInterface struct { | ||||
| 	ctrl     *gomock.Controller | ||||
| 	recorder *MockInterfaceMockRecorder | ||||
| } | ||||
|  | ||||
| // MockInterfaceMockRecorder is the mock recorder for MockInterface. | ||||
| type MockInterfaceMockRecorder struct { | ||||
| 	mock *MockInterface | ||||
| } | ||||
|  | ||||
| // NewMockInterface creates a new mock instance. | ||||
| func NewMockInterface(ctrl *gomock.Controller) *MockInterface { | ||||
| 	mock := &MockInterface{ctrl: ctrl} | ||||
| 	mock.recorder = &MockInterfaceMockRecorder{mock} | ||||
| 	return mock | ||||
| } | ||||
|  | ||||
| // EXPECT returns an object that allows the caller to indicate expected use. | ||||
| func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate mocks base method. | ||||
| func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, managedClusterName string, parameters resources.Deployment, etag string) *retry.Error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, managedClusterName, parameters, etag) | ||||
| 	ret0, _ := ret[0].(*retry.Error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate indicates an expected call of CreateOrUpdate. | ||||
| func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, managedClusterName, parameters, etag interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, managedClusterName, parameters, etag) | ||||
| } | ||||
|  | ||||
| // Delete mocks base method. | ||||
| func (m *MockInterface) Delete(ctx context.Context, resourceGroupName, deploymentName string) *retry.Error { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Delete", ctx, resourceGroupName, deploymentName) | ||||
| 	ret0, _ := ret[0].(*retry.Error) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // Delete indicates an expected call of Delete. | ||||
| func (mr *MockInterfaceMockRecorder) Delete(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), ctx, resourceGroupName, deploymentName) | ||||
| } | ||||
|  | ||||
| // ExportTemplate mocks base method. | ||||
| func (m *MockInterface) ExportTemplate(ctx context.Context, resourceGroupName, deploymentName string) (resources.DeploymentExportResult, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "ExportTemplate", ctx, resourceGroupName, deploymentName) | ||||
| 	ret0, _ := ret[0].(resources.DeploymentExportResult) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // ExportTemplate indicates an expected call of ExportTemplate. | ||||
| func (mr *MockInterfaceMockRecorder) ExportTemplate(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportTemplate", reflect.TypeOf((*MockInterface)(nil).ExportTemplate), ctx, resourceGroupName, deploymentName) | ||||
| } | ||||
|  | ||||
| // Get mocks base method. | ||||
| func (m *MockInterface) Get(ctx context.Context, resourceGroupName, deploymentName string) (resources.DeploymentExtended, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, deploymentName) | ||||
| 	ret0, _ := ret[0].(resources.DeploymentExtended) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // Get indicates an expected call of Get. | ||||
| func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, deploymentName) | ||||
| } | ||||
|  | ||||
| // List mocks base method. | ||||
| func (m *MockInterface) List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "List", ctx, resourceGroupName) | ||||
| 	ret0, _ := ret[0].([]resources.DeploymentExtended) | ||||
| 	ret1, _ := ret[1].(*retry.Error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // List indicates an expected call of List. | ||||
| func (mr *MockInterfaceMockRecorder) List(ctx, resourceGroupName interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), ctx, resourceGroupName) | ||||
| } | ||||
| @@ -1,453 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package diskclient | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/Azure/go-autorest/autorest/azure" | ||||
|  | ||||
| 	"k8s.io/client-go/util/flowcontrol" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/metrics" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| var _ Interface = &Client{} | ||||
|  | ||||
| // Client implements Disk client Interface. | ||||
| type Client struct { | ||||
| 	armClient      armclient.Interface | ||||
| 	subscriptionID string | ||||
|  | ||||
| 	// Rate limiting configures. | ||||
| 	rateLimiterReader flowcontrol.RateLimiter | ||||
| 	rateLimiterWriter flowcontrol.RateLimiter | ||||
|  | ||||
| 	// ARM throttling configures. | ||||
| 	RetryAfterReader time.Time | ||||
| 	RetryAfterWriter time.Time | ||||
| } | ||||
|  | ||||
| // New creates a new Disk client with ratelimiting. | ||||
| func New(config *azclients.ClientConfig) *Client { | ||||
| 	baseURI := config.ResourceManagerEndpoint | ||||
| 	authorizer := config.Authorizer | ||||
| 	apiVersion := APIVersion | ||||
| 	if strings.EqualFold(config.CloudName, AzureStackCloudName) { | ||||
| 		apiVersion = AzureStackCloudAPIVersion | ||||
| 	} | ||||
| 	armClient := armclient.New(authorizer, baseURI, config.UserAgent, apiVersion, config.Location, config.Backoff) | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig) | ||||
|  | ||||
| 	klog.V(2).Infof("Azure DisksClient (read ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPS, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucket) | ||||
| 	klog.V(2).Infof("Azure DisksClient (write ops) using rate limit config: QPS=%g, bucket=%d", | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitQPSWrite, | ||||
| 		config.RateLimitConfig.CloudProviderRateLimitBucketWrite) | ||||
|  | ||||
| 	client := &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 		subscriptionID:    config.SubscriptionID, | ||||
| 	} | ||||
|  | ||||
| 	return client | ||||
| } | ||||
|  | ||||
| // Get gets a Disk. | ||||
| func (c *Client) Get(ctx context.Context, resourceGroupName string, diskName string) (compute.Disk, *retry.Error) { | ||||
| 	mc := metrics.NewMetricContext("disks", "get", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterReader.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return compute.Disk{}, retry.GetRateLimitError(false, "GetDisk") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterReader.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("GetDisk", "client throttled", c.RetryAfterReader) | ||||
| 		return compute.Disk{}, rerr | ||||
| 	} | ||||
|  | ||||
| 	result, rerr := c.getDisk(ctx, resourceGroupName, diskName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterReader = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // getDisk gets a Disk. | ||||
| func (c *Client) getDisk(ctx context.Context, resourceGroupName string, diskName string) (compute.Disk, *retry.Error) { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Compute/disks", | ||||
| 		diskName, | ||||
| 	) | ||||
| 	result := compute.Disk{} | ||||
|  | ||||
| 	response, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.get.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	err := autorest.Respond( | ||||
| 		response, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.get.respond", resourceID, err) | ||||
| 		return result, retry.GetError(response, err) | ||||
| 	} | ||||
|  | ||||
| 	result.Response = autorest.Response{Response: response} | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // CreateOrUpdate creates or updates a Disk. | ||||
| func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.Disk) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("disks", "create_or_update", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "DiskCreateOrUpdate") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("DiskCreateOrUpdate", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.createOrUpdateDisk(ctx, resourceGroupName, diskName, diskParameter) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // createOrUpdateDisk creates or updates a Disk. | ||||
| func (c *Client) createOrUpdateDisk(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.Disk) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Compute/disks", | ||||
| 		diskName, | ||||
| 	) | ||||
|  | ||||
| 	response, rerr := c.armClient.PutResource(ctx, resourceID, diskParameter) | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.request", resourceID, rerr.Error()) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	if response != nil && response.StatusCode != http.StatusNoContent { | ||||
| 		_, rerr = c.createOrUpdateResponder(response) | ||||
| 		if rerr != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.respond", resourceID, rerr.Error()) | ||||
| 			return rerr | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) createOrUpdateResponder(resp *http.Response) (*compute.Disk, *retry.Error) { | ||||
| 	result := &compute.Disk{} | ||||
| 	err := autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return result, retry.GetError(resp, err) | ||||
| } | ||||
|  | ||||
| // Update creates or updates a Disk. | ||||
| func (c *Client) Update(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.DiskUpdate) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("disks", "update", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "DiskUpdate") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("DiskUpdate", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.updateDisk(ctx, resourceGroupName, diskName, diskParameter) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // updateDisk updates a Disk. | ||||
| func (c *Client) updateDisk(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.DiskUpdate) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Compute/disks", | ||||
| 		diskName, | ||||
| 	) | ||||
|  | ||||
| 	response, rerr := c.armClient.PatchResource(ctx, resourceID, diskParameter) | ||||
| 	defer c.armClient.CloseResponse(ctx, response) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.request", resourceID, rerr.Error()) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	if response != nil && response.StatusCode != http.StatusNoContent { | ||||
| 		_, rerr = c.updateResponder(response) | ||||
| 		if rerr != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.respond", resourceID, rerr.Error()) | ||||
| 			return rerr | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *Client) updateResponder(resp *http.Response) (*compute.Disk, *retry.Error) { | ||||
| 	result := &compute.Disk{} | ||||
| 	err := autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated), | ||||
| 		autorest.ByUnmarshallingJSON(&result)) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return result, retry.GetError(resp, err) | ||||
| } | ||||
|  | ||||
| // Delete deletes a Disk by name. | ||||
| func (c *Client) Delete(ctx context.Context, resourceGroupName string, diskName string) *retry.Error { | ||||
| 	mc := metrics.NewMetricContext("disks", "delete", resourceGroupName, c.subscriptionID, "") | ||||
|  | ||||
| 	// Report errors if the client is rate limited. | ||||
| 	if !c.rateLimiterWriter.TryAccept() { | ||||
| 		mc.RateLimitedCount() | ||||
| 		return retry.GetRateLimitError(true, "DiskDelete") | ||||
| 	} | ||||
|  | ||||
| 	// Report errors if the client is throttled. | ||||
| 	if c.RetryAfterWriter.After(time.Now()) { | ||||
| 		mc.ThrottledCount() | ||||
| 		rerr := retry.GetThrottlingError("DiskDelete", "client throttled", c.RetryAfterWriter) | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	rerr := c.deleteDisk(ctx, resourceGroupName, diskName) | ||||
| 	mc.Observe(rerr.Error()) | ||||
| 	if rerr != nil { | ||||
| 		if rerr.IsThrottled() { | ||||
| 			// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires. | ||||
| 			c.RetryAfterWriter = rerr.RetryAfter | ||||
| 		} | ||||
|  | ||||
| 		return rerr | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // deleteDisk deletes a PublicIPAddress by name. | ||||
| func (c *Client) deleteDisk(ctx context.Context, resourceGroupName string, diskName string) *retry.Error { | ||||
| 	resourceID := armclient.GetResourceID( | ||||
| 		c.subscriptionID, | ||||
| 		resourceGroupName, | ||||
| 		"Microsoft.Compute/disks", | ||||
| 		diskName, | ||||
| 	) | ||||
|  | ||||
| 	return c.armClient.DeleteResource(ctx, resourceID, "") | ||||
| } | ||||
|  | ||||
| // ListByResourceGroup lists all the disks under a resource group. | ||||
| func (c *Client) ListByResourceGroup(ctx context.Context, resourceGroupName string) ([]compute.Disk, *retry.Error) { | ||||
| 	resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks", | ||||
| 		autorest.Encode("path", c.subscriptionID), | ||||
| 		autorest.Encode("path", resourceGroupName)) | ||||
|  | ||||
| 	result := make([]compute.Disk, 0) | ||||
| 	page := &DiskListPage{} | ||||
| 	page.fn = c.listNextResults | ||||
|  | ||||
| 	resp, rerr := c.armClient.GetResource(ctx, resourceID, "") | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.request", resourceID, rerr.Error()) | ||||
| 		return result, rerr | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	page.dl, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.respond", resourceID, err) | ||||
| 		return result, retry.GetError(resp, err) | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		result = append(result, page.Values()...) | ||||
|  | ||||
| 		// Abort the loop when there's no nextLink in the response. | ||||
| 		if pointer.StringDeref(page.Response().NextLink, "") == "" { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if err = page.NextWithContext(ctx); err != nil { | ||||
| 			klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.next", resourceID, err) | ||||
| 			return result, retry.GetError(page.Response().Response.Response, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // listNextResults retrieves the next set of results, if any. | ||||
| func (c *Client) listNextResults(ctx context.Context, lastResults compute.DiskList) (result compute.DiskList, err error) { | ||||
| 	req, err := c.diskListPreparer(ctx, lastResults) | ||||
| 	if err != nil { | ||||
| 		return result, autorest.NewErrorWithError(err, "diskclient", "listNextResults", nil, "Failure preparing next results request") | ||||
| 	} | ||||
| 	if req == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp, rerr := c.armClient.Send(ctx, req) | ||||
| 	defer c.armClient.CloseResponse(ctx, resp) | ||||
| 	if rerr != nil { | ||||
| 		result.Response = autorest.Response{Response: resp} | ||||
| 		return result, autorest.NewErrorWithError(rerr.Error(), "diskclient", "listNextResults", resp, "Failure sending next results request") | ||||
| 	} | ||||
|  | ||||
| 	result, err = c.listResponder(resp) | ||||
| 	if err != nil { | ||||
| 		err = autorest.NewErrorWithError(err, "diskclient", "listNextResults", resp, "Failure responding to next results request") | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // listResponder handles the response to the List request. The method always | ||||
| // closes the http.Response Body. | ||||
| func (c *Client) listResponder(resp *http.Response) (result compute.DiskList, err error) { | ||||
| 	err = autorest.Respond( | ||||
| 		resp, | ||||
| 		azure.WithErrorUnlessStatusCode(http.StatusOK), | ||||
| 		autorest.ByUnmarshallingJSON(&result), | ||||
| 		autorest.ByClosing()) | ||||
| 	result.Response = autorest.Response{Response: resp} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (c *Client) diskListPreparer(ctx context.Context, lr compute.DiskList) (*http.Request, error) { | ||||
| 	if lr.NextLink == nil || len(pointer.StringDeref(lr.NextLink, "")) < 1 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return autorest.Prepare((&http.Request{}).WithContext(ctx), | ||||
| 		autorest.AsJSON(), | ||||
| 		autorest.AsGet(), | ||||
| 		autorest.WithBaseURL(pointer.StringDeref(lr.NextLink, ""))) | ||||
| } | ||||
|  | ||||
| // DiskListPage contains a page of Disk values. | ||||
| type DiskListPage struct { | ||||
| 	fn func(context.Context, compute.DiskList) (compute.DiskList, error) | ||||
| 	dl compute.DiskList | ||||
| } | ||||
|  | ||||
| // NextWithContext advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| func (page *DiskListPage) NextWithContext(ctx context.Context) (err error) { | ||||
| 	next, err := page.fn(ctx, page.dl) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	page.dl = next | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Next advances to the next page of values.  If there was an error making | ||||
| // the request the page does not advance and the error is returned. | ||||
| // Deprecated: Use NextWithContext() instead. | ||||
| func (page *DiskListPage) Next() error { | ||||
| 	return page.NextWithContext(context.Background()) | ||||
| } | ||||
|  | ||||
| // NotDone returns true if the page enumeration should be started or is not yet complete. | ||||
| func (page DiskListPage) NotDone() bool { | ||||
| 	return !page.dl.IsEmpty() | ||||
| } | ||||
|  | ||||
| // Response returns the raw server response from the last page request. | ||||
| func (page DiskListPage) Response() compute.DiskList { | ||||
| 	return page.dl | ||||
| } | ||||
|  | ||||
| // Values returns the slice of values for the current page or nil if there are no values. | ||||
| func (page DiskListPage) Values() []compute.Disk { | ||||
| 	if page.dl.IsEmpty() { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return *page.dl.Value | ||||
| } | ||||
| @@ -1,247 +0,0 @@ | ||||
| //go:build !providerless | ||||
| // +build !providerless | ||||
|  | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package diskclient | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute" | ||||
| 	"github.com/Azure/go-autorest/autorest" | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	azclients "k8s.io/legacy-cloud-providers/azure/clients" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient" | ||||
| 	"k8s.io/legacy-cloud-providers/azure/retry" | ||||
| 	"k8s.io/utils/pointer" | ||||
| ) | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| 	config := &azclients.ClientConfig{ | ||||
| 		SubscriptionID:          "sub", | ||||
| 		ResourceManagerEndpoint: "endpoint", | ||||
| 		Location:                "eastus", | ||||
| 		RateLimitConfig: &azclients.RateLimitConfig{ | ||||
| 			CloudProviderRateLimit:            true, | ||||
| 			CloudProviderRateLimitQPS:         0.5, | ||||
| 			CloudProviderRateLimitBucket:      1, | ||||
| 			CloudProviderRateLimitQPSWrite:    0.5, | ||||
| 			CloudProviderRateLimitBucketWrite: 1, | ||||
| 		}, | ||||
| 		Backoff: &retry.Backoff{Steps: 1}, | ||||
| 	} | ||||
|  | ||||
| 	diskClient := New(config) | ||||
| 	assert.Equal(t, "sub", diskClient.subscriptionID) | ||||
| 	assert.NotEmpty(t, diskClient.rateLimiterReader) | ||||
| 	assert.NotEmpty(t, diskClient.rateLimiterWriter) | ||||
| } | ||||
|  | ||||
| func TestGetNotFound(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusNotFound, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	expected := compute.Disk{Response: autorest.Response{}} | ||||
| 	result, rerr := diskClient.Get(context.TODO(), "rg", "disk1") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestGetInternalError(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusInternalServerError, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	expected := compute.Disk{Response: autorest.Response{}} | ||||
| 	result, rerr := diskClient.Get(context.TODO(), "rg", "disk1") | ||||
| 	assert.Equal(t, expected, result) | ||||
| 	assert.NotNil(t, rerr) | ||||
| 	assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode) | ||||
| } | ||||
|  | ||||
| func TestGetThrottle(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1" | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	result, rerr := diskClient.Get(context.TODO(), "rg", "disk1") | ||||
| 	assert.Empty(t, result) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestCreateOrUpdate(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	disk := getTestDisk("disk1") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PutResource(gomock.Any(), pointer.StringDeref(disk.ID, ""), disk).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	rerr := diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk) | ||||
| 	assert.Nil(t, rerr) | ||||
|  | ||||
| 	response = &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	armClient.EXPECT().PutResource(gomock.Any(), pointer.StringDeref(disk.ID, ""), disk).Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
| 	rerr = diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func TestUpdate(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1" | ||||
| 	diskUpdate := getTestDiskUpdate() | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	response := &http.Response{ | ||||
| 		StatusCode: http.StatusOK, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte(""))), | ||||
| 	} | ||||
| 	armClient.EXPECT().PatchResource(gomock.Any(), resourceID, diskUpdate).Return(response, nil).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	rerr := diskClient.Update(context.TODO(), "rg", "disk1", diskUpdate) | ||||
| 	assert.Nil(t, rerr) | ||||
|  | ||||
| 	response = &http.Response{ | ||||
| 		StatusCode: http.StatusTooManyRequests, | ||||
| 		Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))), | ||||
| 	} | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
|  | ||||
| 	armClient.EXPECT().PatchResource(gomock.Any(), resourceID, diskUpdate).Return(response, throttleErr).Times(1) | ||||
| 	armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1) | ||||
| 	rerr = diskClient.Update(context.TODO(), "rg", "disk1", diskUpdate) | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func getTestDiskUpdate() compute.DiskUpdate { | ||||
| 	return compute.DiskUpdate{ | ||||
| 		DiskUpdateProperties: &compute.DiskUpdateProperties{ | ||||
| 			DiskSizeGB: pointer.Int32(100), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDelete(t *testing.T) { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
|  | ||||
| 	r := getTestDisk("disk1") | ||||
| 	armClient := mockarmclient.NewMockInterface(ctrl) | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(r.ID, ""), "").Return(nil).Times(1) | ||||
|  | ||||
| 	diskClient := getTestDiskClient(armClient) | ||||
| 	rerr := diskClient.Delete(context.TODO(), "rg", "disk1") | ||||
| 	assert.Nil(t, rerr) | ||||
|  | ||||
| 	throttleErr := &retry.Error{ | ||||
| 		HTTPStatusCode: http.StatusTooManyRequests, | ||||
| 		RawError:       fmt.Errorf("error"), | ||||
| 		Retriable:      true, | ||||
| 		RetryAfter:     time.Unix(100, 0), | ||||
| 	} | ||||
| 	armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(r.ID, ""), "").Return(throttleErr).Times(1) | ||||
| 	rerr = diskClient.Delete(context.TODO(), "rg", "disk1") | ||||
| 	assert.Equal(t, throttleErr, rerr) | ||||
| } | ||||
|  | ||||
| func getTestDisk(name string) compute.Disk { | ||||
| 	return compute.Disk{ | ||||
| 		ID:       pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/%s", name)), | ||||
| 		Name:     pointer.String(name), | ||||
| 		Location: pointer.String("eastus"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getTestDiskClient(armClient armclient.Interface) *Client { | ||||
| 	rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{}) | ||||
| 	return &Client{ | ||||
| 		armClient:         armClient, | ||||
| 		subscriptionID:    "subscriptionID", | ||||
| 		rateLimiterReader: rateLimiterReader, | ||||
| 		rateLimiterWriter: rateLimiterWriter, | ||||
| 	} | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| /* | ||||
| Copyright 2020 The Kubernetes Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| // Package diskclient implements the client for Disks. | ||||
| package diskclient // import "k8s.io/legacy-cloud-providers/azure/clients/diskclient" | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot