chore: Cleanup in-tree credential provider azure and cloud provider azure

This commit is contained in:
Qi Ni
2024-01-19 13:52:38 +08:00
parent e9756693d0
commit 3bf2bf8191
503 changed files with 3 additions and 230262 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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)
}

View File

@@ -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.")
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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",

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 ""
}

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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"},
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
})
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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(),
})
}

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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))
}

View File

@@ -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&param1=value1&param2=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)
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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, "")
}

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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