chore: Cleanup in-tree credential provider azure and cloud provider azure
This commit is contained in:
25
LICENSES/vendor/github.com/Azure/azure-sdk-for-go/LICENSE
generated
vendored
25
LICENSES/vendor/github.com/Azure/azure-sdk-for-go/LICENSE
generated
vendored
@@ -1,25 +0,0 @@
|
||||
= vendor/github.com/Azure/azure-sdk-for-go licensed under: =
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
= vendor/github.com/Azure/azure-sdk-for-go/LICENSE.txt 4f7454c9bcbb0acee6d9a971001befe2
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/adal/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/adal/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest/adal licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/date/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/date/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest/date licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/mocks/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/mocks/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest/mocks licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/to/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/to/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest/to licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/validation/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/autorest/validation/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/autorest/validation licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/logger/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/logger/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/logger licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
195
LICENSES/vendor/github.com/Azure/go-autorest/tracing/LICENSE
generated
vendored
195
LICENSES/vendor/github.com/Azure/go-autorest/tracing/LICENSE
generated
vendored
@@ -1,195 +0,0 @@
|
||||
= vendor/github.com/Azure/go-autorest/tracing licensed under: =
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
= vendor/github.com/Azure/go-autorest/LICENSE a250e5ac3848f2acadb5adcb9555c18b
|
||||
24
LICENSES/vendor/github.com/gofrs/uuid/LICENSE
generated
vendored
24
LICENSES/vendor/github.com/gofrs/uuid/LICENSE
generated
vendored
@@ -1,24 +0,0 @@
|
||||
= vendor/github.com/gofrs/uuid licensed under: =
|
||||
|
||||
Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
= vendor/github.com/gofrs/uuid/LICENSE ae4ba217c6e20c2d8f48f69966b9121b
|
||||
26
LICENSES/vendor/github.com/rubiojr/go-vhd/LICENSE
generated
vendored
26
LICENSES/vendor/github.com/rubiojr/go-vhd/LICENSE
generated
vendored
@@ -1,26 +0,0 @@
|
||||
= vendor/github.com/rubiojr/go-vhd licensed under: =
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Sergio Rubio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
= vendor/github.com/rubiojr/go-vhd/LICENSE 9ce5db55ba47444787183e59733e1977
|
||||
@@ -25,7 +25,6 @@ package main
|
||||
import (
|
||||
// NOTE: Importing all in-tree cloud-providers is not required when
|
||||
// implementing an out-of-tree cloud-provider.
|
||||
_ "k8s.io/legacy-cloud-providers/azure"
|
||||
_ "k8s.io/legacy-cloud-providers/gce"
|
||||
_ "k8s.io/legacy-cloud-providers/vsphere"
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"k8s.io/component-base/version/verflag"
|
||||
|
||||
// ensure libs have a chance to globally register their flags
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
|
||||
)
|
||||
|
||||
@@ -60,16 +59,6 @@ func register(global *flag.FlagSet, local *pflag.FlagSet, globalName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// pflagRegister adds a flag to local that targets the Value associated with the Flag named globalName in global
|
||||
func pflagRegister(global, local *pflag.FlagSet, globalName string) {
|
||||
if f := global.Lookup(globalName); f != nil {
|
||||
f.Name = normalize(f.Name)
|
||||
local.AddFlag(f)
|
||||
} else {
|
||||
panic(fmt.Sprintf("failed to find flag in global flagset (pflag): %s", globalName))
|
||||
}
|
||||
}
|
||||
|
||||
// registerDeprecated registers the flag with register, and then marks it deprecated
|
||||
func registerDeprecated(global *flag.FlagSet, local *pflag.FlagSet, globalName, deprecated string) {
|
||||
register(global, local, globalName)
|
||||
@@ -79,10 +68,7 @@ func registerDeprecated(global *flag.FlagSet, local *pflag.FlagSet, globalName,
|
||||
// addCredentialProviderFlags adds flags from k8s.io/kubernetes/pkg/credentialprovider
|
||||
func addCredentialProviderFlags(fs *pflag.FlagSet) {
|
||||
// lookup flags in global flag set and re-register the values with our flagset
|
||||
global := pflag.CommandLine
|
||||
local := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
|
||||
|
||||
addLegacyCloudProviderCredentialProviderFlags(global, local)
|
||||
|
||||
fs.AddFlagSet(local)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func addLegacyCloudProviderCredentialProviderFlags(global, local *pflag.FlagSet) {
|
||||
// TODO(#58034): This is not a static file, so it's not quite as straightforward as --google-json-key.
|
||||
// We need to figure out how ACR users can dynamically provide pull credentials before we can deprecate this.
|
||||
pflagRegister(global, local, "azure-container-registry-config")
|
||||
local.MarkDeprecated("azure-container-registry-config", "Use --image-credential-provider-config and --image-credential-provider-bin-dir to setup acr credential provider instead. Will be removed in a future release.")
|
||||
}
|
||||
@@ -21,7 +21,6 @@ package app
|
||||
|
||||
import (
|
||||
// Credential providers
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
|
||||
|
||||
"k8s.io/component-base/featuregate"
|
||||
|
||||
12
go.mod
12
go.mod
@@ -10,9 +10,6 @@ go 1.21
|
||||
|
||||
require (
|
||||
bitbucket.org/bertimus9/systemstat v0.5.0
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.11.29
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23
|
||||
github.com/GoogleCloudPlatform/k8s-cloud-provider v1.18.1-0.20220218231025-f11817397a1b
|
||||
github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab
|
||||
github.com/Microsoft/go-winio v0.6.0
|
||||
@@ -135,13 +132,6 @@ require (
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.2 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
@@ -171,7 +161,6 @@ require (
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
@@ -209,7 +198,6 @@ require (
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/seccomp/libseccomp-golang v0.10.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
|
||||
45
go.sum
45
go.sum
@@ -166,30 +166,8 @@ cloud.google.com/go/webrisk v1.9.1/go.mod h1:4GCmXKcOa2BZcZPn6DCEvE7HypmEJcJkr4m
|
||||
cloud.google.com/go/websecurityscanner v1.6.1/go.mod h1:Njgaw3rttgRHXzwCB8kgCYqv5/rGpFCsBOvPbYgszpg=
|
||||
cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
|
||||
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GoogleCloudPlatform/k8s-cloud-provider v1.18.1-0.20220218231025-f11817397a1b h1:Heo1J/ttaQFgGJSVnCZquy3e5eH5j1nqxBuomztB3P0=
|
||||
@@ -317,8 +295,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=
|
||||
@@ -399,14 +375,11 @@ github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -708,8 +681,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021 h1:if3/24+h9Sq6eDx8UUz1SO9cT9tizyIsATfB7b4D3tc=
|
||||
github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -760,7 +731,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
@@ -860,9 +830,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -902,7 +869,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -946,10 +912,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -981,7 +944,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1045,15 +1007,11 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1064,8 +1022,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1131,7 +1087,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"github.com/go-openapi/validate": "use k8s.io/kube-openapi/pkg/validation/validate instead",
|
||||
"github.com/gogo/googleapis": "depends on unmaintained github.com/gogo/protobuf",
|
||||
"github.com/gogo/protobuf": "unmaintained",
|
||||
"github.com/golang/mock": "unmaintained, archive mode",
|
||||
"github.com/google/s2a-go": "cloud dependency, unstable",
|
||||
"github.com/google/shlex": "unmaintained, archive mode",
|
||||
"github.com/googleapis/enterprise-certificate-proxy": "references cloud dependencies",
|
||||
@@ -55,7 +54,6 @@
|
||||
"github.com/mndrix/tap-go": "unmaintained",
|
||||
"github.com/onsi/ginkgo": "Ginkgo has been migrated to V2, refer to #109111",
|
||||
"github.com/pkg/errors": "unmaintained, archive mode",
|
||||
"github.com/rubiojr/go-vhd": "unmaintained, archive mode",
|
||||
"github.com/smartystreets/goconvey": "MPL license not in CNCF allowlist",
|
||||
"github.com/spf13/viper": "refer to #102598",
|
||||
"github.com/xeipuuv/gojsonschema": "unmaintained",
|
||||
@@ -126,10 +124,6 @@
|
||||
"k8s.io/kubernetes",
|
||||
"k8s.io/metrics"
|
||||
],
|
||||
"github.com/golang/mock": [
|
||||
"k8s.io/kubernetes",
|
||||
"k8s.io/legacy-cloud-providers"
|
||||
],
|
||||
"github.com/google/s2a-go": [
|
||||
"cloud.google.com/go/compute",
|
||||
"google.golang.org/api"
|
||||
@@ -187,9 +181,6 @@
|
||||
"sigs.k8s.io/kustomize/api",
|
||||
"sigs.k8s.io/kustomize/kustomize/v5"
|
||||
],
|
||||
"github.com/rubiojr/go-vhd": [
|
||||
"k8s.io/legacy-cloud-providers"
|
||||
],
|
||||
"go.opencensus.io": [
|
||||
"cloud.google.com/go/compute",
|
||||
"github.com/Microsoft/hcsshim",
|
||||
@@ -238,7 +229,6 @@
|
||||
"cloud.google.com/go/compute",
|
||||
"github.com/GoogleCloudPlatform/k8s-cloud-provider",
|
||||
"github.com/gogo/protobuf",
|
||||
"github.com/golang/mock",
|
||||
"github.com/google/s2a-go",
|
||||
"github.com/google/shlex",
|
||||
"github.com/googleapis/enterprise-certificate-proxy",
|
||||
@@ -249,7 +239,6 @@
|
||||
"github.com/json-iterator/go",
|
||||
"github.com/mailru/easyjson",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/rubiojr/go-vhd",
|
||||
"go.opencensus.io",
|
||||
"golang.org/x/exp",
|
||||
"google.golang.org/api",
|
||||
|
||||
@@ -21,7 +21,6 @@ package cloudprovider
|
||||
|
||||
import (
|
||||
// Cloud providers
|
||||
_ "k8s.io/legacy-cloud-providers/azure"
|
||||
_ "k8s.io/legacy-cloud-providers/gce"
|
||||
_ "k8s.io/legacy-cloud-providers/vsphere"
|
||||
)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- andyzhangx
|
||||
- feiskyer
|
||||
- khenidak
|
||||
reviewers:
|
||||
- andyzhangx
|
||||
- feiskyer
|
||||
- khenidak
|
||||
emeritus_approvers:
|
||||
- karataliu
|
||||
- brendandburns
|
||||
@@ -1,291 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2017 Microsoft Corporation
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
*/
|
||||
|
||||
// Source: https://github.com/Azure/acr-docker-credential-helper/blob/a79b541f3ee761f6cc4511863ed41fb038c19464/src/docker-credential-acr/acr_login.go
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
)
|
||||
|
||||
type authDirective struct {
|
||||
service string
|
||||
realm string
|
||||
}
|
||||
|
||||
type acrAuthResponse struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// 5 minutes buffer time to allow timeshift between local machine and AAD
|
||||
const userAgentHeader = "User-Agent"
|
||||
const userAgent = "kubernetes-credentialprovider-acr"
|
||||
|
||||
const dockerTokenLoginUsernameGUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
var client = &http.Client{
|
||||
Transport: utilnet.SetTransportDefaults(&http.Transport{}),
|
||||
Timeout: time.Second * 60,
|
||||
}
|
||||
|
||||
func receiveChallengeFromLoginServer(serverAddress string) (*authDirective, error) {
|
||||
challengeURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: serverAddress,
|
||||
Path: "v2/",
|
||||
}
|
||||
var err error
|
||||
var r *http.Request
|
||||
r, err = http.NewRequest("GET", challengeURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct request, got %v", err)
|
||||
}
|
||||
r.Header.Add(userAgentHeader, userAgent)
|
||||
|
||||
var challenge *http.Response
|
||||
if challenge, err = client.Do(r); err != nil {
|
||||
return nil, fmt.Errorf("error reaching registry endpoint %s, error: %s", challengeURL.String(), err)
|
||||
}
|
||||
defer challenge.Body.Close()
|
||||
|
||||
if challenge.StatusCode != 401 {
|
||||
return nil, fmt.Errorf("registry did not issue a valid AAD challenge, status: %d", challenge.StatusCode)
|
||||
}
|
||||
|
||||
var authHeader []string
|
||||
var ok bool
|
||||
if authHeader, ok = challenge.Header["Www-Authenticate"]; !ok {
|
||||
return nil, fmt.Errorf("challenge response does not contain header 'Www-Authenticate'")
|
||||
}
|
||||
|
||||
if len(authHeader) != 1 {
|
||||
return nil, fmt.Errorf("registry did not issue a valid AAD challenge, authenticate header [%s]",
|
||||
strings.Join(authHeader, ", "))
|
||||
}
|
||||
|
||||
authSections := strings.SplitN(authHeader[0], " ", 2)
|
||||
authType := strings.ToLower(authSections[0])
|
||||
var authParams *map[string]string
|
||||
if authParams, err = parseAssignments(authSections[1]); err != nil {
|
||||
return nil, fmt.Errorf("unable to understand the contents of Www-Authenticate header %s", authSections[1])
|
||||
}
|
||||
|
||||
// verify headers
|
||||
if !strings.EqualFold("Bearer", authType) {
|
||||
return nil, fmt.Errorf("Www-Authenticate: expected realm: Bearer, actual: %s", authType)
|
||||
}
|
||||
if len((*authParams)["service"]) == 0 {
|
||||
return nil, fmt.Errorf("Www-Authenticate: missing header \"service\"")
|
||||
}
|
||||
if len((*authParams)["realm"]) == 0 {
|
||||
return nil, fmt.Errorf("Www-Authenticate: missing header \"realm\"")
|
||||
}
|
||||
|
||||
return &authDirective{
|
||||
service: (*authParams)["service"],
|
||||
realm: (*authParams)["realm"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func performTokenExchange(
|
||||
serverAddress string,
|
||||
directive *authDirective,
|
||||
tenant string,
|
||||
accessToken string) (string, error) {
|
||||
var err error
|
||||
data := url.Values{
|
||||
"service": []string{directive.service},
|
||||
"grant_type": []string{"access_token_refresh_token"},
|
||||
"access_token": []string{accessToken},
|
||||
"refresh_token": []string{accessToken},
|
||||
"tenant": []string{tenant},
|
||||
}
|
||||
|
||||
var realmURL *url.URL
|
||||
if realmURL, err = url.Parse(directive.realm); err != nil {
|
||||
return "", fmt.Errorf("Www-Authenticate: invalid realm %s", directive.realm)
|
||||
}
|
||||
authEndpoint := fmt.Sprintf("%s://%s/oauth2/exchange", realmURL.Scheme, realmURL.Host)
|
||||
|
||||
datac := data.Encode()
|
||||
var r *http.Request
|
||||
r, err = http.NewRequest("POST", authEndpoint, bytes.NewBufferString(datac))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to construct request, got %v", err)
|
||||
}
|
||||
r.Header.Add(userAgentHeader, userAgent)
|
||||
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Add("Content-Length", strconv.Itoa(len(datac)))
|
||||
|
||||
var exchange *http.Response
|
||||
if exchange, err = client.Do(r); err != nil {
|
||||
return "", fmt.Errorf("Www-Authenticate: failed to reach auth url %s", authEndpoint)
|
||||
}
|
||||
|
||||
defer exchange.Body.Close()
|
||||
if exchange.StatusCode != 200 {
|
||||
return "", fmt.Errorf("Www-Authenticate: auth url %s responded with status code %d", authEndpoint, exchange.StatusCode)
|
||||
}
|
||||
|
||||
var content []byte
|
||||
limitedReader := &io.LimitedReader{R: exchange.Body, N: maxReadLength}
|
||||
if content, err = io.ReadAll(limitedReader); err != nil {
|
||||
return "", fmt.Errorf("Www-Authenticate: error reading response from %s", authEndpoint)
|
||||
}
|
||||
|
||||
if limitedReader.N <= 0 {
|
||||
return "", errors.New("the read limit is reached")
|
||||
}
|
||||
|
||||
var authResp acrAuthResponse
|
||||
if err = json.Unmarshal(content, &authResp); err != nil {
|
||||
return "", fmt.Errorf("Www-Authenticate: unable to read response %s", content)
|
||||
}
|
||||
|
||||
return authResp.RefreshToken, nil
|
||||
}
|
||||
|
||||
// Try and parse a string of assignments in the form of:
|
||||
// key1 = value1, key2 = "value 2", key3 = ""
|
||||
// Note: this method and handle quotes but does not handle escaping of quotes
|
||||
func parseAssignments(statements string) (*map[string]string, error) {
|
||||
var cursor int
|
||||
result := make(map[string]string)
|
||||
var errorMsg = fmt.Errorf("malformed header value: %s", statements)
|
||||
for {
|
||||
// parse key
|
||||
equalIndex := nextOccurrence(statements, cursor, "=")
|
||||
if equalIndex == -1 {
|
||||
return nil, errorMsg
|
||||
}
|
||||
key := strings.TrimSpace(statements[cursor:equalIndex])
|
||||
|
||||
// parse value
|
||||
cursor = nextNoneSpace(statements, equalIndex+1)
|
||||
if cursor == -1 {
|
||||
return nil, errorMsg
|
||||
}
|
||||
// case: value is quoted
|
||||
if statements[cursor] == '"' {
|
||||
cursor = cursor + 1
|
||||
// like I said, not handling escapes, but this will skip any comma that's
|
||||
// within the quotes which is somewhat more likely
|
||||
closeQuoteIndex := nextOccurrence(statements, cursor, "\"")
|
||||
if closeQuoteIndex == -1 {
|
||||
return nil, errorMsg
|
||||
}
|
||||
value := statements[cursor:closeQuoteIndex]
|
||||
result[key] = value
|
||||
|
||||
commaIndex := nextNoneSpace(statements, closeQuoteIndex+1)
|
||||
if commaIndex == -1 {
|
||||
// no more comma, done
|
||||
return &result, nil
|
||||
} else if statements[commaIndex] != ',' {
|
||||
// expect comma immediately after close quote
|
||||
return nil, errorMsg
|
||||
} else {
|
||||
cursor = commaIndex + 1
|
||||
}
|
||||
} else {
|
||||
commaIndex := nextOccurrence(statements, cursor, ",")
|
||||
endStatements := commaIndex == -1
|
||||
var untrimmed string
|
||||
if endStatements {
|
||||
untrimmed = statements[cursor:commaIndex]
|
||||
} else {
|
||||
untrimmed = statements[cursor:]
|
||||
}
|
||||
value := strings.TrimSpace(untrimmed)
|
||||
|
||||
if len(value) == 0 {
|
||||
// disallow empty value without quote
|
||||
return nil, errorMsg
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
|
||||
if endStatements {
|
||||
return &result, nil
|
||||
}
|
||||
cursor = commaIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextOccurrence(str string, start int, sep string) int {
|
||||
if start >= len(str) {
|
||||
return -1
|
||||
}
|
||||
offset := strings.Index(str[start:], sep)
|
||||
if offset == -1 {
|
||||
return -1
|
||||
}
|
||||
return offset + start
|
||||
}
|
||||
|
||||
func nextNoneSpace(str string, start int) int {
|
||||
if start >= len(str) {
|
||||
return -1
|
||||
}
|
||||
offset := strings.IndexFunc(str[start:], func(c rune) bool { return !unicode.IsSpace(c) })
|
||||
if offset == -1 {
|
||||
return -1
|
||||
}
|
||||
return offset + start
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/legacy-cloud-providers/azure/auth"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var flagConfigFile = pflag.String("azure-container-registry-config", "",
|
||||
"Path to the file containing Azure container registry configuration information.")
|
||||
|
||||
const (
|
||||
dummyRegistryEmail = "name@contoso.com"
|
||||
maxReadLength = 10 * 1 << 20 // 10MB
|
||||
)
|
||||
|
||||
var (
|
||||
containerRegistryUrls = []string{"*.azurecr.io", "*.azurecr.cn", "*.azurecr.de", "*.azurecr.us"}
|
||||
acrRE = regexp.MustCompile(`.*\.azurecr\.io|.*\.azurecr\.cn|.*\.azurecr\.de|.*\.azurecr\.us`)
|
||||
warnOnce sync.Once
|
||||
)
|
||||
|
||||
// init registers the various means by which credentials may
|
||||
// be resolved on Azure.
|
||||
func init() {
|
||||
credentialprovider.RegisterCredentialProvider(
|
||||
"azure",
|
||||
NewACRProvider(flagConfigFile),
|
||||
)
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
expiresAt time.Time
|
||||
credentials credentialprovider.DockerConfigEntry
|
||||
registry string
|
||||
}
|
||||
|
||||
// acrExpirationPolicy implements ExpirationPolicy from client-go.
|
||||
type acrExpirationPolicy struct{}
|
||||
|
||||
// stringKeyFunc returns the cache key as a string
|
||||
func stringKeyFunc(obj interface{}) (string, error) {
|
||||
key := obj.(*cacheEntry).registry
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// IsExpired checks if the ACR credentials are expired.
|
||||
func (p *acrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
|
||||
return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
|
||||
}
|
||||
|
||||
// RegistriesClient is a testable interface for the ACR client List operation.
|
||||
type RegistriesClient interface {
|
||||
List(ctx context.Context) ([]containerregistry.Registry, error)
|
||||
}
|
||||
|
||||
// NewACRProvider parses the specified configFile and returns a DockerConfigProvider
|
||||
func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
|
||||
return &acrProvider{
|
||||
file: configFile,
|
||||
cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
|
||||
}
|
||||
}
|
||||
|
||||
type acrProvider struct {
|
||||
file *string
|
||||
config *auth.AzureAuthConfig
|
||||
environment *azure.Environment
|
||||
servicePrincipalToken *adal.ServicePrincipalToken
|
||||
cache cache.Store
|
||||
}
|
||||
|
||||
// ParseConfig returns a parsed configuration for an Azure cloudprovider config file
|
||||
func parseConfig(configReader io.Reader) (*auth.AzureAuthConfig, error) {
|
||||
var config auth.AzureAuthConfig
|
||||
|
||||
if configReader == nil {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
limitedReader := &io.LimitedReader{R: configReader, N: maxReadLength}
|
||||
configContents, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limitedReader.N <= 0 {
|
||||
return nil, errors.New("the read limit is reached")
|
||||
}
|
||||
err = yaml.Unmarshal(configContents, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (a *acrProvider) loadConfig(rdr io.Reader) error {
|
||||
var err error
|
||||
a.config, err = parseConfig(rdr)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to load azure credential file: %v", err)
|
||||
}
|
||||
|
||||
a.environment, err = auth.ParseAzureEnvironment(a.config.Cloud, a.config.ResourceManagerEndpoint, a.config.IdentitySystem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *acrProvider) Enabled() bool {
|
||||
if a.file == nil || len(*a.file) == 0 {
|
||||
klog.V(5).Infof("Azure config unspecified, disabling")
|
||||
return false
|
||||
}
|
||||
|
||||
if credentialprovider.AreLegacyCloudCredentialProvidersDisabled() {
|
||||
warnOnce.Do(func() {
|
||||
klog.V(4).Infof("Azure credential provider is now disabled. Please refer to sig-cloud-provider for guidance on external credential provider integration for Azure")
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := os.Open(*a.file)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to load config from file: %s", *a.file)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = a.loadConfig(f)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to load config from file: %s", *a.file)
|
||||
return false
|
||||
}
|
||||
|
||||
a.servicePrincipalToken, err = auth.GetServicePrincipalToken(a.config, a.environment)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to create service principal token: %v", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getFromCache attempts to get credentials from the cache
|
||||
func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.DockerConfig, bool) {
|
||||
cfg := credentialprovider.DockerConfig{}
|
||||
obj, exists, err := a.cache.GetByKey(loginServer)
|
||||
if err != nil {
|
||||
klog.Errorf("error getting ACR credentials from cache: %v", err)
|
||||
return cfg, false
|
||||
}
|
||||
if !exists {
|
||||
return cfg, false
|
||||
}
|
||||
|
||||
entry := obj.(*cacheEntry)
|
||||
cfg[entry.registry] = entry.credentials
|
||||
return cfg, true
|
||||
}
|
||||
|
||||
// getFromACR gets credentials from ACR since they are not in the cache
|
||||
func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerConfig, error) {
|
||||
cfg := credentialprovider.DockerConfig{}
|
||||
cred, err := getACRDockerEntryFromARMToken(a, loginServer)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
entry := &cacheEntry{
|
||||
expiresAt: time.Now().Add(10 * time.Minute),
|
||||
credentials: *cred,
|
||||
registry: loginServer,
|
||||
}
|
||||
if err := a.cache.Add(entry); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
cfg[loginServer] = *cred
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
loginServer := a.parseACRLoginServerFromImage(image)
|
||||
if loginServer == "" {
|
||||
klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
cfg := credentialprovider.DockerConfig{}
|
||||
if a.config != nil && a.config.UseManagedIdentityExtension {
|
||||
var exists bool
|
||||
cfg, exists = a.getFromCache(loginServer)
|
||||
if exists {
|
||||
klog.V(4).Infof("Got ACR credentials from cache for %s", loginServer)
|
||||
} else {
|
||||
klog.V(2).Infof("unable to get ACR credentials from cache for %s, checking ACR API", loginServer)
|
||||
var err error
|
||||
cfg, err = a.getFromACR(loginServer)
|
||||
if err != nil {
|
||||
klog.Errorf("error getting credentials from ACR for %s %v", loginServer, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add our entry for each of the supported container registry URLs
|
||||
for _, url := range containerRegistryUrls {
|
||||
cred := &credentialprovider.DockerConfigEntry{
|
||||
Username: a.config.AADClientID,
|
||||
Password: a.config.AADClientSecret,
|
||||
Email: dummyRegistryEmail,
|
||||
}
|
||||
cfg[url] = *cred
|
||||
}
|
||||
|
||||
// Handle the custom cloud case
|
||||
// In clouds where ACR is not yet deployed, the string will be empty
|
||||
if a.environment != nil && strings.Contains(a.environment.ContainerRegistryDNSSuffix, ".azurecr.") {
|
||||
customAcrSuffix := "*" + a.environment.ContainerRegistryDNSSuffix
|
||||
hasBeenAdded := false
|
||||
for _, url := range containerRegistryUrls {
|
||||
if strings.EqualFold(url, customAcrSuffix) {
|
||||
hasBeenAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasBeenAdded {
|
||||
cred := &credentialprovider.DockerConfigEntry{
|
||||
Username: a.config.AADClientID,
|
||||
Password: a.config.AADClientSecret,
|
||||
Email: dummyRegistryEmail,
|
||||
}
|
||||
cfg[customAcrSuffix] = *cred
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add ACR anonymous repo support: use empty username and password for anonymous access
|
||||
defaultConfigEntry := credentialprovider.DockerConfigEntry{
|
||||
Username: "",
|
||||
Password: "",
|
||||
Email: dummyRegistryEmail,
|
||||
}
|
||||
cfg["*.azurecr.*"] = defaultConfigEntry
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getLoginServer(registry containerregistry.Registry) string {
|
||||
return *(*registry.RegistryProperties).LoginServer
|
||||
}
|
||||
|
||||
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
|
||||
if a.servicePrincipalToken == nil {
|
||||
token, err := auth.GetServicePrincipalToken(a.config, a.environment)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to create service principal token: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
a.servicePrincipalToken = token
|
||||
} else {
|
||||
// Run EnsureFresh to make sure the token is valid and does not expire
|
||||
if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
|
||||
klog.Errorf("Failed to ensure fresh service principal token: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
armAccessToken := a.servicePrincipalToken.OAuthToken()
|
||||
|
||||
klog.V(4).Infof("discovering auth redirects for: %s", loginServer)
|
||||
directive, err := receiveChallengeFromLoginServer(loginServer)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to receive challenge: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("exchanging an acr refresh_token")
|
||||
registryRefreshToken, err := performTokenExchange(
|
||||
loginServer, directive, a.config.TenantID, armAccessToken)
|
||||
if err != nil {
|
||||
klog.Errorf("failed to perform token exchange: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
|
||||
return &credentialprovider.DockerConfigEntry{
|
||||
Username: dockerTokenLoginUsernameGUID,
|
||||
Password: registryRefreshToken,
|
||||
Email: dummyRegistryEmail,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseACRLoginServerFromImage takes image as parameter and returns login server of it.
|
||||
// Parameter `image` is expected in following format: foo.azurecr.io/bar/imageName:version
|
||||
// If the provided image is not an acr image, this function will return an empty string.
|
||||
func (a *acrProvider) parseACRLoginServerFromImage(image string) string {
|
||||
match := acrRE.FindAllString(image, -1)
|
||||
if len(match) == 1 {
|
||||
return match[0]
|
||||
}
|
||||
|
||||
// handle the custom cloud case
|
||||
if a != nil && a.environment != nil {
|
||||
cloudAcrSuffix := a.environment.ContainerRegistryDNSSuffix
|
||||
cloudAcrSuffixLength := len(cloudAcrSuffix)
|
||||
if cloudAcrSuffixLength > 0 {
|
||||
customAcrSuffixIndex := strings.Index(image, cloudAcrSuffix)
|
||||
if customAcrSuffixIndex != -1 {
|
||||
endIndex := customAcrSuffixIndex + cloudAcrSuffixLength
|
||||
return image[0:endIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
configStr := `
|
||||
{
|
||||
"aadClientId": "foo",
|
||||
"aadClientSecret": "bar"
|
||||
}`
|
||||
result := []containerregistry.Registry{
|
||||
{
|
||||
Name: pointer.String("foo"),
|
||||
RegistryProperties: &containerregistry.RegistryProperties{
|
||||
LoginServer: pointer.String("*.azurecr.io"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.String("bar"),
|
||||
RegistryProperties: &containerregistry.RegistryProperties{
|
||||
LoginServer: pointer.String("*.azurecr.cn"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.String("baz"),
|
||||
RegistryProperties: &containerregistry.RegistryProperties{
|
||||
LoginServer: pointer.String("*.azurecr.de"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.String("bus"),
|
||||
RegistryProperties: &containerregistry.RegistryProperties{
|
||||
LoginServer: pointer.String("*.azurecr.us"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider := &acrProvider{
|
||||
cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
|
||||
}
|
||||
provider.loadConfig(bytes.NewBufferString(configStr))
|
||||
|
||||
creds := provider.Provide("foo.azurecr.io/nginx:v1")
|
||||
|
||||
if len(creds) != len(result)+1 {
|
||||
t.Errorf("Unexpected list: %v, expected length %d", creds, len(result)+1)
|
||||
}
|
||||
for _, cred := range creds {
|
||||
if cred.Username != "" && cred.Username != "foo" {
|
||||
t.Errorf("expected 'foo' for username, saw: %v", cred.Username)
|
||||
}
|
||||
if cred.Password != "" && cred.Password != "bar" {
|
||||
t.Errorf("expected 'bar' for password, saw: %v", cred.Username)
|
||||
}
|
||||
}
|
||||
for _, val := range result {
|
||||
registryName := getLoginServer(val)
|
||||
if _, found := creds[registryName]; !found {
|
||||
t.Errorf("Missing expected registry: %s", registryName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvide(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
image string
|
||||
configStr string
|
||||
expectedCredsLength int
|
||||
}{
|
||||
{
|
||||
desc: "return multiple credentials using Service Principal",
|
||||
image: "foo.azurecr.io/bar/image:v1",
|
||||
configStr: `
|
||||
{
|
||||
"aadClientId": "foo",
|
||||
"aadClientSecret": "bar"
|
||||
}`,
|
||||
expectedCredsLength: 5,
|
||||
},
|
||||
{
|
||||
desc: "retuen 0 credential for non-ACR image using Managed Identity",
|
||||
image: "busybox",
|
||||
configStr: `
|
||||
{
|
||||
"UseManagedIdentityExtension": true
|
||||
}`,
|
||||
expectedCredsLength: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
provider := &acrProvider{
|
||||
cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
|
||||
}
|
||||
provider.loadConfig(bytes.NewBufferString(test.configStr))
|
||||
|
||||
creds := provider.Provide(test.image)
|
||||
assert.Equal(t, test.expectedCredsLength, len(creds), "TestCase[%d]: %s", i, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseACRLoginServerFromImage(t *testing.T) {
|
||||
configStr := `
|
||||
{
|
||||
"aadClientId": "foo",
|
||||
"aadClientSecret": "bar"
|
||||
}`
|
||||
|
||||
provider := &acrProvider{}
|
||||
provider.loadConfig(bytes.NewBufferString(configStr))
|
||||
provider.environment = &azure.Environment{
|
||||
ContainerRegistryDNSSuffix: ".azurecr.my.cloud",
|
||||
}
|
||||
tests := []struct {
|
||||
image string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
image: "invalidImage",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
image: "docker.io/library/busybox:latest",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
image: "foo.azurecr.io/bar/image:version",
|
||||
expected: "foo.azurecr.io",
|
||||
},
|
||||
{
|
||||
image: "foo.azurecr.cn/bar/image:version",
|
||||
expected: "foo.azurecr.cn",
|
||||
},
|
||||
{
|
||||
image: "foo.azurecr.de/bar/image:version",
|
||||
expected: "foo.azurecr.de",
|
||||
},
|
||||
{
|
||||
image: "foo.azurecr.us/bar/image:version",
|
||||
expected: "foo.azurecr.us",
|
||||
},
|
||||
{
|
||||
image: "foo.azurecr.my.cloud/bar/image:version",
|
||||
expected: "foo.azurecr.my.cloud",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if loginServer := provider.parseACRLoginServerFromImage(test.image); loginServer != test.expected {
|
||||
t.Errorf("function parseACRLoginServerFromImage returns \"%s\" for image %s, expected \"%s\"", loginServer, test.image, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
@@ -40,7 +40,6 @@ var (
|
||||
external bool
|
||||
detail string
|
||||
}{
|
||||
{"azure", false, "The Azure provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes-sigs/cloud-provider-azure"},
|
||||
{"gce", false, "The GCE provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes/cloud-provider-gcp"},
|
||||
{"vsphere", false, "The vSphere provider is deprecated and will be removed in a future release. Please use https://github.com/kubernetes/cloud-provider-vsphere"},
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
# We are no longer accepting features into k8s.io/legacy-cloud-providers.
|
||||
# Any kind/feature PRs must be approved by SIG Cloud Provider going forward.
|
||||
|
||||
emeritus_approvers:
|
||||
- andyzhangx
|
||||
- brendandburns
|
||||
- feiskyer
|
||||
- karataliu
|
||||
- khenidak
|
||||
- nilo19
|
||||
reviewers:
|
||||
- andyzhangx
|
||||
- aramase
|
||||
- feiskyer
|
||||
- khenidak
|
||||
- ritazh
|
||||
- nilo19
|
||||
@@ -1,290 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ADFSIdentitySystem is the override value for tenantID on Azure Stack clouds.
|
||||
ADFSIdentitySystem = "adfs"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrorNoAuth indicates that no credentials are provided.
|
||||
ErrorNoAuth = fmt.Errorf("no credentials provided for Azure cloud provider")
|
||||
)
|
||||
|
||||
// AzureAuthConfig holds auth related part of cloud config
|
||||
type AzureAuthConfig struct {
|
||||
// The cloud environment identifier. Takes values from https://github.com/Azure/go-autorest/blob/ec5f4903f77ed9927ac95b19ab8e44ada64c1356/autorest/azure/environments.go#L13
|
||||
Cloud string `json:"cloud,omitempty" yaml:"cloud,omitempty"`
|
||||
// The AAD Tenant ID for the Subscription that the cluster is deployed in
|
||||
TenantID string `json:"tenantId,omitempty" yaml:"tenantId,omitempty"`
|
||||
// The ClientID for an AAD application with RBAC access to talk to Azure RM APIs
|
||||
AADClientID string `json:"aadClientId,omitempty" yaml:"aadClientId,omitempty"`
|
||||
// The ClientSecret for an AAD application with RBAC access to talk to Azure RM APIs
|
||||
AADClientSecret string `json:"aadClientSecret,omitempty" yaml:"aadClientSecret,omitempty" datapolicy:"token"`
|
||||
// The path of a client certificate for an AAD application with RBAC access to talk to Azure RM APIs
|
||||
AADClientCertPath string `json:"aadClientCertPath,omitempty" yaml:"aadClientCertPath,omitempty"`
|
||||
// The password of the client certificate for an AAD application with RBAC access to talk to Azure RM APIs
|
||||
AADClientCertPassword string `json:"aadClientCertPassword,omitempty" yaml:"aadClientCertPassword,omitempty" datapolicy:"password"`
|
||||
// Use managed service identity for the virtual machine to access Azure ARM APIs
|
||||
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension,omitempty" yaml:"useManagedIdentityExtension,omitempty"`
|
||||
// UserAssignedIdentityID contains the Client ID of the user assigned MSI which is assigned to the underlying VMs. If empty the user assigned identity is not used.
|
||||
// More details of the user assigned identity can be found at: https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview
|
||||
// For the user assigned identity specified here to be used, the UseManagedIdentityExtension has to be set to true.
|
||||
UserAssignedIdentityID string `json:"userAssignedIdentityID,omitempty" yaml:"userAssignedIdentityID,omitempty"`
|
||||
// The ID of the Azure Subscription that the cluster is deployed in
|
||||
SubscriptionID string `json:"subscriptionId,omitempty" yaml:"subscriptionId,omitempty"`
|
||||
// IdentitySystem indicates the identity provider. Relevant only to hybrid clouds (Azure Stack).
|
||||
// Allowed values are 'azure_ad' (default), 'adfs'.
|
||||
IdentitySystem string `json:"identitySystem,omitempty" yaml:"identitySystem,omitempty"`
|
||||
// ResourceManagerEndpoint is the cloud's resource manager endpoint. If set, cloud provider queries this endpoint
|
||||
// in order to generate an autorest.Environment instance instead of using one of the pre-defined Environments.
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint,omitempty" yaml:"resourceManagerEndpoint,omitempty"`
|
||||
// The AAD Tenant ID for the Subscription that the network resources are deployed in
|
||||
NetworkResourceTenantID string `json:"networkResourceTenantID,omitempty" yaml:"networkResourceTenantID,omitempty"`
|
||||
// The ID of the Azure Subscription that the network resources are deployed in
|
||||
NetworkResourceSubscriptionID string `json:"networkResourceSubscriptionID,omitempty" yaml:"networkResourceSubscriptionID,omitempty"`
|
||||
}
|
||||
|
||||
// GetServicePrincipalToken creates a new service principal token based on the configuration.
|
||||
//
|
||||
// By default, the cluster and its network resources are deployed in the same AAD Tenant and Subscription,
|
||||
// and all azure clients use this method to fetch Service Principal Token.
|
||||
//
|
||||
// If NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID, network resources are deployed in different AAD Tenant and Subscription than those for the cluster,
|
||||
// than only azure clients except VM/VMSS and network resource ones use this method to fetch Token.
|
||||
// For tokens for VM/VMSS and network resource ones, please check GetMultiTenantServicePrincipalToken and GetNetworkResourceServicePrincipalToken.
|
||||
func GetServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.ServicePrincipalToken, error) {
|
||||
var tenantID string
|
||||
if strings.EqualFold(config.IdentitySystem, ADFSIdentitySystem) {
|
||||
tenantID = ADFSIdentitySystem
|
||||
} else {
|
||||
tenantID = config.TenantID
|
||||
}
|
||||
|
||||
if config.UseManagedIdentityExtension {
|
||||
klog.V(2).Infoln("azure: using managed identity extension to retrieve access token")
|
||||
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Getting the managed service identity endpoint: %v", err)
|
||||
}
|
||||
if len(config.UserAssignedIdentityID) > 0 {
|
||||
klog.V(4).Info("azure: using User Assigned MSI ID to retrieve access token")
|
||||
return adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint,
|
||||
env.ServiceManagementEndpoint,
|
||||
config.UserAssignedIdentityID)
|
||||
}
|
||||
klog.V(4).Info("azure: using System Assigned MSI to retrieve access token")
|
||||
return adal.NewServicePrincipalTokenFromMSI(
|
||||
msiEndpoint,
|
||||
env.ServiceManagementEndpoint)
|
||||
}
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, tenantID, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating the OAuth config: %v", err)
|
||||
}
|
||||
|
||||
if len(config.AADClientSecret) > 0 {
|
||||
klog.V(2).Infoln("azure: using client_id+client_secret to retrieve access token")
|
||||
return adal.NewServicePrincipalToken(
|
||||
*oauthConfig,
|
||||
config.AADClientID,
|
||||
config.AADClientSecret,
|
||||
env.ServiceManagementEndpoint)
|
||||
}
|
||||
|
||||
if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 {
|
||||
klog.V(2).Infoln("azure: using jwt client_assertion (client_cert+client_private_key) to retrieve access token")
|
||||
certData, err := ioutil.ReadFile(config.AADClientCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading the client certificate from file %s: %v", config.AADClientCertPath, err)
|
||||
}
|
||||
certificate, privateKey, err := decodePkcs12(certData, config.AADClientCertPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding the client certificate: %v", err)
|
||||
}
|
||||
return adal.NewServicePrincipalTokenFromCertificate(
|
||||
*oauthConfig,
|
||||
config.AADClientID,
|
||||
certificate,
|
||||
privateKey,
|
||||
env.ServiceManagementEndpoint)
|
||||
}
|
||||
|
||||
return nil, ErrorNoAuth
|
||||
}
|
||||
|
||||
// GetMultiTenantServicePrincipalToken is used when (and only when) NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID.
|
||||
//
|
||||
// In that scenario, network resources are deployed in different AAD Tenant and Subscription than those for the cluster,
|
||||
// and this method creates a new multi-tenant service principal token based on the configuration.
|
||||
//
|
||||
// PrimaryToken of the returned multi-tenant token is for the AAD Tenant specified by TenantID, and AuxiliaryToken of the returned multi-tenant token is for the AAD Tenant specified by NetworkResourceTenantID.
|
||||
//
|
||||
// Azure VM/VMSS clients use this multi-tenant token, in order to operate those VM/VMSS in AAD Tenant specified by TenantID, and meanwhile in their payload they are referencing network resources (e.g. Load Balancer, Network Security Group, etc.) in AAD Tenant specified by NetworkResourceTenantID.
|
||||
func GetMultiTenantServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.MultiTenantServicePrincipalToken, error) {
|
||||
err := config.checkConfigWhenNetworkResourceInDifferentTenant()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("got error(%v) in getting multi-tenant service principal token", err)
|
||||
}
|
||||
|
||||
multiTenantOAuthConfig, err := adal.NewMultiTenantOAuthConfig(
|
||||
env.ActiveDirectoryEndpoint, config.TenantID, []string{config.NetworkResourceTenantID}, adal.OAuthOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating the multi-tenant OAuth config: %v", err)
|
||||
}
|
||||
|
||||
if len(config.AADClientSecret) > 0 {
|
||||
klog.V(2).Infoln("azure: using client_id+client_secret to retrieve multi-tenant access token")
|
||||
return adal.NewMultiTenantServicePrincipalToken(
|
||||
multiTenantOAuthConfig,
|
||||
config.AADClientID,
|
||||
config.AADClientSecret,
|
||||
env.ServiceManagementEndpoint)
|
||||
}
|
||||
|
||||
if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 {
|
||||
return nil, fmt.Errorf("AAD Application client certificate authentication is not supported in getting multi-tenant service principal token")
|
||||
}
|
||||
|
||||
return nil, ErrorNoAuth
|
||||
}
|
||||
|
||||
// GetNetworkResourceServicePrincipalToken is used when (and only when) NetworkResourceTenantID and NetworkResourceSubscriptionID are specified to have different values than TenantID and SubscriptionID.
|
||||
//
|
||||
// In that scenario, network resources are deployed in different AAD Tenant and Subscription than those for the cluster,
|
||||
// and this method creates a new service principal token for network resources tenant based on the configuration.
|
||||
//
|
||||
// Azure network resource (Load Balancer, Public IP, Route Table, Network Security Group and their sub level resources) clients use this multi-tenant token, in order to operate resources in AAD Tenant specified by NetworkResourceTenantID.
|
||||
func GetNetworkResourceServicePrincipalToken(config *AzureAuthConfig, env *azure.Environment) (*adal.ServicePrincipalToken, error) {
|
||||
err := config.checkConfigWhenNetworkResourceInDifferentTenant()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("got error(%v) in getting network resources service principal token", err)
|
||||
}
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.NetworkResourceTenantID, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating the OAuth config for network resources tenant: %v", err)
|
||||
}
|
||||
|
||||
if len(config.AADClientSecret) > 0 {
|
||||
klog.V(2).Infoln("azure: using client_id+client_secret to retrieve access token for network resources tenant")
|
||||
return adal.NewServicePrincipalToken(
|
||||
*oauthConfig,
|
||||
config.AADClientID,
|
||||
config.AADClientSecret,
|
||||
env.ServiceManagementEndpoint)
|
||||
}
|
||||
|
||||
if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 {
|
||||
return nil, fmt.Errorf("AAD Application client certificate authentication is not supported in getting network resources service principal token")
|
||||
}
|
||||
|
||||
return nil, ErrorNoAuth
|
||||
}
|
||||
|
||||
// ParseAzureEnvironment returns the azure environment.
|
||||
// If 'resourceManagerEndpoint' is set, the environment is computed by querying the cloud's resource manager endpoint.
|
||||
// Otherwise, a pre-defined Environment is looked up by name.
|
||||
func ParseAzureEnvironment(cloudName, resourceManagerEndpoint, identitySystem string) (*azure.Environment, error) {
|
||||
var env azure.Environment
|
||||
var err error
|
||||
if resourceManagerEndpoint != "" {
|
||||
klog.V(4).Infof("Loading environment from resource manager endpoint: %s", resourceManagerEndpoint)
|
||||
nameOverride := azure.OverrideProperty{Key: azure.EnvironmentName, Value: cloudName}
|
||||
env, err = azure.EnvironmentFromURL(resourceManagerEndpoint, nameOverride)
|
||||
if err == nil {
|
||||
azureStackOverrides(&env, resourceManagerEndpoint, identitySystem)
|
||||
}
|
||||
} else if cloudName == "" {
|
||||
klog.V(4).Info("Using public cloud environment")
|
||||
env = azure.PublicCloud
|
||||
} else {
|
||||
klog.V(4).Infof("Using %s environment", cloudName)
|
||||
env, err = azure.EnvironmentFromName(cloudName)
|
||||
}
|
||||
return &env, err
|
||||
}
|
||||
|
||||
// UsesNetworkResourceInDifferentTenant determines whether the AzureAuthConfig indicates to use network resources in different AAD Tenant and Subscription than those for the cluster
|
||||
// Return true only when both NetworkResourceTenantID and NetworkResourceSubscriptionID are specified
|
||||
// and they are not equals to TenantID and SubscriptionID
|
||||
func (config *AzureAuthConfig) UsesNetworkResourceInDifferentTenant() bool {
|
||||
return len(config.NetworkResourceTenantID) > 0 &&
|
||||
len(config.NetworkResourceSubscriptionID) > 0 &&
|
||||
!strings.EqualFold(config.NetworkResourceTenantID, config.TenantID) &&
|
||||
!strings.EqualFold(config.NetworkResourceSubscriptionID, config.SubscriptionID)
|
||||
}
|
||||
|
||||
// decodePkcs12 decodes a PKCS#12 client certificate by extracting the public certificate and
|
||||
// the private RSA key
|
||||
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decoding the PKCS#12 client certificate: %v", err)
|
||||
}
|
||||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
|
||||
if !isRsaKey {
|
||||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain a RSA private key")
|
||||
}
|
||||
|
||||
return certificate, rsaPrivateKey, nil
|
||||
}
|
||||
|
||||
// azureStackOverrides ensures that the Environment matches what AKSe currently generates for Azure Stack
|
||||
func azureStackOverrides(env *azure.Environment, resourceManagerEndpoint, identitySystem string) {
|
||||
env.ManagementPortalURL = strings.Replace(resourceManagerEndpoint, "https://management.", "https://portal.", -1)
|
||||
env.ServiceManagementEndpoint = env.TokenAudience
|
||||
env.ResourceManagerVMDNSSuffix = strings.Replace(resourceManagerEndpoint, "https://management.", "cloudapp.", -1)
|
||||
env.ResourceManagerVMDNSSuffix = strings.TrimSuffix(env.ResourceManagerVMDNSSuffix, "/")
|
||||
if strings.EqualFold(identitySystem, ADFSIdentitySystem) {
|
||||
env.ActiveDirectoryEndpoint = strings.TrimSuffix(env.ActiveDirectoryEndpoint, "/")
|
||||
env.ActiveDirectoryEndpoint = strings.TrimSuffix(env.ActiveDirectoryEndpoint, "adfs")
|
||||
}
|
||||
}
|
||||
|
||||
// checkConfigWhenNetworkResourceInDifferentTenant checks configuration for the scenario of using network resource in different tenant
|
||||
func (config *AzureAuthConfig) checkConfigWhenNetworkResourceInDifferentTenant() error {
|
||||
if !config.UsesNetworkResourceInDifferentTenant() {
|
||||
return fmt.Errorf("NetworkResourceTenantID and NetworkResourceSubscriptionID must be configured")
|
||||
}
|
||||
|
||||
if strings.EqualFold(config.IdentitySystem, ADFSIdentitySystem) {
|
||||
return fmt.Errorf("ADFS identity system is not supported")
|
||||
}
|
||||
|
||||
if config.UseManagedIdentityExtension {
|
||||
return fmt.Errorf("managed identity is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
CrossTenantNetworkResourceNegativeConfig = []*AzureAuthConfig{
|
||||
{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
},
|
||||
{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
NetworkResourceTenantID: "NetworkResourceTenantID",
|
||||
NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID",
|
||||
IdentitySystem: ADFSIdentitySystem,
|
||||
},
|
||||
{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
NetworkResourceTenantID: "NetworkResourceTenantID",
|
||||
NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID",
|
||||
UseManagedIdentityExtension: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestGetServicePrincipalToken(t *testing.T) {
|
||||
config := &AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
}
|
||||
env := &azure.PublicCloud
|
||||
|
||||
token, err := GetServicePrincipalToken(config, env)
|
||||
assert.NoError(t, err)
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.TenantID, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, token, spt)
|
||||
}
|
||||
|
||||
func TestGetMultiTenantServicePrincipalToken(t *testing.T) {
|
||||
config := &AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
NetworkResourceTenantID: "NetworkResourceTenantID",
|
||||
NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID",
|
||||
}
|
||||
env := &azure.PublicCloud
|
||||
|
||||
multiTenantToken, err := GetMultiTenantServicePrincipalToken(config, env)
|
||||
assert.NoError(t, err)
|
||||
|
||||
multiTenantOAuthConfig, err := adal.NewMultiTenantOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID, []string{config.NetworkResourceTenantID}, adal.OAuthOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
spt, err := adal.NewMultiTenantServicePrincipalToken(multiTenantOAuthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, multiTenantToken, spt)
|
||||
}
|
||||
|
||||
func TestGetMultiTenantServicePrincipalTokenNegative(t *testing.T) {
|
||||
env := &azure.PublicCloud
|
||||
for _, config := range CrossTenantNetworkResourceNegativeConfig {
|
||||
_, err := GetMultiTenantServicePrincipalToken(config, env)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetworkResourceServicePrincipalToken(t *testing.T) {
|
||||
config := &AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
NetworkResourceTenantID: "NetworkResourceTenantID",
|
||||
NetworkResourceSubscriptionID: "NetworkResourceSubscriptionID",
|
||||
}
|
||||
env := &azure.PublicCloud
|
||||
|
||||
token, err := GetNetworkResourceServicePrincipalToken(config, env)
|
||||
assert.NoError(t, err)
|
||||
|
||||
oauthConfig, err := adal.NewOAuthConfigWithAPIVersion(env.ActiveDirectoryEndpoint, config.NetworkResourceTenantID, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.AADClientID, config.AADClientSecret, env.ServiceManagementEndpoint)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, token, spt)
|
||||
}
|
||||
|
||||
func TestGetNetworkResourceServicePrincipalTokenNegative(t *testing.T) {
|
||||
env := &azure.PublicCloud
|
||||
for _, config := range CrossTenantNetworkResourceNegativeConfig {
|
||||
_, err := GetNetworkResourceServicePrincipalToken(config, env)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAzureEnvironment(t *testing.T) {
|
||||
cases := []struct {
|
||||
cloudName string
|
||||
resourceManagerEndpoint string
|
||||
identitySystem string
|
||||
expected *azure.Environment
|
||||
}{
|
||||
{
|
||||
cloudName: "",
|
||||
resourceManagerEndpoint: "",
|
||||
identitySystem: "",
|
||||
expected: &azure.PublicCloud,
|
||||
},
|
||||
{
|
||||
cloudName: "AZURECHINACLOUD",
|
||||
resourceManagerEndpoint: "",
|
||||
identitySystem: "",
|
||||
expected: &azure.ChinaCloud,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
env, err := ParseAzureEnvironment(c.cloudName, c.resourceManagerEndpoint, c.identitySystem)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.expected, env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureStackOverrides(t *testing.T) {
|
||||
env := &azure.PublicCloud
|
||||
resourceManagerEndpoint := "https://management.test.com/"
|
||||
|
||||
azureStackOverrides(env, resourceManagerEndpoint, "")
|
||||
assert.Equal(t, env.ManagementPortalURL, "https://portal.test.com/")
|
||||
assert.Equal(t, env.ServiceManagementEndpoint, env.TokenAudience)
|
||||
assert.Equal(t, env.ResourceManagerVMDNSSuffix, "cloudapp.test.com")
|
||||
assert.Equal(t, env.ActiveDirectoryEndpoint, "https://login.microsoftonline.com/")
|
||||
|
||||
azureStackOverrides(env, resourceManagerEndpoint, "adfs")
|
||||
assert.Equal(t, env.ManagementPortalURL, "https://portal.test.com/")
|
||||
assert.Equal(t, env.ServiceManagementEndpoint, env.TokenAudience)
|
||||
assert.Equal(t, env.ResourceManagerVMDNSSuffix, "cloudapp.test.com")
|
||||
assert.Equal(t, env.ActiveDirectoryEndpoint, "https://login.microsoftonline.com")
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package auth provides a general libraty to authorize Azure ARM clients.
|
||||
package auth // import "k8s.io/legacy-cloud-providers/azure/auth"
|
||||
@@ -1,988 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/legacy-cloud-providers/azure/auth"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/diskclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/fileclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/publicipclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routeclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routetableclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/snapshotclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/subnetclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmsizeclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
|
||||
// ensure the newly added package from azure-sdk-for-go is in vendor/
|
||||
_ "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient"
|
||||
// ensure the newly added package from azure-sdk-for-go is in vendor/
|
||||
_ "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// CloudProviderName is the value used for the --cloud-provider flag
|
||||
CloudProviderName = "azure"
|
||||
// AzureStackCloudName is the cloud name of Azure Stack
|
||||
AzureStackCloudName = "AZURESTACKCLOUD"
|
||||
rateLimitQPSDefault = 1.0
|
||||
rateLimitBucketDefault = 5
|
||||
backoffRetriesDefault = 6
|
||||
backoffExponentDefault = 1.5
|
||||
backoffDurationDefault = 5 // in seconds
|
||||
backoffJitterDefault = 1.0
|
||||
// According to https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#load-balancer.
|
||||
maximumLoadBalancerRuleCount = 250
|
||||
|
||||
vmTypeVMSS = "vmss"
|
||||
vmTypeStandard = "standard"
|
||||
|
||||
loadBalancerSkuBasic = "basic"
|
||||
loadBalancerSkuStandard = "standard"
|
||||
|
||||
externalResourceGroupLabel = "kubernetes.azure.com/resource-group"
|
||||
managedByAzureLabel = "kubernetes.azure.com/managed"
|
||||
)
|
||||
|
||||
const (
|
||||
// PreConfiguredBackendPoolLoadBalancerTypesNone means that the load balancers are not pre-configured
|
||||
PreConfiguredBackendPoolLoadBalancerTypesNone = ""
|
||||
// PreConfiguredBackendPoolLoadBalancerTypesInternal means that the `internal` load balancers are pre-configured
|
||||
PreConfiguredBackendPoolLoadBalancerTypesInternal = "internal"
|
||||
// PreConfiguredBackendPoolLoadBalancerTypesExternal means that the `external` load balancers are pre-configured
|
||||
PreConfiguredBackendPoolLoadBalancerTypesExternal = "external"
|
||||
// PreConfiguredBackendPoolLoadBalancerTypesAll means that all load balancers are pre-configured
|
||||
PreConfiguredBackendPoolLoadBalancerTypesAll = "all"
|
||||
)
|
||||
|
||||
var (
|
||||
// Master nodes are not added to standard load balancer by default.
|
||||
defaultExcludeMasterFromStandardLB = true
|
||||
// Outbound SNAT is enabled by default.
|
||||
defaultDisableOutboundSNAT = false
|
||||
)
|
||||
|
||||
// Config holds the configuration parsed from the --cloud-config flag
|
||||
// All fields are required unless otherwise specified
|
||||
// NOTE: Cloud config files should follow the same Kubernetes deprecation policy as
|
||||
// flags or CLIs. Config fields should not change behavior in incompatible ways and
|
||||
// should be deprecated for at least 2 release prior to removing.
|
||||
// See https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-a-flag-or-cli
|
||||
// for more details.
|
||||
type Config struct {
|
||||
auth.AzureAuthConfig
|
||||
CloudProviderRateLimitConfig
|
||||
|
||||
// The name of the resource group that the cluster is deployed in
|
||||
ResourceGroup string `json:"resourceGroup,omitempty" yaml:"resourceGroup,omitempty"`
|
||||
// The location of the resource group that the cluster is deployed in
|
||||
Location string `json:"location,omitempty" yaml:"location,omitempty"`
|
||||
// The name of the VNet that the cluster is deployed in
|
||||
VnetName string `json:"vnetName,omitempty" yaml:"vnetName,omitempty"`
|
||||
// The name of the resource group that the Vnet is deployed in
|
||||
VnetResourceGroup string `json:"vnetResourceGroup,omitempty" yaml:"vnetResourceGroup,omitempty"`
|
||||
// The name of the subnet that the cluster is deployed in
|
||||
SubnetName string `json:"subnetName,omitempty" yaml:"subnetName,omitempty"`
|
||||
// The name of the security group attached to the cluster's subnet
|
||||
SecurityGroupName string `json:"securityGroupName,omitempty" yaml:"securityGroupName,omitempty"`
|
||||
// The name of the resource group that the security group is deployed in
|
||||
SecurityGroupResourceGroup string `json:"securityGroupResourceGroup,omitempty" yaml:"securityGroupResourceGroup,omitempty"`
|
||||
// (Optional in 1.6) The name of the route table attached to the subnet that the cluster is deployed in
|
||||
RouteTableName string `json:"routeTableName,omitempty" yaml:"routeTableName,omitempty"`
|
||||
// The name of the resource group that the RouteTable is deployed in
|
||||
RouteTableResourceGroup string `json:"routeTableResourceGroup,omitempty" yaml:"routeTableResourceGroup,omitempty"`
|
||||
// (Optional) The name of the availability set that should be used as the load balancer backend
|
||||
// If this is set, the Azure cloudprovider will only add nodes from that availability set to the load
|
||||
// balancer backend pool. If this is not set, and multiple agent pools (availability sets) are used, then
|
||||
// the cloudprovider will try to add all nodes to a single backend pool which is forbidden.
|
||||
// In other words, if you use multiple agent pools (availability sets), you MUST set this field.
|
||||
PrimaryAvailabilitySetName string `json:"primaryAvailabilitySetName,omitempty" yaml:"primaryAvailabilitySetName,omitempty"`
|
||||
// The type of azure nodes. Candidate values are: vmss and standard.
|
||||
// If not set, it will be default to standard.
|
||||
VMType string `json:"vmType,omitempty" yaml:"vmType,omitempty"`
|
||||
// The name of the scale set that should be used as the load balancer backend.
|
||||
// If this is set, the Azure cloudprovider will only add nodes from that scale set to the load
|
||||
// balancer backend pool. If this is not set, and multiple agent pools (scale sets) are used, then
|
||||
// the cloudprovider will try to add all nodes to a single backend pool which is forbidden.
|
||||
// In other words, if you use multiple agent pools (scale sets), you MUST set this field.
|
||||
PrimaryScaleSetName string `json:"primaryScaleSetName,omitempty" yaml:"primaryScaleSetName,omitempty"`
|
||||
// Enable exponential backoff to manage resource request retries
|
||||
CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"`
|
||||
// Backoff retry limit
|
||||
CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries,omitempty" yaml:"cloudProviderBackoffRetries,omitempty"`
|
||||
// Backoff exponent
|
||||
CloudProviderBackoffExponent float64 `json:"cloudProviderBackoffExponent,omitempty" yaml:"cloudProviderBackoffExponent,omitempty"`
|
||||
// Backoff duration
|
||||
CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration,omitempty" yaml:"cloudProviderBackoffDuration,omitempty"`
|
||||
// Backoff jitter
|
||||
CloudProviderBackoffJitter float64 `json:"cloudProviderBackoffJitter,omitempty" yaml:"cloudProviderBackoffJitter,omitempty"`
|
||||
// Use instance metadata service where possible
|
||||
UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty" yaml:"useInstanceMetadata,omitempty"`
|
||||
|
||||
// Sku of Load Balancer and Public IP. Candidate values are: basic and standard.
|
||||
// If not set, it will be default to basic.
|
||||
LoadBalancerSku string `json:"loadBalancerSku,omitempty" yaml:"loadBalancerSku,omitempty"`
|
||||
// ExcludeMasterFromStandardLB excludes master nodes from standard load balancer.
|
||||
// If not set, it will be default to true.
|
||||
ExcludeMasterFromStandardLB *bool `json:"excludeMasterFromStandardLB,omitempty" yaml:"excludeMasterFromStandardLB,omitempty"`
|
||||
// DisableOutboundSNAT disables the outbound SNAT for public load balancer rules.
|
||||
// It should only be set when loadBalancerSku is standard. If not set, it will be default to false.
|
||||
DisableOutboundSNAT *bool `json:"disableOutboundSNAT,omitempty" yaml:"disableOutboundSNAT,omitempty"`
|
||||
|
||||
// Maximum allowed LoadBalancer Rule Count is the limit enforced by Azure Load balancer
|
||||
MaximumLoadBalancerRuleCount int `json:"maximumLoadBalancerRuleCount,omitempty" yaml:"maximumLoadBalancerRuleCount,omitempty"`
|
||||
|
||||
// The cloud configure type for Azure cloud provider. Supported values are file, secret and merge.
|
||||
CloudConfigType cloudConfigType `json:"cloudConfigType,omitempty" yaml:"cloudConfigType,omitempty"`
|
||||
|
||||
// LoadBalancerName determines the specific name of the load balancer user want to use, working with
|
||||
// LoadBalancerResourceGroup
|
||||
LoadBalancerName string `json:"loadBalancerName,omitempty" yaml:"loadBalancerName,omitempty"`
|
||||
// LoadBalancerResourceGroup determines the specific resource group of the load balancer user want to use, working
|
||||
// with LoadBalancerName
|
||||
LoadBalancerResourceGroup string `json:"loadBalancerResourceGroup,omitempty" yaml:"loadBalancerResourceGroup,omitempty"`
|
||||
// PreConfiguredBackendPoolLoadBalancerTypes determines whether the LoadBalancer BackendPool has been preconfigured.
|
||||
// Candidate values are:
|
||||
// "": exactly with today (not pre-configured for any LBs)
|
||||
// "internal": for internal LoadBalancer
|
||||
// "external": for external LoadBalancer
|
||||
// "all": for both internal and external LoadBalancer
|
||||
PreConfiguredBackendPoolLoadBalancerTypes string `json:"preConfiguredBackendPoolLoadBalancerTypes,omitempty" yaml:"preConfiguredBackendPoolLoadBalancerTypes,omitempty"`
|
||||
// EnableMultipleStandardLoadBalancers determines the behavior of the standard load balancer. If set to true
|
||||
// there would be one standard load balancer per VMAS or VMSS, which is similar with the behavior of the basic
|
||||
// load balancer. Users could select the specific standard load balancer for their service by the service
|
||||
// annotation `service.beta.kubernetes.io/azure-load-balancer-mode`, If set to false, the same standard load balancer
|
||||
// would be shared by all services in the cluster. In this case, the mode selection annotation would be ignored.
|
||||
EnableMultipleStandardLoadBalancers bool `json:"enableMultipleStandardLoadBalancers,omitempty" yaml:"enableMultipleStandardLoadBalancers,omitempty"`
|
||||
|
||||
// AvailabilitySetNodesCacheTTLInSeconds sets the Cache TTL for availabilitySetNodesCache
|
||||
// if not set, will use default value
|
||||
AvailabilitySetNodesCacheTTLInSeconds int `json:"availabilitySetNodesCacheTTLInSeconds,omitempty" yaml:"availabilitySetNodesCacheTTLInSeconds,omitempty"`
|
||||
// VmssCacheTTLInSeconds sets the cache TTL for VMSS
|
||||
VmssCacheTTLInSeconds int `json:"vmssCacheTTLInSeconds,omitempty" yaml:"vmssCacheTTLInSeconds,omitempty"`
|
||||
// VmssVirtualMachinesCacheTTLInSeconds sets the cache TTL for vmssVirtualMachines
|
||||
VmssVirtualMachinesCacheTTLInSeconds int `json:"vmssVirtualMachinesCacheTTLInSeconds,omitempty" yaml:"vmssVirtualMachinesCacheTTLInSeconds,omitempty"`
|
||||
// VmCacheTTLInSeconds sets the cache TTL for vm
|
||||
VMCacheTTLInSeconds int `json:"vmCacheTTLInSeconds,omitempty" yaml:"vmCacheTTLInSeconds,omitempty"`
|
||||
// LoadBalancerCacheTTLInSeconds sets the cache TTL for load balancer
|
||||
LoadBalancerCacheTTLInSeconds int `json:"loadBalancerCacheTTLInSeconds,omitempty" yaml:"loadBalancerCacheTTLInSeconds,omitempty"`
|
||||
// NsgCacheTTLInSeconds sets the cache TTL for network security group
|
||||
NsgCacheTTLInSeconds int `json:"nsgCacheTTLInSeconds,omitempty" yaml:"nsgCacheTTLInSeconds,omitempty"`
|
||||
// RouteTableCacheTTLInSeconds sets the cache TTL for route table
|
||||
RouteTableCacheTTLInSeconds int `json:"routeTableCacheTTLInSeconds,omitempty" yaml:"routeTableCacheTTLInSeconds,omitempty"`
|
||||
|
||||
// DisableAvailabilitySetNodes disables VMAS nodes support when "VMType" is set to "vmss".
|
||||
DisableAvailabilitySetNodes bool `json:"disableAvailabilitySetNodes,omitempty" yaml:"disableAvailabilitySetNodes,omitempty"`
|
||||
|
||||
// Tags determines what tags shall be applied to the shared resources managed by controller manager, which
|
||||
// includes load balancer, security group and route table. The supported format is `a=b,c=d,...`. After updated
|
||||
// this config, the old tags would be replaced by the new ones.
|
||||
Tags string `json:"tags,omitempty" yaml:"tags,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ cloudprovider.Interface = (*Cloud)(nil)
|
||||
_ cloudprovider.Instances = (*Cloud)(nil)
|
||||
_ cloudprovider.LoadBalancer = (*Cloud)(nil)
|
||||
_ cloudprovider.Routes = (*Cloud)(nil)
|
||||
_ cloudprovider.Zones = (*Cloud)(nil)
|
||||
_ cloudprovider.PVLabeler = (*Cloud)(nil)
|
||||
)
|
||||
|
||||
// Cloud holds the config and clients
|
||||
type Cloud struct {
|
||||
Config
|
||||
Environment azure.Environment
|
||||
|
||||
RoutesClient routeclient.Interface
|
||||
SubnetsClient subnetclient.Interface
|
||||
InterfacesClient interfaceclient.Interface
|
||||
RouteTablesClient routetableclient.Interface
|
||||
LoadBalancerClient loadbalancerclient.Interface
|
||||
PublicIPAddressesClient publicipclient.Interface
|
||||
SecurityGroupsClient securitygroupclient.Interface
|
||||
VirtualMachinesClient vmclient.Interface
|
||||
StorageAccountClient storageaccountclient.Interface
|
||||
DisksClient diskclient.Interface
|
||||
SnapshotsClient snapshotclient.Interface
|
||||
FileClient fileclient.Interface
|
||||
VirtualMachineScaleSetsClient vmssclient.Interface
|
||||
VirtualMachineScaleSetVMsClient vmssvmclient.Interface
|
||||
VirtualMachineSizesClient vmsizeclient.Interface
|
||||
|
||||
ResourceRequestBackoff wait.Backoff
|
||||
metadata *InstanceMetadataService
|
||||
VMSet VMSet
|
||||
|
||||
// ipv6DualStack allows overriding for unit testing. It's normally initialized from featuregates
|
||||
ipv6DualStackEnabled bool
|
||||
// Lock for access to node caches, includes nodeZones, nodeResourceGroups, and unmanagedNodes.
|
||||
nodeCachesLock sync.RWMutex
|
||||
// nodeNames holds current nodes for tracking added nodes in VM caches.
|
||||
nodeNames sets.String
|
||||
// nodeZones is a mapping from Zone to a sets.String of Node's names in the Zone
|
||||
// it is updated by the nodeInformer
|
||||
nodeZones map[string]sets.String
|
||||
// nodeResourceGroups holds nodes external resource groups
|
||||
nodeResourceGroups map[string]string
|
||||
// unmanagedNodes holds a list of nodes not managed by Azure cloud provider.
|
||||
unmanagedNodes sets.String
|
||||
// excludeLoadBalancerNodes holds a list of nodes that should be excluded from LoadBalancer.
|
||||
excludeLoadBalancerNodes sets.String
|
||||
// nodeInformerSynced is for determining if the informer has synced.
|
||||
nodeInformerSynced cache.InformerSynced
|
||||
|
||||
// routeCIDRsLock holds lock for routeCIDRs cache.
|
||||
routeCIDRsLock sync.Mutex
|
||||
// routeCIDRs holds cache for route CIDRs.
|
||||
routeCIDRs map[string]string
|
||||
|
||||
KubeClient clientset.Interface
|
||||
eventBroadcaster record.EventBroadcaster
|
||||
eventRecorder record.EventRecorder
|
||||
routeUpdater *delayedRouteUpdater
|
||||
|
||||
vmCache *azcache.TimedCache
|
||||
lbCache *azcache.TimedCache
|
||||
nsgCache *azcache.TimedCache
|
||||
rtCache *azcache.TimedCache
|
||||
|
||||
*BlobDiskController
|
||||
*ManagedDiskController
|
||||
*controllerCommon
|
||||
}
|
||||
|
||||
func init() {
|
||||
// In go-autorest SDK https://github.com/Azure/go-autorest/blob/master/autorest/sender.go#L258-L287,
|
||||
// if ARM returns http.StatusTooManyRequests, the sender doesn't increase the retry attempt count,
|
||||
// hence the Azure clients will keep retrying forever until it get a status code other than 429.
|
||||
// So we explicitly removes http.StatusTooManyRequests from autorest.StatusCodesForRetry.
|
||||
// Refer https://github.com/Azure/go-autorest/issues/398.
|
||||
// TODO(feiskyer): Use autorest.SendDecorator to customize the retry policy when new Azure SDK is available.
|
||||
statusCodesForRetry := make([]int, 0)
|
||||
for _, code := range autorest.StatusCodesForRetry {
|
||||
if code != http.StatusTooManyRequests {
|
||||
statusCodesForRetry = append(statusCodesForRetry, code)
|
||||
}
|
||||
}
|
||||
autorest.StatusCodesForRetry = statusCodesForRetry
|
||||
|
||||
cloudprovider.RegisterCloudProvider(CloudProviderName, NewCloud)
|
||||
}
|
||||
|
||||
// NewCloud returns a Cloud with initialized clients
|
||||
func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) {
|
||||
az, err := NewCloudWithoutFeatureGates(configReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
az.ipv6DualStackEnabled = true
|
||||
|
||||
return az, nil
|
||||
}
|
||||
|
||||
// NewCloudWithoutFeatureGates returns a Cloud without trying to wire the feature gates. This is used by the unit tests
|
||||
// that don't load the actual features being used in the cluster.
|
||||
func NewCloudWithoutFeatureGates(configReader io.Reader) (*Cloud, error) {
|
||||
config, err := parseConfig(configReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
az := &Cloud{
|
||||
nodeNames: sets.NewString(),
|
||||
nodeZones: map[string]sets.String{},
|
||||
nodeResourceGroups: map[string]string{},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
excludeLoadBalancerNodes: sets.NewString(),
|
||||
routeCIDRs: map[string]string{},
|
||||
}
|
||||
|
||||
err = az.InitializeCloudFromConfig(config, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return az, nil
|
||||
}
|
||||
|
||||
// InitializeCloudFromConfig initializes the Cloud from config.
|
||||
func (az *Cloud) InitializeCloudFromConfig(config *Config, fromSecret bool) error {
|
||||
// cloud-config not set, return nil so that it would be initialized from secret.
|
||||
if config == nil {
|
||||
klog.Warning("cloud-config is not provided, Azure cloud provider would be initialized from secret")
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.RouteTableResourceGroup == "" {
|
||||
config.RouteTableResourceGroup = config.ResourceGroup
|
||||
}
|
||||
|
||||
if config.SecurityGroupResourceGroup == "" {
|
||||
config.SecurityGroupResourceGroup = config.ResourceGroup
|
||||
}
|
||||
|
||||
if config.VMType == "" {
|
||||
// default to standard vmType if not set.
|
||||
config.VMType = vmTypeStandard
|
||||
}
|
||||
|
||||
if config.DisableAvailabilitySetNodes && config.VMType != vmTypeVMSS {
|
||||
return fmt.Errorf("disableAvailabilitySetNodes %v is only supported when vmType is 'vmss'", config.DisableAvailabilitySetNodes)
|
||||
}
|
||||
|
||||
if config.CloudConfigType == "" {
|
||||
// The default cloud config type is cloudConfigTypeMerge.
|
||||
config.CloudConfigType = cloudConfigTypeMerge
|
||||
} else {
|
||||
supportedCloudConfigTypes := sets.NewString(
|
||||
string(cloudConfigTypeMerge),
|
||||
string(cloudConfigTypeFile),
|
||||
string(cloudConfigTypeSecret))
|
||||
if !supportedCloudConfigTypes.Has(string(config.CloudConfigType)) {
|
||||
return fmt.Errorf("cloudConfigType %v is not supported, supported values are %v", config.CloudConfigType, supportedCloudConfigTypes.List())
|
||||
}
|
||||
}
|
||||
|
||||
env, err := auth.ParseAzureEnvironment(config.Cloud, config.ResourceManagerEndpoint, config.IdentitySystem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
servicePrincipalToken, err := auth.GetServicePrincipalToken(&config.AzureAuthConfig, env)
|
||||
if err == auth.ErrorNoAuth {
|
||||
// Only controller-manager would lazy-initialize from secret, and credentials are required for such case.
|
||||
if fromSecret {
|
||||
err := fmt.Errorf("no credentials provided for Azure cloud provider")
|
||||
klog.Fatalf("%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// No credentials provided, useInstanceMetadata should be enabled for Kubelet.
|
||||
// TODO(feiskyer): print different error message for Kubelet and controller-manager, as they're
|
||||
// requiring different credential settings.
|
||||
if !config.UseInstanceMetadata && config.CloudConfigType == cloudConfigTypeFile {
|
||||
return fmt.Errorf("useInstanceMetadata must be enabled without Azure credentials")
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Azure cloud provider is starting without credentials")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize rate limiting config options.
|
||||
InitializeCloudProviderRateLimitConfig(&config.CloudProviderRateLimitConfig)
|
||||
|
||||
// Conditionally configure resource request backoff
|
||||
resourceRequestBackoff := wait.Backoff{
|
||||
Steps: 1,
|
||||
}
|
||||
if config.CloudProviderBackoff {
|
||||
// Assign backoff defaults if no configuration was passed in
|
||||
if config.CloudProviderBackoffRetries == 0 {
|
||||
config.CloudProviderBackoffRetries = backoffRetriesDefault
|
||||
}
|
||||
if config.CloudProviderBackoffDuration == 0 {
|
||||
config.CloudProviderBackoffDuration = backoffDurationDefault
|
||||
}
|
||||
if config.CloudProviderBackoffExponent == 0 {
|
||||
config.CloudProviderBackoffExponent = backoffExponentDefault
|
||||
}
|
||||
|
||||
if config.CloudProviderBackoffJitter == 0 {
|
||||
config.CloudProviderBackoffJitter = backoffJitterDefault
|
||||
}
|
||||
|
||||
resourceRequestBackoff = wait.Backoff{
|
||||
Steps: config.CloudProviderBackoffRetries,
|
||||
Factor: config.CloudProviderBackoffExponent,
|
||||
Duration: time.Duration(config.CloudProviderBackoffDuration) * time.Second,
|
||||
Jitter: config.CloudProviderBackoffJitter,
|
||||
}
|
||||
klog.V(2).Infof("Azure cloudprovider using try backoff: retries=%d, exponent=%f, duration=%d, jitter=%f",
|
||||
config.CloudProviderBackoffRetries,
|
||||
config.CloudProviderBackoffExponent,
|
||||
config.CloudProviderBackoffDuration,
|
||||
config.CloudProviderBackoffJitter)
|
||||
} else {
|
||||
// CloudProviderBackoffRetries will be set to 1 by default as the requirements of Azure SDK.
|
||||
config.CloudProviderBackoffRetries = 1
|
||||
config.CloudProviderBackoffDuration = backoffDurationDefault
|
||||
}
|
||||
|
||||
if strings.EqualFold(config.LoadBalancerSku, loadBalancerSkuStandard) {
|
||||
// Do not add master nodes to standard LB by default.
|
||||
if config.ExcludeMasterFromStandardLB == nil {
|
||||
config.ExcludeMasterFromStandardLB = &defaultExcludeMasterFromStandardLB
|
||||
}
|
||||
|
||||
// Enable outbound SNAT by default.
|
||||
if config.DisableOutboundSNAT == nil {
|
||||
config.DisableOutboundSNAT = &defaultDisableOutboundSNAT
|
||||
}
|
||||
} else {
|
||||
if config.DisableOutboundSNAT != nil && *config.DisableOutboundSNAT {
|
||||
return fmt.Errorf("disableOutboundSNAT should only set when loadBalancerSku is standard")
|
||||
}
|
||||
}
|
||||
|
||||
az.Config = *config
|
||||
az.Environment = *env
|
||||
az.ResourceRequestBackoff = resourceRequestBackoff
|
||||
az.metadata, err = NewInstanceMetadataService(imdsServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No credentials provided, InstanceMetadataService would be used for getting Azure resources.
|
||||
// Note that this only applies to Kubelet, controller-manager should configure credentials for managing Azure resources.
|
||||
if servicePrincipalToken == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If uses network resources in different AAD Tenant, then prepare corresponding Service Principal Token for VM/VMSS client and network resources client
|
||||
var multiTenantServicePrincipalToken *adal.MultiTenantServicePrincipalToken
|
||||
var networkResourceServicePrincipalToken *adal.ServicePrincipalToken
|
||||
if az.Config.UsesNetworkResourceInDifferentTenant() {
|
||||
multiTenantServicePrincipalToken, err = auth.GetMultiTenantServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
networkResourceServicePrincipalToken, err = auth.GetNetworkResourceServicePrincipalToken(&az.Config.AzureAuthConfig, &az.Environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
az.configAzureClients(servicePrincipalToken, multiTenantServicePrincipalToken, networkResourceServicePrincipalToken)
|
||||
|
||||
if az.MaximumLoadBalancerRuleCount == 0 {
|
||||
az.MaximumLoadBalancerRuleCount = maximumLoadBalancerRuleCount
|
||||
}
|
||||
|
||||
if strings.EqualFold(vmTypeVMSS, az.Config.VMType) {
|
||||
az.VMSet, err = newScaleSet(az)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
az.VMSet = newAvailabilitySet(az)
|
||||
}
|
||||
|
||||
az.vmCache, err = az.newVMCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
az.lbCache, err = az.newLBCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
az.nsgCache, err = az.newNSGCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
az.rtCache, err = az.newRouteTableCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initDiskControllers(az); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start delayed route updater.
|
||||
az.routeUpdater = newDelayedRouteUpdater(az, routeUpdateInterval)
|
||||
go az.routeUpdater.run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (az *Cloud) configAzureClients(
|
||||
servicePrincipalToken *adal.ServicePrincipalToken,
|
||||
multiTenantServicePrincipalToken *adal.MultiTenantServicePrincipalToken,
|
||||
networkResourceServicePrincipalToken *adal.ServicePrincipalToken) {
|
||||
azClientConfig := az.getAzureClientConfig(servicePrincipalToken)
|
||||
|
||||
// Prepare AzureClientConfig for all azure clients
|
||||
interfaceClientConfig := azClientConfig.WithRateLimiter(az.Config.InterfaceRateLimit)
|
||||
vmSizeClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineSizeRateLimit)
|
||||
snapshotClientConfig := azClientConfig.WithRateLimiter(az.Config.SnapshotRateLimit)
|
||||
storageAccountClientConfig := azClientConfig.WithRateLimiter(az.Config.StorageAccountRateLimit)
|
||||
diskClientConfig := azClientConfig.WithRateLimiter(az.Config.DiskRateLimit)
|
||||
vmClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineRateLimit)
|
||||
vmssClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineScaleSetRateLimit)
|
||||
// Error "not an active Virtual Machine Scale Set VM" is not retriable for VMSS VM.
|
||||
// But http.StatusNotFound is retriable because of ARM replication latency.
|
||||
vmssVMClientConfig := azClientConfig.WithRateLimiter(az.Config.VirtualMachineScaleSetRateLimit)
|
||||
vmssVMClientConfig.Backoff = vmssVMClientConfig.Backoff.WithNonRetriableErrors([]string{vmssVMNotActiveErrorMessage}).WithRetriableHTTPStatusCodes([]int{http.StatusNotFound})
|
||||
routeClientConfig := azClientConfig.WithRateLimiter(az.Config.RouteRateLimit)
|
||||
subnetClientConfig := azClientConfig.WithRateLimiter(az.Config.SubnetsRateLimit)
|
||||
routeTableClientConfig := azClientConfig.WithRateLimiter(az.Config.RouteTableRateLimit)
|
||||
loadBalancerClientConfig := azClientConfig.WithRateLimiter(az.Config.LoadBalancerRateLimit)
|
||||
securityGroupClientConfig := azClientConfig.WithRateLimiter(az.Config.SecurityGroupRateLimit)
|
||||
publicIPClientConfig := azClientConfig.WithRateLimiter(az.Config.PublicIPAddressRateLimit)
|
||||
// TODO(ZeroMagic): add azurefileRateLimit
|
||||
fileClientConfig := azClientConfig.WithRateLimiter(nil)
|
||||
|
||||
// If uses network resources in different AAD Tenant, update Authorizer for VM/VMSS client config
|
||||
if multiTenantServicePrincipalToken != nil {
|
||||
multiTenantServicePrincipalTokenAuthorizer := autorest.NewMultiTenantServicePrincipalTokenAuthorizer(multiTenantServicePrincipalToken)
|
||||
vmClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer
|
||||
vmssClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer
|
||||
vmssVMClientConfig.Authorizer = multiTenantServicePrincipalTokenAuthorizer
|
||||
}
|
||||
|
||||
// If uses network resources in different AAD Tenant, update SubscriptionID and Authorizer for network resources client config
|
||||
if networkResourceServicePrincipalToken != nil {
|
||||
networkResourceServicePrincipalTokenAuthorizer := autorest.NewBearerAuthorizer(networkResourceServicePrincipalToken)
|
||||
routeClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
subnetClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
routeTableClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
loadBalancerClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
securityGroupClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
publicIPClientConfig.Authorizer = networkResourceServicePrincipalTokenAuthorizer
|
||||
|
||||
routeClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
subnetClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
routeTableClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
loadBalancerClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
securityGroupClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
publicIPClientConfig.SubscriptionID = az.Config.NetworkResourceSubscriptionID
|
||||
}
|
||||
|
||||
// Initialize all azure clients based on client config
|
||||
az.InterfacesClient = interfaceclient.New(interfaceClientConfig)
|
||||
az.VirtualMachineSizesClient = vmsizeclient.New(vmSizeClientConfig)
|
||||
az.SnapshotsClient = snapshotclient.New(snapshotClientConfig)
|
||||
az.StorageAccountClient = storageaccountclient.New(storageAccountClientConfig)
|
||||
az.DisksClient = diskclient.New(diskClientConfig)
|
||||
az.VirtualMachinesClient = vmclient.New(vmClientConfig)
|
||||
az.VirtualMachineScaleSetsClient = vmssclient.New(vmssClientConfig)
|
||||
az.VirtualMachineScaleSetVMsClient = vmssvmclient.New(vmssVMClientConfig)
|
||||
az.RoutesClient = routeclient.New(routeClientConfig)
|
||||
az.SubnetsClient = subnetclient.New(subnetClientConfig)
|
||||
az.RouteTablesClient = routetableclient.New(routeTableClientConfig)
|
||||
az.LoadBalancerClient = loadbalancerclient.New(loadBalancerClientConfig)
|
||||
az.SecurityGroupsClient = securitygroupclient.New(securityGroupClientConfig)
|
||||
az.PublicIPAddressesClient = publicipclient.New(publicIPClientConfig)
|
||||
az.FileClient = fileclient.New(fileClientConfig)
|
||||
}
|
||||
|
||||
func (az *Cloud) getAzureClientConfig(servicePrincipalToken *adal.ServicePrincipalToken) *azclients.ClientConfig {
|
||||
azClientConfig := &azclients.ClientConfig{
|
||||
CloudName: az.Config.Cloud,
|
||||
Location: az.Config.Location,
|
||||
SubscriptionID: az.Config.SubscriptionID,
|
||||
ResourceManagerEndpoint: az.Environment.ResourceManagerEndpoint,
|
||||
Authorizer: autorest.NewBearerAuthorizer(servicePrincipalToken),
|
||||
Backoff: &retry.Backoff{Steps: 1},
|
||||
}
|
||||
|
||||
if az.Config.CloudProviderBackoff {
|
||||
azClientConfig.Backoff = &retry.Backoff{
|
||||
Steps: az.Config.CloudProviderBackoffRetries,
|
||||
Factor: az.Config.CloudProviderBackoffExponent,
|
||||
Duration: time.Duration(az.Config.CloudProviderBackoffDuration) * time.Second,
|
||||
Jitter: az.Config.CloudProviderBackoffJitter,
|
||||
}
|
||||
}
|
||||
|
||||
return azClientConfig
|
||||
}
|
||||
|
||||
// parseConfig returns a parsed configuration for an Azure cloudprovider config file
|
||||
func parseConfig(configReader io.Reader) (*Config, error) {
|
||||
var config Config
|
||||
if configReader == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
configContents, err := ioutil.ReadAll(configReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(configContents, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The resource group name may be in different cases from different Azure APIs, hence it is converted to lower here.
|
||||
// See more context at https://github.com/kubernetes/kubernetes/issues/71994.
|
||||
config.ResourceGroup = strings.ToLower(config.ResourceGroup)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
|
||||
func (az *Cloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
|
||||
az.KubeClient = clientBuilder.ClientOrDie("azure-cloud-provider")
|
||||
az.eventBroadcaster = record.NewBroadcaster()
|
||||
az.eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: az.KubeClient.CoreV1().Events("")})
|
||||
az.eventRecorder = az.eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "azure-cloud-provider"})
|
||||
az.InitializeCloudFromSecret()
|
||||
}
|
||||
|
||||
// LoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise.
|
||||
func (az *Cloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
|
||||
return az, true
|
||||
}
|
||||
|
||||
// Instances returns an instances interface. Also returns true if the interface is supported, false otherwise.
|
||||
func (az *Cloud) Instances() (cloudprovider.Instances, bool) {
|
||||
return az, true
|
||||
}
|
||||
|
||||
// InstancesV2 returns an instancesV2 interface. Also returns true if the interface is supported, false otherwise.
|
||||
// TODO: implement ONLY for external cloud provider
|
||||
func (az *Cloud) InstancesV2() (cloudprovider.InstancesV2, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Zones returns a zones interface. Also returns true if the interface is supported, false otherwise.
|
||||
func (az *Cloud) Zones() (cloudprovider.Zones, bool) {
|
||||
return az, true
|
||||
}
|
||||
|
||||
// Clusters returns a clusters interface. Also returns true if the interface is supported, false otherwise.
|
||||
func (az *Cloud) Clusters() (cloudprovider.Clusters, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Routes returns a routes interface along with whether the interface is supported.
|
||||
func (az *Cloud) Routes() (cloudprovider.Routes, bool) {
|
||||
return az, true
|
||||
}
|
||||
|
||||
// HasClusterID returns true if the cluster has a clusterID
|
||||
func (az *Cloud) HasClusterID() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProviderName returns the cloud provider ID.
|
||||
func (az *Cloud) ProviderName() string {
|
||||
return CloudProviderName
|
||||
}
|
||||
|
||||
func initDiskControllers(az *Cloud) error {
|
||||
// Common controller contains the function
|
||||
// needed by both blob disk and managed disk controllers
|
||||
|
||||
common := &controllerCommon{
|
||||
location: az.Location,
|
||||
storageEndpointSuffix: az.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: az.ResourceGroup,
|
||||
subscriptionID: az.SubscriptionID,
|
||||
cloud: az,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
|
||||
az.BlobDiskController = &BlobDiskController{common: common}
|
||||
az.ManagedDiskController = &ManagedDiskController{common: common}
|
||||
az.controllerCommon = common
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetInformers sets informers for Azure cloud provider.
|
||||
func (az *Cloud) SetInformers(informerFactory informers.SharedInformerFactory) {
|
||||
klog.Infof("Setting up informers for Azure cloud provider")
|
||||
nodeInformer := informerFactory.Core().V1().Nodes().Informer()
|
||||
nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
node := obj.(*v1.Node)
|
||||
az.updateNodeCaches(nil, node)
|
||||
},
|
||||
UpdateFunc: func(prev, obj interface{}) {
|
||||
prevNode := prev.(*v1.Node)
|
||||
newNode := obj.(*v1.Node)
|
||||
az.updateNodeCaches(prevNode, newNode)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
node, isNode := obj.(*v1.Node)
|
||||
// We can get DeletedFinalStateUnknown instead of *v1.Node here
|
||||
// and we need to handle that correctly.
|
||||
if !isNode {
|
||||
deletedState, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
klog.Errorf("Received unexpected object: %v", obj)
|
||||
return
|
||||
}
|
||||
node, ok = deletedState.Obj.(*v1.Node)
|
||||
if !ok {
|
||||
klog.Errorf("DeletedFinalStateUnknown contained non-Node object: %v", deletedState.Obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
az.updateNodeCaches(node, nil)
|
||||
},
|
||||
})
|
||||
az.nodeInformerSynced = nodeInformer.HasSynced
|
||||
}
|
||||
|
||||
// updateNodeCaches updates local cache for node's zones and external resource groups.
|
||||
func (az *Cloud) updateNodeCaches(prevNode, newNode *v1.Node) {
|
||||
az.nodeCachesLock.Lock()
|
||||
defer az.nodeCachesLock.Unlock()
|
||||
|
||||
if prevNode != nil {
|
||||
|
||||
// Remove from nodeNames cache.
|
||||
az.nodeNames.Delete(prevNode.ObjectMeta.Name)
|
||||
|
||||
// Remove from nodeZones cache
|
||||
prevZone, ok := prevNode.ObjectMeta.Labels[v1.LabelTopologyZone]
|
||||
|
||||
if ok && az.isAvailabilityZone(prevZone) {
|
||||
az.nodeZones[prevZone].Delete(prevNode.ObjectMeta.Name)
|
||||
if az.nodeZones[prevZone].Len() == 0 {
|
||||
az.nodeZones[prevZone] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from nodeZones cache if using deprecated LabelFailureDomainBetaZone
|
||||
prevZoneFailureDomain, ok := prevNode.ObjectMeta.Labels[v1.LabelFailureDomainBetaZone]
|
||||
if ok && az.isAvailabilityZone(prevZoneFailureDomain) {
|
||||
az.nodeZones[prevZone].Delete(prevNode.ObjectMeta.Name)
|
||||
if az.nodeZones[prevZone].Len() == 0 {
|
||||
az.nodeZones[prevZone] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from nodeResourceGroups cache.
|
||||
_, ok = prevNode.ObjectMeta.Labels[externalResourceGroupLabel]
|
||||
if ok {
|
||||
delete(az.nodeResourceGroups, prevNode.ObjectMeta.Name)
|
||||
}
|
||||
|
||||
managed, ok := prevNode.ObjectMeta.Labels[managedByAzureLabel]
|
||||
isNodeManagedByCloudProvider := !ok || managed != "false"
|
||||
|
||||
// Remove from unmanagedNodes cache
|
||||
if !isNodeManagedByCloudProvider {
|
||||
az.unmanagedNodes.Delete(prevNode.ObjectMeta.Name)
|
||||
}
|
||||
|
||||
// if the node is being deleted from the cluster, exclude it from load balancers
|
||||
if newNode == nil {
|
||||
az.excludeLoadBalancerNodes.Insert(prevNode.ObjectMeta.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if newNode != nil {
|
||||
// Add to nodeNames cache.
|
||||
az.nodeNames.Insert(newNode.ObjectMeta.Name)
|
||||
|
||||
// Add to nodeZones cache.
|
||||
newZone, ok := newNode.ObjectMeta.Labels[v1.LabelTopologyZone]
|
||||
if ok && az.isAvailabilityZone(newZone) {
|
||||
if az.nodeZones[newZone] == nil {
|
||||
az.nodeZones[newZone] = sets.NewString()
|
||||
}
|
||||
az.nodeZones[newZone].Insert(newNode.ObjectMeta.Name)
|
||||
}
|
||||
|
||||
// Add to nodeResourceGroups cache.
|
||||
newRG, ok := newNode.ObjectMeta.Labels[externalResourceGroupLabel]
|
||||
if ok && len(newRG) > 0 {
|
||||
az.nodeResourceGroups[newNode.ObjectMeta.Name] = strings.ToLower(newRG)
|
||||
}
|
||||
|
||||
_, hasExcludeBalancerLabel := newNode.ObjectMeta.Labels[v1.LabelNodeExcludeBalancers]
|
||||
managed, ok := newNode.ObjectMeta.Labels[managedByAzureLabel]
|
||||
isNodeManagedByCloudProvider := !ok || managed != "false"
|
||||
|
||||
// Update unmanagedNodes cache
|
||||
if !isNodeManagedByCloudProvider {
|
||||
az.unmanagedNodes.Insert(newNode.ObjectMeta.Name)
|
||||
}
|
||||
|
||||
// Update excludeLoadBalancerNodes cache
|
||||
switch {
|
||||
case !isNodeManagedByCloudProvider:
|
||||
az.excludeLoadBalancerNodes.Insert(newNode.ObjectMeta.Name)
|
||||
|
||||
case hasExcludeBalancerLabel:
|
||||
az.excludeLoadBalancerNodes.Insert(newNode.ObjectMeta.Name)
|
||||
|
||||
default:
|
||||
// Nodes not falling into the three cases above are valid backends and
|
||||
// should not appear in excludeLoadBalancerNodes cache.
|
||||
az.excludeLoadBalancerNodes.Delete(newNode.ObjectMeta.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveZones returns all the zones in which k8s nodes are currently running.
|
||||
func (az *Cloud) GetActiveZones() (sets.String, error) {
|
||||
if az.nodeInformerSynced == nil {
|
||||
return nil, fmt.Errorf("azure cloud provider doesn't have informers set")
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return nil, fmt.Errorf("node informer is not synced when trying to GetActiveZones")
|
||||
}
|
||||
|
||||
zones := sets.NewString()
|
||||
for zone, nodes := range az.nodeZones {
|
||||
if len(nodes) > 0 {
|
||||
zones.Insert(zone)
|
||||
}
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
// GetLocation returns the location in which k8s cluster is currently running.
|
||||
func (az *Cloud) GetLocation() string {
|
||||
return az.Location
|
||||
}
|
||||
|
||||
// GetNodeResourceGroup gets resource group for given node.
|
||||
func (az *Cloud) GetNodeResourceGroup(nodeName string) (string, error) {
|
||||
// Kubelet won't set az.nodeInformerSynced, always return configured resourceGroup.
|
||||
if az.nodeInformerSynced == nil {
|
||||
return az.ResourceGroup, nil
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return "", fmt.Errorf("node informer is not synced when trying to GetNodeResourceGroup")
|
||||
}
|
||||
|
||||
// Return external resource group if it has been cached.
|
||||
if cachedRG, ok := az.nodeResourceGroups[nodeName]; ok {
|
||||
return cachedRG, nil
|
||||
}
|
||||
|
||||
// Return resource group from cloud provider options.
|
||||
return az.ResourceGroup, nil
|
||||
}
|
||||
|
||||
// GetNodeNames returns a set of all node names in the k8s cluster.
|
||||
func (az *Cloud) GetNodeNames() (sets.String, error) {
|
||||
// Kubelet won't set az.nodeInformerSynced, return nil.
|
||||
if az.nodeInformerSynced == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return nil, fmt.Errorf("node informer is not synced when trying to GetNodeNames")
|
||||
}
|
||||
|
||||
return sets.NewString(az.nodeNames.List()...), nil
|
||||
}
|
||||
|
||||
// GetResourceGroups returns a set of resource groups that all nodes are running on.
|
||||
func (az *Cloud) GetResourceGroups() (sets.String, error) {
|
||||
// Kubelet won't set az.nodeInformerSynced, always return configured resourceGroup.
|
||||
if az.nodeInformerSynced == nil {
|
||||
return sets.NewString(az.ResourceGroup), nil
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return nil, fmt.Errorf("node informer is not synced when trying to GetResourceGroups")
|
||||
}
|
||||
|
||||
resourceGroups := sets.NewString(az.ResourceGroup)
|
||||
for _, rg := range az.nodeResourceGroups {
|
||||
resourceGroups.Insert(rg)
|
||||
}
|
||||
|
||||
return resourceGroups, nil
|
||||
}
|
||||
|
||||
// GetUnmanagedNodes returns a list of nodes not managed by Azure cloud provider (e.g. on-prem nodes).
|
||||
func (az *Cloud) GetUnmanagedNodes() (sets.String, error) {
|
||||
// Kubelet won't set az.nodeInformerSynced, always return nil.
|
||||
if az.nodeInformerSynced == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return nil, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes")
|
||||
}
|
||||
|
||||
return sets.NewString(az.unmanagedNodes.List()...), nil
|
||||
}
|
||||
|
||||
// ShouldNodeExcludedFromLoadBalancer returns true if node is unmanaged, in external resource group or labeled with "node.kubernetes.io/exclude-from-external-load-balancers".
|
||||
func (az *Cloud) ShouldNodeExcludedFromLoadBalancer(nodeName string) (bool, error) {
|
||||
// Kubelet won't set az.nodeInformerSynced, always return nil.
|
||||
if az.nodeInformerSynced == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
az.nodeCachesLock.RLock()
|
||||
defer az.nodeCachesLock.RUnlock()
|
||||
if !az.nodeInformerSynced() {
|
||||
return false, fmt.Errorf("node informer is not synced when trying to fetch node caches")
|
||||
}
|
||||
|
||||
// Return true if the node is in external resource group.
|
||||
if cachedRG, ok := az.nodeResourceGroups[nodeName]; ok && !strings.EqualFold(cachedRG, az.ResourceGroup) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return az.excludeLoadBalancerNodes.Has(nodeName), nil
|
||||
}
|
||||
@@ -1,470 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const (
|
||||
// not active means the instance is under deleting from Azure VMSS.
|
||||
vmssVMNotActiveErrorMessage = "not an active Virtual Machine Scale Set VM instanceId"
|
||||
|
||||
// operationCanceledErrorMessage means the operation is canceled by another new operation.
|
||||
operationCanceledErrorMessage = "canceledandsupersededduetoanotheroperation"
|
||||
|
||||
cannotDeletePublicIPErrorMessageCode = "PublicIPAddressCannotBeDeleted"
|
||||
|
||||
referencedResourceNotProvisionedMessageCode = "ReferencedResourceNotProvisioned"
|
||||
)
|
||||
|
||||
var (
|
||||
pipErrorMessageRE = regexp.MustCompile(`(?:.*)/subscriptions/(?:.*)/resourceGroups/(.*)/providers/Microsoft.Network/publicIPAddresses/([^\s]+)(?:.*)`)
|
||||
)
|
||||
|
||||
// RequestBackoff if backoff is disabled in cloud provider it
|
||||
// returns a new Backoff object steps = 1
|
||||
// This is to make sure that the requested command executes
|
||||
// at least once
|
||||
func (az *Cloud) RequestBackoff() (resourceRequestBackoff wait.Backoff) {
|
||||
if az.CloudProviderBackoff {
|
||||
return az.ResourceRequestBackoff
|
||||
}
|
||||
resourceRequestBackoff = wait.Backoff{
|
||||
Steps: 1,
|
||||
}
|
||||
return resourceRequestBackoff
|
||||
}
|
||||
|
||||
// Event creates a event for the specified object.
|
||||
func (az *Cloud) Event(obj runtime.Object, eventType, reason, message string) {
|
||||
if obj != nil && reason != "" {
|
||||
az.eventRecorder.Event(obj, eventType, reason, message)
|
||||
}
|
||||
}
|
||||
|
||||
// GetVirtualMachineWithRetry invokes az.getVirtualMachine with exponential backoff retry
|
||||
func (az *Cloud) GetVirtualMachineWithRetry(name types.NodeName, crt azcache.AzureCacheReadType) (compute.VirtualMachine, error) {
|
||||
var machine compute.VirtualMachine
|
||||
var retryErr error
|
||||
err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) {
|
||||
machine, retryErr = az.getVirtualMachine(name, crt)
|
||||
if retryErr == cloudprovider.InstanceNotFound {
|
||||
return true, cloudprovider.InstanceNotFound
|
||||
}
|
||||
if retryErr != nil {
|
||||
klog.Errorf("GetVirtualMachineWithRetry(%s): backoff failure, will retry, err=%v", name, retryErr)
|
||||
return false, nil
|
||||
}
|
||||
klog.V(2).Infof("GetVirtualMachineWithRetry(%s): backoff success", name)
|
||||
return true, nil
|
||||
})
|
||||
if err == wait.ErrWaitTimeout {
|
||||
err = retryErr
|
||||
}
|
||||
return machine, err
|
||||
}
|
||||
|
||||
// ListVirtualMachines invokes az.VirtualMachinesClient.List with exponential backoff retry
|
||||
func (az *Cloud) ListVirtualMachines(resourceGroup string) ([]compute.VirtualMachine, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
allNodes, rerr := az.VirtualMachinesClient.List(ctx, resourceGroup)
|
||||
if rerr != nil {
|
||||
klog.Errorf("VirtualMachinesClient.List(%v) failure with err=%v", resourceGroup, rerr)
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
klog.V(2).Infof("VirtualMachinesClient.List(%v) success", resourceGroup)
|
||||
return allNodes, nil
|
||||
}
|
||||
|
||||
// getPrivateIPsForMachine is wrapper for optional backoff getting private ips
|
||||
// list of a node by name
|
||||
func (az *Cloud) getPrivateIPsForMachine(nodeName types.NodeName) ([]string, error) {
|
||||
return az.getPrivateIPsForMachineWithRetry(nodeName)
|
||||
}
|
||||
|
||||
func (az *Cloud) getPrivateIPsForMachineWithRetry(nodeName types.NodeName) ([]string, error) {
|
||||
var privateIPs []string
|
||||
err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) {
|
||||
var retryErr error
|
||||
privateIPs, retryErr = az.VMSet.GetPrivateIPsByNodeName(string(nodeName))
|
||||
if retryErr != nil {
|
||||
// won't retry since the instance doesn't exist on Azure.
|
||||
if retryErr == cloudprovider.InstanceNotFound {
|
||||
return true, retryErr
|
||||
}
|
||||
klog.Errorf("GetPrivateIPsByNodeName(%s): backoff failure, will retry,err=%v", nodeName, retryErr)
|
||||
return false, nil
|
||||
}
|
||||
klog.V(3).Infof("GetPrivateIPsByNodeName(%s): backoff success", nodeName)
|
||||
return true, nil
|
||||
})
|
||||
return privateIPs, err
|
||||
}
|
||||
|
||||
func (az *Cloud) getIPForMachine(nodeName types.NodeName) (string, string, error) {
|
||||
return az.GetIPForMachineWithRetry(nodeName)
|
||||
}
|
||||
|
||||
// GetIPForMachineWithRetry invokes az.getIPForMachine with exponential backoff retry
|
||||
func (az *Cloud) GetIPForMachineWithRetry(name types.NodeName) (string, string, error) {
|
||||
var ip, publicIP string
|
||||
err := wait.ExponentialBackoff(az.RequestBackoff(), func() (bool, error) {
|
||||
var retryErr error
|
||||
ip, publicIP, retryErr = az.VMSet.GetIPByNodeName(string(name))
|
||||
if retryErr != nil {
|
||||
klog.Errorf("GetIPForMachineWithRetry(%s): backoff failure, will retry,err=%v", name, retryErr)
|
||||
return false, nil
|
||||
}
|
||||
klog.V(3).Infof("GetIPForMachineWithRetry(%s): backoff success", name)
|
||||
return true, nil
|
||||
})
|
||||
return ip, publicIP, err
|
||||
}
|
||||
|
||||
// CreateOrUpdateSecurityGroup invokes az.SecurityGroupsClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdateSecurityGroup(sg network.SecurityGroup) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.SecurityGroupsClient.CreateOrUpdate(ctx, az.SecurityGroupResourceGroup, *sg.Name, sg, pointer.StringDeref(sg.Etag, ""))
|
||||
klog.V(10).Infof("SecurityGroupsClient.CreateOrUpdate(%s): end", *sg.Name)
|
||||
if rerr == nil {
|
||||
// Invalidate the cache right after updating
|
||||
az.nsgCache.Delete(*sg.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate the cache because ETAG precondition mismatch.
|
||||
if rerr.HTTPStatusCode == http.StatusPreconditionFailed {
|
||||
klog.V(3).Infof("SecurityGroup cache for %s is cleanup because of http.StatusPreconditionFailed", *sg.Name)
|
||||
az.nsgCache.Delete(*sg.Name)
|
||||
}
|
||||
|
||||
// Invalidate the cache because another new operation has canceled the current request.
|
||||
if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) {
|
||||
klog.V(3).Infof("SecurityGroup cache for %s is cleanup because CreateOrUpdateSecurityGroup is canceled by another operation", *sg.Name)
|
||||
az.nsgCache.Delete(*sg.Name)
|
||||
}
|
||||
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
func cleanupSubnetInFrontendIPConfigurations(lb *network.LoadBalancer) network.LoadBalancer {
|
||||
if lb.LoadBalancerPropertiesFormat == nil || lb.FrontendIPConfigurations == nil {
|
||||
return *lb
|
||||
}
|
||||
|
||||
frontendIPConfigurations := *lb.FrontendIPConfigurations
|
||||
for i := range frontendIPConfigurations {
|
||||
config := frontendIPConfigurations[i]
|
||||
if config.FrontendIPConfigurationPropertiesFormat != nil &&
|
||||
config.Subnet != nil &&
|
||||
config.Subnet.ID != nil {
|
||||
subnet := network.Subnet{
|
||||
ID: config.Subnet.ID,
|
||||
}
|
||||
if config.Subnet.Name != nil {
|
||||
subnet.Name = config.FrontendIPConfigurationPropertiesFormat.Subnet.Name
|
||||
}
|
||||
config.FrontendIPConfigurationPropertiesFormat.Subnet = &subnet
|
||||
frontendIPConfigurations[i] = config
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
lb.FrontendIPConfigurations = &frontendIPConfigurations
|
||||
return *lb
|
||||
}
|
||||
|
||||
// CreateOrUpdateLB invokes az.LoadBalancerClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdateLB(service *v1.Service, lb network.LoadBalancer) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
lb = cleanupSubnetInFrontendIPConfigurations(&lb)
|
||||
|
||||
rgName := az.getLoadBalancerResourceGroup()
|
||||
rerr := az.LoadBalancerClient.CreateOrUpdate(ctx, rgName, pointer.StringDeref(lb.Name, ""), lb, pointer.StringDeref(lb.Etag, ""))
|
||||
klog.V(10).Infof("LoadBalancerClient.CreateOrUpdate(%s): end", *lb.Name)
|
||||
if rerr == nil {
|
||||
// Invalidate the cache right after updating
|
||||
az.lbCache.Delete(*lb.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate the cache because ETAG precondition mismatch.
|
||||
if rerr.HTTPStatusCode == http.StatusPreconditionFailed {
|
||||
klog.V(3).Infof("LoadBalancer cache for %s is cleanup because of http.StatusPreconditionFailed", pointer.StringDeref(lb.Name, ""))
|
||||
az.lbCache.Delete(*lb.Name)
|
||||
}
|
||||
|
||||
retryErrorMessage := rerr.Error().Error()
|
||||
// Invalidate the cache because another new operation has canceled the current request.
|
||||
if strings.Contains(strings.ToLower(retryErrorMessage), operationCanceledErrorMessage) {
|
||||
klog.V(3).Infof("LoadBalancer cache for %s is cleanup because CreateOrUpdate is canceled by another operation", pointer.StringDeref(lb.Name, ""))
|
||||
az.lbCache.Delete(*lb.Name)
|
||||
}
|
||||
|
||||
// The LB update may fail because the referenced PIP is not in the Succeeded provisioning state
|
||||
if strings.Contains(strings.ToLower(retryErrorMessage), strings.ToLower(referencedResourceNotProvisionedMessageCode)) {
|
||||
matches := pipErrorMessageRE.FindStringSubmatch(retryErrorMessage)
|
||||
if len(matches) != 3 {
|
||||
klog.Warningf("Failed to parse the retry error message %s", retryErrorMessage)
|
||||
return rerr.Error()
|
||||
}
|
||||
pipRG, pipName := matches[1], matches[2]
|
||||
klog.V(3).Infof("The public IP %s referenced by load balancer %s is not in Succeeded provisioning state, will try to update it", pipName, pointer.StringDeref(lb.Name, ""))
|
||||
pip, _, err := az.getPublicIPAddress(pipRG, pipName)
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to get the public IP %s in resource group %s: %v", pipName, pipRG, err)
|
||||
return rerr.Error()
|
||||
}
|
||||
// Perform a dummy update to fix the provisioning state
|
||||
err = az.CreateOrUpdatePIP(service, pipRG, pip)
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to update the public IP %s in resource group %s: %v", pipName, pipRG, err)
|
||||
return rerr.Error()
|
||||
}
|
||||
// Invalidate the LB cache, return the error, and the controller manager
|
||||
// would retry the LB update in the next reconcile loop
|
||||
az.lbCache.Delete(*lb.Name)
|
||||
}
|
||||
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
// ListLB invokes az.LoadBalancerClient.List with exponential backoff retry
|
||||
func (az *Cloud) ListLB(service *v1.Service) ([]network.LoadBalancer, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rgName := az.getLoadBalancerResourceGroup()
|
||||
allLBs, rerr := az.LoadBalancerClient.List(ctx, rgName)
|
||||
if rerr != nil {
|
||||
if rerr.IsNotFound() {
|
||||
return nil, nil
|
||||
}
|
||||
az.Event(service, v1.EventTypeWarning, "ListLoadBalancers", rerr.Error().Error())
|
||||
klog.Errorf("LoadBalancerClient.List(%v) failure with err=%v", rgName, rerr)
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
klog.V(2).Infof("LoadBalancerClient.List(%v) success", rgName)
|
||||
return allLBs, nil
|
||||
}
|
||||
|
||||
// ListPIP list the PIP resources in the given resource group
|
||||
func (az *Cloud) ListPIP(service *v1.Service, pipResourceGroup string) ([]network.PublicIPAddress, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
allPIPs, rerr := az.PublicIPAddressesClient.List(ctx, pipResourceGroup)
|
||||
if rerr != nil {
|
||||
if rerr.IsNotFound() {
|
||||
return nil, nil
|
||||
}
|
||||
az.Event(service, v1.EventTypeWarning, "ListPublicIPs", rerr.Error().Error())
|
||||
klog.Errorf("PublicIPAddressesClient.List(%v) failure with err=%v", pipResourceGroup, rerr)
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
klog.V(2).Infof("PublicIPAddressesClient.List(%v) success", pipResourceGroup)
|
||||
return allPIPs, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdatePIP invokes az.PublicIPAddressesClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdatePIP(service *v1.Service, pipResourceGroup string, pip network.PublicIPAddress) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.PublicIPAddressesClient.CreateOrUpdate(ctx, pipResourceGroup, pointer.StringDeref(pip.Name, ""), pip)
|
||||
klog.V(10).Infof("PublicIPAddressesClient.CreateOrUpdate(%s, %s): end", pipResourceGroup, pointer.StringDeref(pip.Name, ""))
|
||||
if rerr != nil {
|
||||
klog.Errorf("PublicIPAddressesClient.CreateOrUpdate(%s, %s) failed: %s", pipResourceGroup, pointer.StringDeref(pip.Name, ""), rerr.Error().Error())
|
||||
az.Event(service, v1.EventTypeWarning, "CreateOrUpdatePublicIPAddress", rerr.Error().Error())
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateInterface invokes az.PublicIPAddressesClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdateInterface(service *v1.Service, nic network.Interface) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.InterfacesClient.CreateOrUpdate(ctx, az.ResourceGroup, *nic.Name, nic)
|
||||
klog.V(10).Infof("InterfacesClient.CreateOrUpdate(%s): end", *nic.Name)
|
||||
if rerr != nil {
|
||||
klog.Errorf("InterfacesClient.CreateOrUpdate(%s) failed: %s", *nic.Name, rerr.Error().Error())
|
||||
az.Event(service, v1.EventTypeWarning, "CreateOrUpdateInterface", rerr.Error().Error())
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePublicIP invokes az.PublicIPAddressesClient.Delete with exponential backoff retry
|
||||
func (az *Cloud) DeletePublicIP(service *v1.Service, pipResourceGroup string, pipName string) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.PublicIPAddressesClient.Delete(ctx, pipResourceGroup, pipName)
|
||||
if rerr != nil {
|
||||
klog.Errorf("PublicIPAddressesClient.Delete(%s) failed: %s", pipName, rerr.Error().Error())
|
||||
az.Event(service, v1.EventTypeWarning, "DeletePublicIPAddress", rerr.Error().Error())
|
||||
|
||||
if strings.Contains(rerr.Error().Error(), cannotDeletePublicIPErrorMessageCode) {
|
||||
klog.Warningf("DeletePublicIP for public IP %s failed with error %v, this is because other resources are referencing the public IP. The deletion of the service will continue.", pipName, rerr.Error())
|
||||
return nil
|
||||
}
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteLB invokes az.LoadBalancerClient.Delete with exponential backoff retry
|
||||
func (az *Cloud) DeleteLB(service *v1.Service, lbName string) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rgName := az.getLoadBalancerResourceGroup()
|
||||
rerr := az.LoadBalancerClient.Delete(ctx, rgName, lbName)
|
||||
if rerr == nil {
|
||||
// Invalidate the cache right after updating
|
||||
az.lbCache.Delete(lbName)
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.Errorf("LoadBalancerClient.Delete(%s) failed: %s", lbName, rerr.Error().Error())
|
||||
az.Event(service, v1.EventTypeWarning, "DeleteLoadBalancer", rerr.Error().Error())
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
// CreateOrUpdateRouteTable invokes az.RouteTablesClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdateRouteTable(routeTable network.RouteTable) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.RouteTablesClient.CreateOrUpdate(ctx, az.RouteTableResourceGroup, az.RouteTableName, routeTable, pointer.StringDeref(routeTable.Etag, ""))
|
||||
if rerr == nil {
|
||||
// Invalidate the cache right after updating
|
||||
az.rtCache.Delete(*routeTable.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate the cache because etag mismatch.
|
||||
if rerr.HTTPStatusCode == http.StatusPreconditionFailed {
|
||||
klog.V(3).Infof("Route table cache for %s is cleanup because of http.StatusPreconditionFailed", *routeTable.Name)
|
||||
az.rtCache.Delete(*routeTable.Name)
|
||||
}
|
||||
// Invalidate the cache because another new operation has canceled the current request.
|
||||
if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) {
|
||||
klog.V(3).Infof("Route table cache for %s is cleanup because CreateOrUpdateRouteTable is canceled by another operation", *routeTable.Name)
|
||||
az.rtCache.Delete(*routeTable.Name)
|
||||
}
|
||||
klog.Errorf("RouteTablesClient.CreateOrUpdate(%s) failed: %v", az.RouteTableName, rerr.Error())
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
// CreateOrUpdateRoute invokes az.RoutesClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) CreateOrUpdateRoute(route network.Route) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.RoutesClient.CreateOrUpdate(ctx, az.RouteTableResourceGroup, az.RouteTableName, *route.Name, route, pointer.StringDeref(route.Etag, ""))
|
||||
klog.V(10).Infof("RoutesClient.CreateOrUpdate(%s): end", *route.Name)
|
||||
if rerr == nil {
|
||||
az.rtCache.Delete(az.RouteTableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
if rerr.HTTPStatusCode == http.StatusPreconditionFailed {
|
||||
klog.V(3).Infof("Route cache for %s is cleanup because of http.StatusPreconditionFailed", *route.Name)
|
||||
az.rtCache.Delete(az.RouteTableName)
|
||||
}
|
||||
// Invalidate the cache because another new operation has canceled the current request.
|
||||
if strings.Contains(strings.ToLower(rerr.Error().Error()), operationCanceledErrorMessage) {
|
||||
klog.V(3).Infof("Route cache for %s is cleanup because CreateOrUpdateRouteTable is canceled by another operation", *route.Name)
|
||||
az.rtCache.Delete(az.RouteTableName)
|
||||
}
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
// DeleteRouteWithName invokes az.RoutesClient.CreateOrUpdate with exponential backoff retry
|
||||
func (az *Cloud) DeleteRouteWithName(routeName string) error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
rerr := az.RoutesClient.Delete(ctx, az.RouteTableResourceGroup, az.RouteTableName, routeName)
|
||||
klog.V(10).Infof("RoutesClient.Delete(%s,%s): end", az.RouteTableName, routeName)
|
||||
if rerr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.Errorf("RoutesClient.Delete(%s, %s) failed: %v", az.RouteTableName, routeName, rerr.Error())
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
// CreateOrUpdateVMSS invokes az.VirtualMachineScaleSetsClient.Update().
|
||||
func (az *Cloud) CreateOrUpdateVMSS(resourceGroupName string, VMScaleSetName string, parameters compute.VirtualMachineScaleSet) *retry.Error {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
// When vmss is being deleted, CreateOrUpdate API would report "the vmss is being deleted" error.
|
||||
// Since it is being deleted, we shouldn't send more CreateOrUpdate requests for it.
|
||||
klog.V(3).Infof("CreateOrUpdateVMSS: verify the status of the vmss being created or updated")
|
||||
vmss, rerr := az.VirtualMachineScaleSetsClient.Get(ctx, resourceGroupName, VMScaleSetName)
|
||||
if rerr != nil {
|
||||
klog.Errorf("CreateOrUpdateVMSS: error getting vmss(%s): %v", VMScaleSetName, rerr)
|
||||
return rerr
|
||||
}
|
||||
if vmss.ProvisioningState != nil && strings.EqualFold(*vmss.ProvisioningState, virtualMachineScaleSetsDeallocating) {
|
||||
klog.V(3).Infof("CreateOrUpdateVMSS: found vmss %s being deleted, skipping", VMScaleSetName)
|
||||
return nil
|
||||
}
|
||||
|
||||
rerr = az.VirtualMachineScaleSetsClient.CreateOrUpdate(ctx, resourceGroupName, VMScaleSetName, parameters)
|
||||
klog.V(10).Infof("UpdateVmssVMWithRetry: VirtualMachineScaleSetsClient.CreateOrUpdate(%s): end", VMScaleSetName)
|
||||
if rerr != nil {
|
||||
klog.Errorf("CreateOrUpdateVMSS: error CreateOrUpdate vmss(%s): %v", VMScaleSetName, rerr)
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/mockloadbalancerclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routeclient/mockrouteclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient/mocksecuritygroupclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetVirtualMachineWithRetry(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
vmClientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
vmClientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound},
|
||||
expectedErr: cloudprovider.InstanceNotFound,
|
||||
},
|
||||
{
|
||||
vmClientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(compute.VirtualMachine{}, test.vmClientErr)
|
||||
|
||||
vm, err := az.GetVirtualMachineWithRetry("vm", cache.CacheReadTypeDefault)
|
||||
assert.Empty(t, vm)
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPrivateIPsForMachine(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
vmClientErr *retry.Error
|
||||
expectedPrivateIPs []string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
expectedPrivateIPs: []string{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
vmClientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound},
|
||||
expectedErr: cloudprovider.InstanceNotFound,
|
||||
expectedPrivateIPs: []string{},
|
||||
},
|
||||
{
|
||||
vmClientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: wait.ErrWaitTimeout,
|
||||
expectedPrivateIPs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
expectedVM := compute.VirtualMachine{
|
||||
VirtualMachineProperties: &compute.VirtualMachineProperties{
|
||||
AvailabilitySet: &compute.SubResource{ID: pointer.String("availability-set")},
|
||||
NetworkProfile: &compute.NetworkProfile{
|
||||
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
|
||||
{
|
||||
NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{
|
||||
Primary: pointer.Bool(true),
|
||||
},
|
||||
ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedInterface := network.Interface{
|
||||
InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
|
||||
IPConfigurations: &[]network.InterfaceIPConfiguration{
|
||||
{
|
||||
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: pointer.String("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(expectedVM, test.vmClientErr)
|
||||
|
||||
mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface)
|
||||
mockInterfaceClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).MaxTimes(1)
|
||||
|
||||
privateIPs, err := az.getPrivateIPsForMachine("vm")
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.expectedPrivateIPs, privateIPs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIPForMachineWithRetry(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedPrivateIP string
|
||||
expectedPublicIP string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
expectedPrivateIP: "1.2.3.4",
|
||||
expectedPublicIP: "5.6.7.8",
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound},
|
||||
expectedErr: wait.ErrWaitTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
expectedVM := compute.VirtualMachine{
|
||||
VirtualMachineProperties: &compute.VirtualMachineProperties{
|
||||
AvailabilitySet: &compute.SubResource{ID: pointer.String("availability-set")},
|
||||
NetworkProfile: &compute.NetworkProfile{
|
||||
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
|
||||
{
|
||||
NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{
|
||||
Primary: pointer.Bool(true),
|
||||
},
|
||||
ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedInterface := network.Interface{
|
||||
InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
|
||||
IPConfigurations: &[]network.InterfaceIPConfiguration{
|
||||
{
|
||||
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: pointer.String("1.2.3.4"),
|
||||
PublicIPAddress: &network.PublicIPAddress{
|
||||
ID: pointer.String("test/pip"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedPIP := network.PublicIPAddress{
|
||||
PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
|
||||
IPAddress: pointer.String("5.6.7.8"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm", gomock.Any()).Return(expectedVM, test.clientErr)
|
||||
|
||||
mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface)
|
||||
mockInterfaceClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).MaxTimes(1)
|
||||
|
||||
mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPIPClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(expectedPIP, nil).MaxTimes(1)
|
||||
|
||||
privateIP, publicIP, err := az.GetIPForMachineWithRetry("vm")
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.expectedPrivateIP, privateIP)
|
||||
assert.Equal(t, test.expectedPublicIP, publicIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateSecurityGroupCanceled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
az.nsgCache.Set("sg", "test")
|
||||
|
||||
mockSGClient := az.SecurityGroupsClient.(*mocksecuritygroupclient.MockInterface)
|
||||
mockSGClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{
|
||||
RawError: fmt.Errorf(operationCanceledErrorMessage),
|
||||
})
|
||||
mockSGClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "sg", gomock.Any()).Return(network.SecurityGroup{}, nil)
|
||||
|
||||
err := az.CreateOrUpdateSecurityGroup(network.SecurityGroup{Name: pointer.String("sg")})
|
||||
assert.EqualError(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")), err.Error())
|
||||
|
||||
// security group should be removed from cache if the operation is canceled
|
||||
shouldBeEmpty, err := az.nsgCache.Get("sg", cache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, shouldBeEmpty)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateLB(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
referencedResourceNotProvisionedRawErrorString := `Code="ReferencedResourceNotProvisioned" Message="Cannot proceed with operation because resource /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip used by resource /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb is not in Succeeded state. Resource is in Failed state and the last operation that updated/is updating the resource is PutPublicIpAddressOperation."`
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{RawError: fmt.Errorf(referencedResourceNotProvisionedRawErrorString)},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf(referencedResourceNotProvisionedRawErrorString)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
az.lbCache.Set("lb", "test")
|
||||
|
||||
mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
|
||||
mockLBClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr)
|
||||
mockLBClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "lb", gomock.Any()).Return(network.LoadBalancer{}, nil)
|
||||
|
||||
mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPIPClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(nil).AnyTimes()
|
||||
mockPIPClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "pip", gomock.Any()).Return(network.PublicIPAddress{
|
||||
Name: pointer.String("pip"),
|
||||
PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
|
||||
ProvisioningState: pointer.String("Succeeded"),
|
||||
},
|
||||
}, nil).AnyTimes()
|
||||
|
||||
err := az.CreateOrUpdateLB(&v1.Service{}, network.LoadBalancer{
|
||||
Name: pointer.String("lb"),
|
||||
Etag: pointer.String("etag"),
|
||||
})
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
// loadbalancer should be removed from cache if the etag is mismatch or the operation is canceled
|
||||
shouldBeEmpty, err := az.lbCache.Get("lb", cache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, shouldBeEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListLB(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
|
||||
mockLBClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(nil, test.clientErr)
|
||||
|
||||
pips, err := az.ListLB(&v1.Service{})
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Empty(t, pips)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestListPIP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusNotFound},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPIPClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(nil, test.clientErr)
|
||||
|
||||
pips, err := az.ListPIP(&v1.Service{}, az.ResourceGroup)
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Empty(t, pips)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdatePIP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPIPClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError})
|
||||
|
||||
err := az.CreateOrUpdatePIP(&v1.Service{}, az.ResourceGroup, network.PublicIPAddress{Name: pointer.String("nic")})
|
||||
assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateInterface(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
mockInterfaceClient := az.InterfacesClient.(*mockinterfaceclient.MockInterface)
|
||||
mockInterfaceClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "nic", gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError})
|
||||
|
||||
err := az.CreateOrUpdateInterface(&v1.Service{}, network.Interface{Name: pointer.String("nic")})
|
||||
assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err)
|
||||
}
|
||||
|
||||
func TestDeletePublicIP(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
mockPIPClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPIPClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "pip").Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError})
|
||||
|
||||
err := az.DeletePublicIP(&v1.Service{}, az.ResourceGroup, "pip")
|
||||
assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err)
|
||||
}
|
||||
|
||||
func TestDeleteLB(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
mockLBClient := az.LoadBalancerClient.(*mockloadbalancerclient.MockInterface)
|
||||
mockLBClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "lb").Return(&retry.Error{HTTPStatusCode: http.StatusInternalServerError})
|
||||
|
||||
err := az.DeleteLB(&v1.Service{}, "lb")
|
||||
assert.Equal(t, fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)), err)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRouteTable(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
az.rtCache.Set("rt", "test")
|
||||
|
||||
mockRTClient := az.RouteTablesClient.(*mockroutetableclient.MockInterface)
|
||||
mockRTClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr)
|
||||
mockRTClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "rt", gomock.Any()).Return(network.RouteTable{}, nil)
|
||||
|
||||
err := az.CreateOrUpdateRouteTable(network.RouteTable{
|
||||
Name: pointer.String("rt"),
|
||||
Etag: pointer.String("etag"),
|
||||
})
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
// route table should be removed from cache if the etag is mismatch or the operation is canceled
|
||||
shouldBeEmpty, err := az.rtCache.Get("rt", cache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, shouldBeEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRoute(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusPreconditionFailed},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 412, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{RawError: fmt.Errorf("canceledandsupersededduetoanotheroperation")},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("canceledandsupersededduetoanotheroperation")),
|
||||
},
|
||||
{
|
||||
clientErr: nil,
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
az.rtCache.Set("rt", "test")
|
||||
|
||||
mockRTClient := az.RoutesClient.(*mockrouteclient.MockInterface)
|
||||
mockRTClient.EXPECT().CreateOrUpdate(gomock.Any(), az.ResourceGroup, "rt", gomock.Any(), gomock.Any(), gomock.Any()).Return(test.clientErr)
|
||||
|
||||
mockRTableClient := az.RouteTablesClient.(*mockroutetableclient.MockInterface)
|
||||
mockRTableClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "rt", gomock.Any()).Return(network.RouteTable{}, nil)
|
||||
|
||||
err := az.CreateOrUpdateRoute(network.Route{
|
||||
Name: pointer.String("rt"),
|
||||
Etag: pointer.String("etag"),
|
||||
})
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
|
||||
shouldBeEmpty, err := az.rtCache.Get("rt", cache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, shouldBeEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRouteWithName(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
clientErr *retry.Error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil)),
|
||||
},
|
||||
{
|
||||
clientErr: nil,
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
|
||||
mockRTClient := az.RoutesClient.(*mockrouteclient.MockInterface)
|
||||
mockRTClient.EXPECT().Delete(gomock.Any(), az.ResourceGroup, "rt", "rt").Return(test.clientErr)
|
||||
|
||||
err := az.DeleteRouteWithName("rt")
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateVMSS(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
vmss compute.VirtualMachineScaleSet
|
||||
clientErr *retry.Error
|
||||
expectedErr *retry.Error
|
||||
}{
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
expectedErr: &retry.Error{HTTPStatusCode: http.StatusInternalServerError},
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{HTTPStatusCode: http.StatusTooManyRequests},
|
||||
expectedErr: &retry.Error{HTTPStatusCode: http.StatusTooManyRequests},
|
||||
},
|
||||
{
|
||||
clientErr: &retry.Error{RawError: fmt.Errorf("azure cloud provider rate limited(write) for operation CreateOrUpdate")},
|
||||
expectedErr: &retry.Error{RawError: fmt.Errorf("azure cloud provider rate limited(write) for operation CreateOrUpdate")},
|
||||
},
|
||||
{
|
||||
vmss: compute.VirtualMachineScaleSet{
|
||||
VirtualMachineScaleSetProperties: &compute.VirtualMachineScaleSetProperties{
|
||||
ProvisioningState: &virtualMachineScaleSetsDeallocating,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
az := GetTestCloud(ctrl)
|
||||
|
||||
mockVMSSClient := az.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
|
||||
mockVMSSClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, testVMSSName).Return(test.vmss, test.clientErr)
|
||||
|
||||
err := az.CreateOrUpdateVMSS(az.ResourceGroup, testVMSSName, compute.VirtualMachineScaleSet{})
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBackoff(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
az.CloudProviderBackoff = true
|
||||
az.ResourceRequestBackoff = wait.Backoff{Steps: 3}
|
||||
|
||||
backoff := az.RequestBackoff()
|
||||
assert.Equal(t, wait.Backoff{Steps: 3}, backoff)
|
||||
|
||||
}
|
||||
@@ -1,648 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
azstorage "github.com/Azure/azure-sdk-for-go/storage"
|
||||
"github.com/rubiojr/go-vhd/vhd"
|
||||
|
||||
kwait "k8s.io/apimachinery/pkg/util/wait"
|
||||
volerr "k8s.io/cloud-provider/volume/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// Attention: blob disk feature is deprecated
|
||||
const (
|
||||
vhdContainerName = "vhds"
|
||||
useHTTPSForBlobBasedDisk = true
|
||||
blobServiceName = "blob"
|
||||
)
|
||||
|
||||
type storageAccountState struct {
|
||||
name string
|
||||
saType storage.SkuName
|
||||
key string
|
||||
diskCount int32
|
||||
isValidating int32
|
||||
defaultContainerCreated bool
|
||||
}
|
||||
|
||||
// BlobDiskController : blob disk controller struct
|
||||
type BlobDiskController struct {
|
||||
common *controllerCommon
|
||||
accounts map[string]*storageAccountState
|
||||
}
|
||||
|
||||
var (
|
||||
accountsLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func (c *BlobDiskController) initStorageAccounts() {
|
||||
accountsLock.Lock()
|
||||
defer accountsLock.Unlock()
|
||||
|
||||
if c.accounts == nil {
|
||||
// get accounts
|
||||
accounts, err := c.getAllStorageAccounts()
|
||||
if err != nil {
|
||||
klog.Errorf("azureDisk - getAllStorageAccounts error: %v", err)
|
||||
c.accounts = make(map[string]*storageAccountState)
|
||||
}
|
||||
c.accounts = accounts
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVolume creates a VHD blob in a storage account that has storageType and location using the given storage account.
|
||||
// If no storage account is given, search all the storage accounts associated with the resource group and pick one that
|
||||
// fits storage type and location.
|
||||
func (c *BlobDiskController) CreateVolume(blobName, accountName, accountType, location string, requestGB int) (string, string, int, error) {
|
||||
accountOptions := &AccountOptions{
|
||||
Name: accountName,
|
||||
Type: accountType,
|
||||
Kind: string(defaultStorageAccountKind),
|
||||
ResourceGroup: c.common.resourceGroup,
|
||||
Location: location,
|
||||
EnableHTTPSTrafficOnly: true,
|
||||
}
|
||||
account, key, err := c.common.cloud.EnsureStorageAccount(accountOptions, dedicatedDiskAccountNamePrefix)
|
||||
if err != nil {
|
||||
return "", "", 0, fmt.Errorf("could not get storage key for storage account %s: %v", accountName, err)
|
||||
}
|
||||
|
||||
client, err := azstorage.NewBasicClientOnSovereignCloud(account, key, c.common.cloud.Environment)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
blobClient := client.GetBlobService()
|
||||
|
||||
// create a page blob in this account's vhd container
|
||||
diskName, diskURI, err := c.createVHDBlobDisk(blobClient, account, blobName, vhdContainerName, int64(requestGB))
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("azureDisk - created vhd blob uri: %s", diskURI)
|
||||
return diskName, diskURI, requestGB, err
|
||||
}
|
||||
|
||||
// DeleteVolume deletes a VHD blob
|
||||
func (c *BlobDiskController) DeleteVolume(diskURI string) error {
|
||||
klog.V(4).Infof("azureDisk - begin to delete volume %s", diskURI)
|
||||
accountName, blob, err := c.common.cloud.getBlobNameAndAccountFromURI(diskURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse vhd URI %v", err)
|
||||
}
|
||||
key, err := c.common.cloud.GetStorageAccesskey(accountName, c.common.resourceGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no key for storage account %s, err %v", accountName, err)
|
||||
}
|
||||
err = c.common.cloud.deleteVhdBlob(accountName, key, blob)
|
||||
if err != nil {
|
||||
klog.Warningf("azureDisk - failed to delete blob %s err: %v", diskURI, err)
|
||||
detail := err.Error()
|
||||
if strings.Contains(detail, errLeaseIDMissing) {
|
||||
// disk is still being used
|
||||
// see https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.storage.blob.protocol.bloberrorcodestrings.leaseidmissing.aspx
|
||||
return volerr.NewDeletedVolumeInUseError(fmt.Sprintf("disk %q is still in use while being deleted", diskURI))
|
||||
}
|
||||
return fmt.Errorf("failed to delete vhd %v, account %s, blob %s, err: %v", diskURI, accountName, blob, err)
|
||||
}
|
||||
klog.V(4).Infof("azureDisk - blob %s deleted", diskURI)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// get diskURI https://foo.blob.core.windows.net/vhds/bar.vhd and return foo (account) and bar.vhd (blob name)
|
||||
func (c *BlobDiskController) getBlobNameAndAccountFromURI(diskURI string) (string, string, error) {
|
||||
scheme := "http"
|
||||
if useHTTPSForBlobBasedDisk {
|
||||
scheme = "https"
|
||||
}
|
||||
host := fmt.Sprintf("%s://(.*).%s.%s", scheme, blobServiceName, c.common.storageEndpointSuffix)
|
||||
reStr := fmt.Sprintf("%s/%s/(.*)", host, vhdContainerName)
|
||||
re := regexp.MustCompile(reStr)
|
||||
res := re.FindSubmatch([]byte(diskURI))
|
||||
if len(res) < 3 {
|
||||
return "", "", fmt.Errorf("invalid vhd URI for regex %s: %s", reStr, diskURI)
|
||||
}
|
||||
return string(res[1]), string(res[2]), nil
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) createVHDBlobDisk(blobClient azstorage.BlobStorageClient, accountName, vhdName, containerName string, sizeGB int64) (string, string, error) {
|
||||
container := blobClient.GetContainerReference(containerName)
|
||||
size := 1024 * 1024 * 1024 * sizeGB
|
||||
vhdSize := size + vhd.VHD_HEADER_SIZE /* header size */
|
||||
// Blob name in URL must end with '.vhd' extension.
|
||||
vhdName = vhdName + ".vhd"
|
||||
|
||||
tags := make(map[string]string)
|
||||
tags["createdby"] = "k8sAzureDataDisk"
|
||||
klog.V(4).Infof("azureDisk - creating page blob %s in container %s account %s", vhdName, containerName, accountName)
|
||||
|
||||
blob := container.GetBlobReference(vhdName)
|
||||
blob.Properties.ContentLength = vhdSize
|
||||
blob.Metadata = tags
|
||||
err := blob.PutPageBlob(nil)
|
||||
if err != nil {
|
||||
// if container doesn't exist, create one and retry PutPageBlob
|
||||
detail := err.Error()
|
||||
if strings.Contains(detail, errContainerNotFound) {
|
||||
err = container.Create(&azstorage.CreateContainerOptions{Access: azstorage.ContainerAccessTypePrivate})
|
||||
if err == nil {
|
||||
err = blob.PutPageBlob(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to put page blob %s in container %s: %v", vhdName, containerName, err)
|
||||
}
|
||||
|
||||
// add VHD signature to the blob
|
||||
h, err := createVHDHeader(uint64(size))
|
||||
if err != nil {
|
||||
blob.DeleteIfExists(nil)
|
||||
return "", "", fmt.Errorf("failed to create vhd header, err: %v", err)
|
||||
}
|
||||
|
||||
blobRange := azstorage.BlobRange{
|
||||
Start: uint64(size),
|
||||
End: uint64(vhdSize - 1),
|
||||
}
|
||||
if err = blob.WriteRange(blobRange, bytes.NewBuffer(h[:vhd.VHD_HEADER_SIZE]), nil); err != nil {
|
||||
klog.Infof("azureDisk - failed to put header page for data disk %s in container %s account %s, error was %s\n",
|
||||
vhdName, containerName, accountName, err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if useHTTPSForBlobBasedDisk {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
host := fmt.Sprintf("%s://%s.%s.%s", scheme, accountName, blobServiceName, c.common.storageEndpointSuffix)
|
||||
uri := fmt.Sprintf("%s/%s/%s", host, containerName, vhdName)
|
||||
return vhdName, uri, nil
|
||||
}
|
||||
|
||||
// delete a vhd blob
|
||||
func (c *BlobDiskController) deleteVhdBlob(accountName, accountKey, blobName string) error {
|
||||
client, err := azstorage.NewBasicClientOnSovereignCloud(accountName, accountKey, c.common.cloud.Environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobSvc := client.GetBlobService()
|
||||
|
||||
container := blobSvc.GetContainerReference(vhdContainerName)
|
||||
blob := container.GetBlobReference(blobName)
|
||||
return blob.Delete(nil)
|
||||
}
|
||||
|
||||
// CreateBlobDisk : create a blob disk in a node
|
||||
func (c *BlobDiskController) CreateBlobDisk(dataDiskName string, storageAccountType storage.SkuName, sizeGB int) (string, error) {
|
||||
klog.V(4).Infof("azureDisk - creating blob data disk named:%s on StorageAccountType:%s", dataDiskName, storageAccountType)
|
||||
|
||||
c.initStorageAccounts()
|
||||
|
||||
storageAccountName, err := c.findSANameForDisk(storageAccountType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blobClient, err := c.getBlobSvcClient(storageAccountName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, diskURI, err := c.createVHDBlobDisk(blobClient, storageAccountName, dataDiskName, vhdContainerName, int64(sizeGB))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
atomic.AddInt32(&c.accounts[storageAccountName].diskCount, 1)
|
||||
|
||||
return diskURI, nil
|
||||
}
|
||||
|
||||
// DeleteBlobDisk : delete a blob disk from a node
|
||||
func (c *BlobDiskController) DeleteBlobDisk(diskURI string) error {
|
||||
storageAccountName, vhdName, err := diskNameAndSANameFromURI(diskURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, ok := c.accounts[storageAccountName]
|
||||
if !ok {
|
||||
// the storage account is specified by user
|
||||
klog.V(4).Infof("azureDisk - deleting volume %s", diskURI)
|
||||
return c.DeleteVolume(diskURI)
|
||||
}
|
||||
|
||||
blobSvc, err := c.getBlobSvcClient(storageAccountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("azureDisk - About to delete vhd file %s on storage account %s container %s", vhdName, storageAccountName, vhdContainerName)
|
||||
|
||||
container := blobSvc.GetContainerReference(vhdContainerName)
|
||||
blob := container.GetBlobReference(vhdName)
|
||||
_, err = blob.DeleteIfExists(nil)
|
||||
|
||||
if c.accounts[storageAccountName].diskCount == -1 {
|
||||
if diskCount, err := c.getDiskCount(storageAccountName); err != nil {
|
||||
c.accounts[storageAccountName].diskCount = int32(diskCount)
|
||||
} else {
|
||||
klog.Warningf("azureDisk - failed to get disk count for %s however the delete disk operation was ok", storageAccountName)
|
||||
return nil // we have failed to acquire a new count. not an error condition
|
||||
}
|
||||
}
|
||||
atomic.AddInt32(&c.accounts[storageAccountName].diskCount, -1)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) getStorageAccountKey(SAName string) (string, error) {
|
||||
if account, exists := c.accounts[SAName]; exists && account.key != "" {
|
||||
return c.accounts[SAName].key, nil
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
listKeysResult, rerr := c.common.cloud.StorageAccountClient.ListKeys(ctx, c.common.resourceGroup, SAName)
|
||||
if rerr != nil {
|
||||
return "", rerr.Error()
|
||||
}
|
||||
if listKeysResult.Keys == nil {
|
||||
return "", fmt.Errorf("azureDisk - empty listKeysResult in storage account:%s keys", SAName)
|
||||
}
|
||||
for _, v := range *listKeysResult.Keys {
|
||||
if v.Value != nil && *v.Value == "key1" {
|
||||
if _, ok := c.accounts[SAName]; !ok {
|
||||
klog.Warningf("azureDisk - account %s was not cached while getting keys", SAName)
|
||||
return *v.Value, nil
|
||||
}
|
||||
c.accounts[SAName].key = *v.Value
|
||||
return c.accounts[SAName].key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("couldn't find key named key1 in storage account:%s keys", SAName)
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) getBlobSvcClient(SAName string) (azstorage.BlobStorageClient, error) {
|
||||
key := ""
|
||||
var client azstorage.Client
|
||||
var blobSvc azstorage.BlobStorageClient
|
||||
var err error
|
||||
if key, err = c.getStorageAccountKey(SAName); err != nil {
|
||||
return blobSvc, err
|
||||
}
|
||||
|
||||
if client, err = azstorage.NewBasicClientOnSovereignCloud(SAName, key, c.common.cloud.Environment); err != nil {
|
||||
return blobSvc, err
|
||||
}
|
||||
|
||||
blobSvc = client.GetBlobService()
|
||||
return blobSvc, nil
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) ensureDefaultContainer(storageAccountName string) error {
|
||||
var err error
|
||||
var blobSvc azstorage.BlobStorageClient
|
||||
|
||||
// short circuit the check via local cache
|
||||
// we are forgiving the fact that account may not be in cache yet
|
||||
if v, ok := c.accounts[storageAccountName]; ok && v.defaultContainerCreated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// not cached, check existence and readiness
|
||||
bExist, provisionState, _ := c.getStorageAccountState(storageAccountName)
|
||||
|
||||
// account does not exist
|
||||
if !bExist {
|
||||
return fmt.Errorf("azureDisk - account %s does not exist while trying to create/ensure default container", storageAccountName)
|
||||
}
|
||||
|
||||
// account exists but not ready yet
|
||||
if provisionState != storage.Succeeded {
|
||||
// we don't want many attempts to validate the account readiness
|
||||
// here hence we are locking
|
||||
counter := 1
|
||||
for swapped := atomic.CompareAndSwapInt32(&c.accounts[storageAccountName].isValidating, 0, 1); swapped != true; {
|
||||
time.Sleep(3 * time.Second)
|
||||
counter = counter + 1
|
||||
// check if we passed the max sleep
|
||||
if counter >= 20 {
|
||||
return fmt.Errorf("azureDisk - timeout waiting to acquire lock to validate account:%s readiness", storageAccountName)
|
||||
}
|
||||
}
|
||||
|
||||
// swapped
|
||||
defer func() {
|
||||
c.accounts[storageAccountName].isValidating = 0
|
||||
}()
|
||||
|
||||
// short circuit the check again.
|
||||
if v, ok := c.accounts[storageAccountName]; ok && v.defaultContainerCreated {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = kwait.ExponentialBackoff(defaultBackOff, func() (bool, error) {
|
||||
_, provisionState, err := c.getStorageAccountState(storageAccountName)
|
||||
|
||||
if err != nil {
|
||||
klog.V(4).Infof("azureDisk - GetStorageAccount:%s err %s", storageAccountName, err.Error())
|
||||
return false, nil // error performing the query - retryable
|
||||
}
|
||||
|
||||
if provisionState == storage.Succeeded {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
klog.V(4).Infof("azureDisk - GetStorageAccount:%s not ready yet (not flagged Succeeded by ARM)", storageAccountName)
|
||||
return false, nil // back off and see if the account becomes ready on next retry
|
||||
})
|
||||
// we have failed to ensure that account is ready for us to create
|
||||
// the default vhd container
|
||||
if err != nil {
|
||||
if err == kwait.ErrWaitTimeout {
|
||||
return fmt.Errorf("azureDisk - timed out waiting for storage account %s to become ready", storageAccountName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if blobSvc, err = c.getBlobSvcClient(storageAccountName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
container := blobSvc.GetContainerReference(vhdContainerName)
|
||||
bCreated, err := container.CreateIfNotExists(&azstorage.CreateContainerOptions{Access: azstorage.ContainerAccessTypePrivate})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bCreated {
|
||||
klog.V(2).Infof("azureDisk - storage account:%s had no default container(%s) and it was created \n", storageAccountName, vhdContainerName)
|
||||
}
|
||||
|
||||
// flag so we no longer have to check on ARM
|
||||
c.accounts[storageAccountName].defaultContainerCreated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets Disk counts per storage account
|
||||
func (c *BlobDiskController) getDiskCount(SAName string) (int, error) {
|
||||
// if we have it in cache
|
||||
if c.accounts[SAName].diskCount != -1 {
|
||||
return int(c.accounts[SAName].diskCount), nil
|
||||
}
|
||||
|
||||
var err error
|
||||
var blobSvc azstorage.BlobStorageClient
|
||||
|
||||
if err = c.ensureDefaultContainer(SAName); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if blobSvc, err = c.getBlobSvcClient(SAName); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
params := azstorage.ListBlobsParameters{}
|
||||
|
||||
container := blobSvc.GetContainerReference(vhdContainerName)
|
||||
response, err := container.ListBlobs(params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
klog.V(4).Infof("azure-Disk - refreshed data count for account %s and found %v", SAName, len(response.Blobs))
|
||||
c.accounts[SAName].diskCount = int32(len(response.Blobs))
|
||||
|
||||
return int(c.accounts[SAName].diskCount), nil
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) getAllStorageAccounts() (map[string]*storageAccountState, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
accountList, rerr := c.common.cloud.StorageAccountClient.ListByResourceGroup(ctx, c.common.resourceGroup)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
accounts := make(map[string]*storageAccountState)
|
||||
for _, v := range accountList {
|
||||
if v.Name == nil || v.Sku == nil {
|
||||
klog.Info("azureDisk - accountListResult Name or Sku is nil")
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(*v.Name, sharedDiskAccountNamePrefix) {
|
||||
continue
|
||||
}
|
||||
klog.Infof("azureDisk - identified account %s as part of shared PVC accounts", *v.Name)
|
||||
|
||||
saState := &storageAccountState{
|
||||
name: *v.Name,
|
||||
saType: (*v.Sku).Name,
|
||||
diskCount: -1,
|
||||
}
|
||||
|
||||
accounts[*v.Name] = saState
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) createStorageAccount(storageAccountName string, storageAccountType storage.SkuName, location string, checkMaxAccounts bool) error {
|
||||
bExist, _, _ := c.getStorageAccountState(storageAccountName)
|
||||
if bExist {
|
||||
newAccountState := &storageAccountState{
|
||||
diskCount: -1,
|
||||
saType: storageAccountType,
|
||||
name: storageAccountName,
|
||||
}
|
||||
|
||||
c.addAccountState(storageAccountName, newAccountState)
|
||||
}
|
||||
// Account Does not exist
|
||||
if !bExist {
|
||||
if len(c.accounts) == maxStorageAccounts && checkMaxAccounts {
|
||||
return fmt.Errorf("azureDisk - can not create new storage account, current storage accounts count:%v Max is:%v", len(c.accounts), maxStorageAccounts)
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - Creating storage account %s type %s", storageAccountName, string(storageAccountType))
|
||||
|
||||
cp := storage.AccountCreateParameters{
|
||||
Sku: &storage.Sku{Name: storageAccountType},
|
||||
// switch to use StorageV2 as it's recommended according to https://docs.microsoft.com/en-us/azure/storage/common/storage-account-options
|
||||
Kind: defaultStorageAccountKind,
|
||||
Tags: map[string]*string{"created-by": pointer.String("azure-dd")},
|
||||
Location: &location}
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
err := c.common.cloud.StorageAccountClient.Create(ctx, c.common.resourceGroup, storageAccountName, cp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Create Storage Account: %s, error: %v", storageAccountName, err)
|
||||
}
|
||||
|
||||
newAccountState := &storageAccountState{
|
||||
diskCount: -1,
|
||||
saType: storageAccountType,
|
||||
name: storageAccountName,
|
||||
}
|
||||
|
||||
c.addAccountState(storageAccountName, newAccountState)
|
||||
}
|
||||
|
||||
// finally, make sure that we default container is created
|
||||
// before handing it back over
|
||||
return c.ensureDefaultContainer(storageAccountName)
|
||||
}
|
||||
|
||||
// finds a new suitable storageAccount for this disk
|
||||
func (c *BlobDiskController) findSANameForDisk(storageAccountType storage.SkuName) (string, error) {
|
||||
maxDiskCount := maxDisksPerStorageAccounts
|
||||
SAName := ""
|
||||
totalDiskCounts := 0
|
||||
countAccounts := 0 // account of this type.
|
||||
for _, v := range c.accounts {
|
||||
// filter out any stand-alone disks/accounts
|
||||
if !strings.HasPrefix(v.name, sharedDiskAccountNamePrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
// note: we compute avg stratified by type.
|
||||
// this is to enable user to grow per SA type to avoid low
|
||||
// avg utilization on one account type skewing all data.
|
||||
|
||||
if v.saType == storageAccountType {
|
||||
// compute average
|
||||
dCount, err := c.getDiskCount(v.name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
totalDiskCounts = totalDiskCounts + dCount
|
||||
countAccounts = countAccounts + 1
|
||||
// empty account
|
||||
if dCount == 0 {
|
||||
klog.V(2).Infof("azureDisk - account %s identified for a new disk is because it has 0 allocated disks", v.name)
|
||||
return v.name, nil // short circuit, avg is good and no need to adjust
|
||||
}
|
||||
// if this account is less allocated
|
||||
if dCount < maxDiskCount {
|
||||
maxDiskCount = dCount
|
||||
SAName = v.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we failed to find storageaccount
|
||||
if SAName == "" {
|
||||
klog.V(2).Infof("azureDisk - failed to identify a suitable account for new disk and will attempt to create new account")
|
||||
SAName = generateStorageAccountName(sharedDiskAccountNamePrefix)
|
||||
err := c.createStorageAccount(SAName, storageAccountType, c.common.location, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return SAName, nil
|
||||
}
|
||||
|
||||
disksAfter := totalDiskCounts + 1 // with the new one!
|
||||
|
||||
avgUtilization := float64(disksAfter) / float64(countAccounts*maxDisksPerStorageAccounts)
|
||||
aboveAvg := avgUtilization > storageAccountUtilizationBeforeGrowing
|
||||
|
||||
// avg are not create and we should create more accounts if we can
|
||||
if aboveAvg && countAccounts < maxStorageAccounts {
|
||||
klog.V(2).Infof("azureDisk - shared storageAccounts utilization(%v) > grow-at-avg-utilization (%v). New storage account will be created", avgUtilization, storageAccountUtilizationBeforeGrowing)
|
||||
SAName = generateStorageAccountName(sharedDiskAccountNamePrefix)
|
||||
err := c.createStorageAccount(SAName, storageAccountType, c.common.location, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return SAName, nil
|
||||
}
|
||||
|
||||
// averages are not ok and we are at capacity (max storage accounts allowed)
|
||||
if aboveAvg && countAccounts == maxStorageAccounts {
|
||||
klog.Infof("azureDisk - shared storageAccounts utilization(%v) > grow-at-avg-utilization (%v). But k8s maxed on SAs for PVC(%v). k8s will now exceed grow-at-avg-utilization without adding accounts",
|
||||
avgUtilization, storageAccountUtilizationBeforeGrowing, maxStorageAccounts)
|
||||
}
|
||||
|
||||
// we found a storage accounts && [ avg are ok || we reached max sa count ]
|
||||
return SAName, nil
|
||||
}
|
||||
|
||||
// Gets storage account exist, provisionStatus, Error if any
|
||||
func (c *BlobDiskController) getStorageAccountState(storageAccountName string) (bool, storage.ProvisioningState, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
account, rerr := c.common.cloud.StorageAccountClient.GetProperties(ctx, c.common.resourceGroup, storageAccountName)
|
||||
if rerr != nil {
|
||||
return false, "", rerr.Error()
|
||||
}
|
||||
return true, account.AccountProperties.ProvisioningState, nil
|
||||
}
|
||||
|
||||
func (c *BlobDiskController) addAccountState(key string, state *storageAccountState) {
|
||||
accountsLock.Lock()
|
||||
defer accountsLock.Unlock()
|
||||
|
||||
if _, ok := c.accounts[key]; !ok {
|
||||
c.accounts[key] = state
|
||||
}
|
||||
}
|
||||
|
||||
func createVHDHeader(size uint64) ([]byte, error) {
|
||||
h := vhd.CreateFixedHeader(size, &vhd.VHDOptions{})
|
||||
b := new(bytes.Buffer)
|
||||
err := binary.Write(b, binary.BigEndian, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func diskNameAndSANameFromURI(diskURI string) (string, string, error) {
|
||||
uri, err := url.Parse(diskURI)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
hostName := uri.Host
|
||||
storageAccountName := strings.Split(hostName, ".")[0]
|
||||
|
||||
segments := strings.Split(uri.Path, "/")
|
||||
diskNameVhd := segments[len(segments)-1]
|
||||
|
||||
return storageAccountName, diskNameVhd, nil
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
azstorage "github.com/Azure/azure-sdk-for-go/storage"
|
||||
autorestazure "github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var retryError500 = retry.Error{HTTPStatusCode: http.StatusInternalServerError}
|
||||
|
||||
func GetTestBlobDiskController(t *testing.T) BlobDiskController {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
az := GetTestCloud(ctrl)
|
||||
az.Environment = autorestazure.PublicCloud
|
||||
common := &controllerCommon{cloud: az, resourceGroup: "rg", location: "westus"}
|
||||
|
||||
return BlobDiskController{
|
||||
common: common,
|
||||
accounts: make(map[string]*storageAccountState),
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitStorageAccounts(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
b.accounts = nil
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return([]storage.Account{}, &retryError500)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
b.initStorageAccounts()
|
||||
assert.Empty(t, b.accounts)
|
||||
|
||||
mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return([]storage.Account{
|
||||
{
|
||||
Name: pointer.String("ds-0"),
|
||||
Sku: &storage.Sku{Name: "sku"},
|
||||
},
|
||||
}, nil)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
b.initStorageAccounts()
|
||||
assert.Equal(t, 1, len(b.accounts))
|
||||
}
|
||||
|
||||
func TestCreateVolume(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{}, &retryError500)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
diskName, diskURI, requestGB, err := b.CreateVolume("testBlob", "testsa", "type", b.common.location, 10)
|
||||
expectedErr := fmt.Errorf("could not get storage key for storage account testsa: could not get storage key for "+
|
||||
"storage account testsa: Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil))
|
||||
assert.EqualError(t, err, expectedErr.Error())
|
||||
assert.Empty(t, diskName)
|
||||
assert.Empty(t, diskURI)
|
||||
assert.Zero(t, requestGB)
|
||||
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("dmFsdWUK"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
diskName, diskURI, requestGB, err = b.CreateVolume("testBlob", "testsa", "type", b.common.location, 10)
|
||||
expectedErrStr := "failed to put page blob testBlob.vhd in container vhds: storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled."
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErrStr))
|
||||
assert.Empty(t, diskName)
|
||||
assert.Empty(t, diskURI)
|
||||
assert.Zero(t, requestGB)
|
||||
}
|
||||
|
||||
func TestDeleteVolume(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
b.common.cloud.BlobDiskController = &b
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "foo").Return(storage.AccountListKeysResult{}, &retryError500).Times(2)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
fakeDiskURL := "fake"
|
||||
diskURL := "https://foo.blob./vhds/bar.vhd"
|
||||
err := b.DeleteVolume(diskURL)
|
||||
expectedErr := fmt.Errorf("no key for storage account foo, err Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", error(nil))
|
||||
assert.EqualError(t, err, expectedErr.Error())
|
||||
|
||||
err = b.DeleteVolume(diskURL)
|
||||
assert.EqualError(t, err, expectedErr.Error())
|
||||
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "foo").Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("dmFsdWUK"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
err = b.DeleteVolume(fakeDiskURL)
|
||||
expectedErr = fmt.Errorf("failed to parse vhd URI invalid vhd URI for regex https://(.*).blob./vhds/(.*): fake")
|
||||
assert.Equal(t, expectedErr, err)
|
||||
|
||||
err = b.DeleteVolume(diskURL)
|
||||
expectedErrStr := "failed to delete vhd https://foo.blob./vhds/bar.vhd, account foo, blob bar.vhd, err: storage: service returned error: " +
|
||||
"StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled."
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErrStr))
|
||||
}
|
||||
|
||||
func TestCreateVHDBlobDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
b.common.cloud.Environment = autorestazure.PublicCloud
|
||||
client, err := azstorage.NewBasicClientOnSovereignCloud("testsa", "a2V5Cg==", b.common.cloud.Environment)
|
||||
assert.NoError(t, err)
|
||||
blobClient := client.GetBlobService()
|
||||
|
||||
_, _, err = b.createVHDBlobDisk(blobClient, "testsa", "blob", vhdContainerName, int64(10))
|
||||
expectedErr := "failed to put page blob blob.vhd in container vhds: storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled."
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErr))
|
||||
}
|
||||
|
||||
func TestGetAllStorageAccounts(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
expectedStorageAccounts := []storage.Account{
|
||||
{
|
||||
Name: pointer.String("this-should-be-skipped"),
|
||||
},
|
||||
{
|
||||
Name: pointer.String("this-should-be-skipped"),
|
||||
Sku: &storage.Sku{Name: "sku"},
|
||||
},
|
||||
{
|
||||
Name: pointer.String("ds-0"),
|
||||
Sku: &storage.Sku{Name: "sku"},
|
||||
},
|
||||
}
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
mockSAClient.EXPECT().ListByResourceGroup(gomock.Any(), b.common.resourceGroup).Return(expectedStorageAccounts, nil)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
accounts, err := b.getAllStorageAccounts()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(accounts))
|
||||
}
|
||||
|
||||
func TestEnsureDefaultContainer(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500)
|
||||
err := b.ensureDefaultContainer("testsa")
|
||||
expectedErr := fmt.Errorf("azureDisk - account testsa does not exist while trying to create/ensure default container")
|
||||
assert.Equal(t, expectedErr, err)
|
||||
|
||||
b.accounts["testsa"] = &storageAccountState{defaultContainerCreated: true}
|
||||
err = b.ensureDefaultContainer("testsa")
|
||||
assert.NoError(t, err)
|
||||
|
||||
b.accounts["testsa"] = &storageAccountState{isValidating: 0}
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{
|
||||
AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Creating},
|
||||
}, nil)
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500)
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{
|
||||
AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Succeeded},
|
||||
}, nil)
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("key1"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
err = b.ensureDefaultContainer("testsa")
|
||||
expectedErrStr := "storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled."
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErrStr))
|
||||
}
|
||||
|
||||
func TestGetDiskCount(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
b.accounts["testsa"] = &storageAccountState{diskCount: 1}
|
||||
count, err := b.getDiskCount("testsa")
|
||||
assert.Equal(t, 1, count)
|
||||
assert.NoError(t, err)
|
||||
|
||||
b.accounts["testsa"] = &storageAccountState{diskCount: -1}
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.Account{}, &retryError500)
|
||||
count, err = b.getDiskCount("testsa")
|
||||
assert.Zero(t, count)
|
||||
expectedErr := fmt.Errorf("azureDisk - account testsa does not exist while trying to create/ensure default container")
|
||||
assert.Equal(t, expectedErr, err)
|
||||
|
||||
b.accounts["testsa"].defaultContainerCreated = true
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, "testsa").Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("key1"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
count, err = b.getDiskCount("testsa")
|
||||
expectedErrStr := "storage: service returned error: StatusCode=403, ErrorCode=AccountIsDisabled, ErrorMessage=The specified account is disabled."
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErrStr))
|
||||
assert.Zero(t, count)
|
||||
}
|
||||
|
||||
func TestFindSANameForDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
|
||||
b.accounts = map[string]*storageAccountState{
|
||||
"this-shall-be-skipped": {name: "fake"},
|
||||
"ds0": {
|
||||
name: "ds0",
|
||||
saType: storage.StandardGRS,
|
||||
diskCount: 50,
|
||||
},
|
||||
}
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.Account{}, &retryError500).Times(2)
|
||||
mockSAClient.EXPECT().GetProperties(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.Account{
|
||||
AccountProperties: &storage.AccountProperties{ProvisioningState: storage.Succeeded},
|
||||
}, nil).Times(2)
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("key1"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
mockSAClient.EXPECT().Create(gomock.Any(), b.common.resourceGroup, gomock.Any(), gomock.Any()).Return(nil)
|
||||
name, err := b.findSANameForDisk(storage.StandardGRS)
|
||||
expectedErr := "does not exist while trying to create/ensure default container"
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErr))
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, name)
|
||||
|
||||
b.accounts = make(map[string]*storageAccountState)
|
||||
name, err = b.findSANameForDisk(storage.StandardGRS)
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, name)
|
||||
|
||||
b.accounts = map[string]*storageAccountState{
|
||||
"ds0": {
|
||||
name: "ds0",
|
||||
saType: storage.StandardGRS,
|
||||
diskCount: 0,
|
||||
},
|
||||
}
|
||||
name, err = b.findSANameForDisk(storage.StandardGRS)
|
||||
assert.Equal(t, "ds0", name)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < maxStorageAccounts; i++ {
|
||||
b.accounts[fmt.Sprintf("ds%d", i)] = &storageAccountState{
|
||||
name: fmt.Sprintf("ds%d", i),
|
||||
saType: storage.StandardGRS,
|
||||
diskCount: 59,
|
||||
}
|
||||
}
|
||||
name, err = b.findSANameForDisk(storage.StandardGRS)
|
||||
assert.NotEmpty(t, name)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateBlobDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
b := GetTestBlobDiskController(t)
|
||||
b.accounts = map[string]*storageAccountState{
|
||||
"ds0": {
|
||||
name: "ds0",
|
||||
saType: storage.StandardGRS,
|
||||
diskCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
mockSAClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
b.common.cloud.StorageAccountClient = mockSAClient
|
||||
mockSAClient.EXPECT().ListKeys(gomock.Any(), b.common.resourceGroup, gomock.Any()).Return(storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{
|
||||
KeyName: pointer.String("key1"),
|
||||
Value: pointer.String("key1"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
diskURI, err := b.CreateBlobDisk("datadisk", storage.StandardGRS, 10)
|
||||
expectedErr := "failed to put page blob datadisk.vhd in container vhds: storage: service returned error: StatusCode=403"
|
||||
assert.Error(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), expectedErr))
|
||||
assert.Empty(t, diskURI)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
cloudConfigNamespace = "kube-system"
|
||||
cloudConfigKey = "cloud-config"
|
||||
cloudConfigSecretName = "azure-cloud-provider"
|
||||
)
|
||||
|
||||
// The config type for Azure cloud provider secret. Supported values are:
|
||||
// * file : The values are read from local cloud-config file.
|
||||
// * secret : The values from secret would override all configures from local cloud-config file.
|
||||
// * merge : The values from secret would override only configurations that are explicitly set in the secret. This is the default value.
|
||||
type cloudConfigType string
|
||||
|
||||
const (
|
||||
cloudConfigTypeFile cloudConfigType = "file"
|
||||
cloudConfigTypeSecret cloudConfigType = "secret"
|
||||
cloudConfigTypeMerge cloudConfigType = "merge"
|
||||
)
|
||||
|
||||
// InitializeCloudFromSecret initializes Azure cloud provider from Kubernetes secret.
|
||||
func (az *Cloud) InitializeCloudFromSecret() {
|
||||
config, err := az.getConfigFromSecret()
|
||||
if err != nil {
|
||||
klog.Warningf("Failed to get cloud-config from secret: %v, skip initializing from secret", err)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
// Skip re-initialization if the config is not override.
|
||||
return
|
||||
}
|
||||
|
||||
if err := az.InitializeCloudFromConfig(config, true); err != nil {
|
||||
klog.Errorf("Failed to initialize Azure cloud provider: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (az *Cloud) getConfigFromSecret() (*Config, error) {
|
||||
// Read config from file and no override, return nil.
|
||||
if az.Config.CloudConfigType == cloudConfigTypeFile {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
secret, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Get(context.TODO(), cloudConfigSecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get secret %s: %v", cloudConfigSecretName, err)
|
||||
}
|
||||
|
||||
cloudConfigData, ok := secret.Data[cloudConfigKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cloud-config is not set in the secret (%s)", cloudConfigSecretName)
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
if az.Config.CloudConfigType == "" || az.Config.CloudConfigType == cloudConfigTypeMerge {
|
||||
// Merge cloud config, set default value to existing config.
|
||||
config = az.Config
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(cloudConfigData, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Azure cloud-config: %v", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
fakeclient "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/legacy-cloud-providers/azure/auth"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func getTestConfig() *Config {
|
||||
return &Config{
|
||||
AzureAuthConfig: auth.AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
SubscriptionID: "SubscriptionID",
|
||||
AADClientID: "AADClientID",
|
||||
AADClientSecret: "AADClientSecret",
|
||||
},
|
||||
ResourceGroup: "ResourceGroup",
|
||||
RouteTableName: "RouteTableName",
|
||||
RouteTableResourceGroup: "RouteTableResourceGroup",
|
||||
Location: "Location",
|
||||
SubnetName: "SubnetName",
|
||||
VnetName: "VnetName",
|
||||
PrimaryAvailabilitySetName: "PrimaryAvailabilitySetName",
|
||||
PrimaryScaleSetName: "PrimaryScaleSetName",
|
||||
LoadBalancerSku: "LoadBalancerSku",
|
||||
ExcludeMasterFromStandardLB: pointer.Bool(true),
|
||||
}
|
||||
}
|
||||
|
||||
func getTestCloudConfigTypeSecretConfig() *Config {
|
||||
return &Config{
|
||||
AzureAuthConfig: auth.AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
SubscriptionID: "SubscriptionID",
|
||||
},
|
||||
ResourceGroup: "ResourceGroup",
|
||||
RouteTableName: "RouteTableName",
|
||||
RouteTableResourceGroup: "RouteTableResourceGroup",
|
||||
SecurityGroupName: "SecurityGroupName",
|
||||
CloudConfigType: cloudConfigTypeSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestCloudConfigTypeMergeConfig() *Config {
|
||||
return &Config{
|
||||
AzureAuthConfig: auth.AzureAuthConfig{
|
||||
TenantID: "TenantID",
|
||||
SubscriptionID: "SubscriptionID",
|
||||
},
|
||||
ResourceGroup: "ResourceGroup",
|
||||
RouteTableName: "RouteTableName",
|
||||
RouteTableResourceGroup: "RouteTableResourceGroup",
|
||||
SecurityGroupName: "SecurityGroupName",
|
||||
CloudConfigType: cloudConfigTypeMerge,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestCloudConfigTypeMergeConfigExpected() *Config {
|
||||
config := getTestConfig()
|
||||
config.SecurityGroupName = "SecurityGroupName"
|
||||
config.CloudConfigType = cloudConfigTypeMerge
|
||||
return config
|
||||
}
|
||||
|
||||
func TestGetConfigFromSecret(t *testing.T) {
|
||||
emptyConfig := &Config{}
|
||||
badConfig := &Config{ResourceGroup: "DuplicateColumnsIncloud-config"}
|
||||
tests := []struct {
|
||||
name string
|
||||
existingConfig *Config
|
||||
secretConfig *Config
|
||||
expected *Config
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Azure config shouldn't be override when cloud config type is file",
|
||||
existingConfig: &Config{
|
||||
ResourceGroup: "ResourceGroup1",
|
||||
CloudConfigType: cloudConfigTypeFile,
|
||||
},
|
||||
secretConfig: getTestConfig(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Azure config should be override when cloud config type is secret",
|
||||
existingConfig: getTestCloudConfigTypeSecretConfig(),
|
||||
secretConfig: getTestConfig(),
|
||||
expected: getTestConfig(),
|
||||
},
|
||||
{
|
||||
name: "Azure config should be override when cloud config type is merge",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
secretConfig: getTestConfig(),
|
||||
expected: getTestCloudConfigTypeMergeConfigExpected(),
|
||||
},
|
||||
{
|
||||
name: "Error should be reported when secret doesn't exists",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error should be reported when secret exists but cloud-config data is not provided",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
secretConfig: emptyConfig,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error should be reported when it failed to parse Azure cloud-config",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
secretConfig: badConfig,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
az := &Cloud{
|
||||
KubeClient: fakeclient.NewSimpleClientset(),
|
||||
}
|
||||
if test.existingConfig != nil {
|
||||
az.Config = *test.existingConfig
|
||||
}
|
||||
if test.secretConfig != nil {
|
||||
secret := &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "azure-cloud-provider",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
}
|
||||
if test.secretConfig != emptyConfig && test.secretConfig != badConfig {
|
||||
secretData, err := yaml.Marshal(test.secretConfig)
|
||||
assert.NoError(t, err, test.name)
|
||||
secret.Data = map[string][]byte{
|
||||
"cloud-config": secretData,
|
||||
}
|
||||
}
|
||||
if test.secretConfig == badConfig {
|
||||
secret.Data = map[string][]byte{"cloud-config": []byte(`unknown: "hello",unknown: "hello"`)}
|
||||
}
|
||||
_, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Create(context.TODO(), secret, metav1.CreateOptions{})
|
||||
assert.NoError(t, err, test.name)
|
||||
}
|
||||
|
||||
real, err := az.getConfigFromSecret()
|
||||
if test.expectErr {
|
||||
assert.Error(t, err, test.name)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err, test.name)
|
||||
assert.Equal(t, test.expected, real, test.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeCloudFromSecret(t *testing.T) {
|
||||
emptyConfig := &Config{}
|
||||
unknownConfigTypeConfig := getTestConfig()
|
||||
unknownConfigTypeConfig.CloudConfigType = "UnknownConfigType"
|
||||
tests := []struct {
|
||||
name string
|
||||
existingConfig *Config
|
||||
secretConfig *Config
|
||||
expected *Config
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Azure config shouldn't be override when cloud config type is file",
|
||||
existingConfig: &Config{
|
||||
ResourceGroup: "ResourceGroup1",
|
||||
CloudConfigType: cloudConfigTypeFile,
|
||||
},
|
||||
secretConfig: getTestConfig(),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Azure config shouldn't be override when cloud config type is unknown",
|
||||
existingConfig: &Config{
|
||||
ResourceGroup: "ResourceGroup1",
|
||||
CloudConfigType: "UnknownConfigType",
|
||||
},
|
||||
secretConfig: unknownConfigTypeConfig,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Azure config should be override when cloud config type is secret",
|
||||
existingConfig: getTestCloudConfigTypeSecretConfig(),
|
||||
secretConfig: getTestConfig(),
|
||||
expected: getTestConfig(),
|
||||
},
|
||||
{
|
||||
name: "Azure config should be override when cloud config type is merge",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
secretConfig: getTestConfig(),
|
||||
expected: getTestCloudConfigTypeMergeConfigExpected(),
|
||||
},
|
||||
{
|
||||
name: "Error should be reported when secret doesn't exists",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Error should be reported when secret exists but cloud-config data is not provided",
|
||||
existingConfig: getTestCloudConfigTypeMergeConfig(),
|
||||
secretConfig: emptyConfig,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
az := &Cloud{
|
||||
KubeClient: fakeclient.NewSimpleClientset(),
|
||||
}
|
||||
if test.existingConfig != nil {
|
||||
az.Config = *test.existingConfig
|
||||
}
|
||||
if test.secretConfig != nil {
|
||||
secret := &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "azure-cloud-provider",
|
||||
Namespace: "kube-system",
|
||||
},
|
||||
}
|
||||
if test.secretConfig != emptyConfig {
|
||||
secretData, err := yaml.Marshal(test.secretConfig)
|
||||
assert.NoError(t, err, test.name)
|
||||
secret.Data = map[string][]byte{
|
||||
"cloud-config": secretData,
|
||||
}
|
||||
}
|
||||
_, err := az.KubeClient.CoreV1().Secrets(cloudConfigNamespace).Create(context.TODO(), secret, metav1.CreateOptions{})
|
||||
assert.NoError(t, err, test.name)
|
||||
}
|
||||
|
||||
az.InitializeCloudFromSecret()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kwait "k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
volerr "k8s.io/cloud-provider/volume/errors"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
const (
|
||||
// for limits check https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#storage-limits
|
||||
maxStorageAccounts = 100 // max # is 200 (250 with special request). this allows 100 for everything else including stand alone disks
|
||||
maxDisksPerStorageAccounts = 60
|
||||
storageAccountUtilizationBeforeGrowing = 0.5
|
||||
// Disk Caching is not supported for disks 4 TiB and larger
|
||||
// https://docs.microsoft.com/en-us/azure/virtual-machines/premium-storage-performance#disk-caching
|
||||
diskCachingLimit = 4096 // GiB
|
||||
|
||||
maxLUN = 64 // max number of LUNs per VM
|
||||
errLeaseIDMissing = "LeaseIdMissing"
|
||||
errContainerNotFound = "ContainerNotFound"
|
||||
errStatusCode400 = "statuscode=400"
|
||||
errInvalidParameter = `code="invalidparameter"`
|
||||
errTargetInstanceIds = `target="instanceids"`
|
||||
sourceSnapshot = "snapshot"
|
||||
sourceVolume = "volume"
|
||||
|
||||
// WriteAcceleratorEnabled support for Azure Write Accelerator on Azure Disks
|
||||
// https://docs.microsoft.com/azure/virtual-machines/windows/how-to-enable-write-accelerator
|
||||
WriteAcceleratorEnabled = "writeacceleratorenabled"
|
||||
|
||||
// see https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#create-a-managed-disk-by-copying-a-snapshot.
|
||||
diskSnapshotPath = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/snapshots/%s"
|
||||
|
||||
// see https://docs.microsoft.com/en-us/rest/api/compute/disks/createorupdate#create-a-managed-disk-from-an-existing-managed-disk-in-the-same-or-different-subscription.
|
||||
managedDiskPath = "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s"
|
||||
)
|
||||
|
||||
var defaultBackOff = kwait.Backoff{
|
||||
Steps: 20,
|
||||
Duration: 2 * time.Second,
|
||||
Factor: 1.5,
|
||||
Jitter: 0.0,
|
||||
}
|
||||
|
||||
var (
|
||||
managedDiskPathRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/disks/(.+)`)
|
||||
diskSnapshotPathRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/snapshots/(.+)`)
|
||||
)
|
||||
|
||||
type controllerCommon struct {
|
||||
subscriptionID string
|
||||
location string
|
||||
storageEndpointSuffix string
|
||||
resourceGroup string
|
||||
// store disk URI when disk is in attaching or detaching process
|
||||
diskAttachDetachMap sync.Map
|
||||
// vm disk map used to lock per vm update calls
|
||||
vmLockMap *lockMap
|
||||
cloud *Cloud
|
||||
}
|
||||
|
||||
// getNodeVMSet gets the VMSet interface based on config.VMType and the real virtual machine type.
|
||||
func (c *controllerCommon) getNodeVMSet(nodeName types.NodeName, crt azcache.AzureCacheReadType) (VMSet, error) {
|
||||
// 1. vmType is standard, return cloud.VMSet directly.
|
||||
if c.cloud.VMType == vmTypeStandard {
|
||||
return c.cloud.VMSet, nil
|
||||
}
|
||||
|
||||
// 2. vmType is Virtual Machine Scale Set (vmss), convert vmSet to scaleSet.
|
||||
ss, ok := c.cloud.VMSet.(*scaleSet)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error of converting vmSet (%q) to scaleSet with vmType %q", c.cloud.VMSet, c.cloud.VMType)
|
||||
}
|
||||
|
||||
// 3. If the node is managed by availability set, then return ss.availabilitySet.
|
||||
managedByAS, err := ss.isNodeManagedByAvailabilitySet(mapNodeNameToVMName(nodeName), crt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if managedByAS {
|
||||
// vm is managed by availability set.
|
||||
return ss.availabilitySet, nil
|
||||
}
|
||||
|
||||
// 4. Node is managed by vmss
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// AttachDisk attaches a vhd to vm. The vhd must exist, can be identified by diskName, diskURI.
|
||||
// return (lun, error)
|
||||
func (c *controllerCommon) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, cachingMode compute.CachingTypes) (int32, error) {
|
||||
diskEncryptionSetID := ""
|
||||
writeAcceleratorEnabled := false
|
||||
|
||||
vmset, err := c.getNodeVMSet(nodeName, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if isManagedDisk {
|
||||
diskName := path.Base(diskURI)
|
||||
resourceGroup, err := getResourceGroupFromDiskURI(diskURI)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
disk, rerr := c.cloud.DisksClient.Get(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
return -1, rerr.Error()
|
||||
}
|
||||
|
||||
if disk.ManagedBy != nil && (disk.MaxShares == nil || *disk.MaxShares <= 1) {
|
||||
attachErr := fmt.Sprintf(
|
||||
"disk(%s) already attached to node(%s), could not be attached to node(%s)",
|
||||
diskURI, *disk.ManagedBy, nodeName)
|
||||
attachedNode, err := vmset.GetNodeNameByProviderID(*disk.ManagedBy)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
klog.V(2).Infof("found dangling volume %s attached to node %s", diskURI, attachedNode)
|
||||
danglingErr := volerr.NewDanglingError(attachErr, attachedNode, "")
|
||||
return -1, danglingErr
|
||||
}
|
||||
|
||||
if disk.DiskProperties != nil {
|
||||
if disk.DiskProperties.DiskSizeGB != nil && *disk.DiskProperties.DiskSizeGB >= diskCachingLimit && cachingMode != compute.CachingTypesNone {
|
||||
// Disk Caching is not supported for disks 4 TiB and larger
|
||||
// https://docs.microsoft.com/en-us/azure/virtual-machines/premium-storage-performance#disk-caching
|
||||
cachingMode = compute.CachingTypesNone
|
||||
klog.Warningf("size of disk(%s) is %dGB which is bigger than limit(%dGB), set cacheMode as None",
|
||||
diskURI, *disk.DiskProperties.DiskSizeGB, diskCachingLimit)
|
||||
}
|
||||
|
||||
if disk.DiskProperties.Encryption != nil &&
|
||||
disk.DiskProperties.Encryption.DiskEncryptionSetID != nil {
|
||||
diskEncryptionSetID = *disk.DiskProperties.Encryption.DiskEncryptionSetID
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := disk.Tags[WriteAcceleratorEnabled]; ok {
|
||||
if v != nil && strings.EqualFold(*v, "true") {
|
||||
writeAcceleratorEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instanceid, err := c.cloud.InstanceID(context.TODO(), nodeName)
|
||||
if err != nil {
|
||||
klog.Warningf("failed to get azure instance id (%v) for node %s", err, nodeName)
|
||||
return -1, fmt.Errorf("failed to get azure instance id for node %q (%v)", nodeName, err)
|
||||
}
|
||||
|
||||
c.vmLockMap.LockEntry(strings.ToLower(string(nodeName)))
|
||||
defer c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName)))
|
||||
|
||||
lun, err := c.GetNextDiskLun(nodeName)
|
||||
if err != nil {
|
||||
klog.Warningf("no LUN available for instance %q (%v)", nodeName, err)
|
||||
return -1, fmt.Errorf("all LUNs are used, cannot attach volume (%s, %s) to instance %q (%v)", diskName, diskURI, instanceid, err)
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Trying to attach volume %q lun %d to node %q.", diskURI, lun, nodeName)
|
||||
c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "attaching")
|
||||
defer c.diskAttachDetachMap.Delete(strings.ToLower(diskURI))
|
||||
return lun, vmset.AttachDisk(isManagedDisk, diskName, diskURI, nodeName, lun, cachingMode, diskEncryptionSetID, writeAcceleratorEnabled)
|
||||
}
|
||||
|
||||
// DetachDisk detaches a disk from host. The vhd can be identified by diskName or diskURI.
|
||||
func (c *controllerCommon) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error {
|
||||
_, err := c.cloud.InstanceID(context.TODO(), nodeName)
|
||||
if err != nil {
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
// if host doesn't exist, no need to detach
|
||||
klog.Warningf("azureDisk - failed to get azure instance id(%q), DetachDisk(%s) will assume disk is already detached",
|
||||
nodeName, diskURI)
|
||||
return nil
|
||||
}
|
||||
klog.Warningf("failed to get azure instance id (%v)", err)
|
||||
return fmt.Errorf("failed to get azure instance id for node %q (%v)", nodeName, err)
|
||||
}
|
||||
|
||||
vmset, err := c.getNodeVMSet(nodeName, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(2).Infof("detach %v from node %q", diskURI, nodeName)
|
||||
|
||||
// make the lock here as small as possible
|
||||
c.vmLockMap.LockEntry(strings.ToLower(string(nodeName)))
|
||||
c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "detaching")
|
||||
err = vmset.DetachDisk(diskName, diskURI, nodeName)
|
||||
c.diskAttachDetachMap.Delete(strings.ToLower(diskURI))
|
||||
c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName)))
|
||||
|
||||
if err != nil {
|
||||
if isInstanceNotFoundError(err) {
|
||||
// if host doesn't exist, no need to detach
|
||||
klog.Warningf("azureDisk - got InstanceNotFoundError(%v), DetachDisk(%s) will assume disk is already detached",
|
||||
err, diskURI)
|
||||
return nil
|
||||
}
|
||||
if retry.IsErrorRetriable(err) && c.cloud.CloudProviderBackoff {
|
||||
klog.Warningf("azureDisk - update backing off: detach disk(%s, %s), err: %v", diskName, diskURI, err)
|
||||
retryErr := kwait.ExponentialBackoff(c.cloud.RequestBackoff(), func() (bool, error) {
|
||||
c.vmLockMap.LockEntry(strings.ToLower(string(nodeName)))
|
||||
c.diskAttachDetachMap.Store(strings.ToLower(diskURI), "detaching")
|
||||
err := vmset.DetachDisk(diskName, diskURI, nodeName)
|
||||
c.diskAttachDetachMap.Delete(strings.ToLower(diskURI))
|
||||
c.vmLockMap.UnlockEntry(strings.ToLower(string(nodeName)))
|
||||
|
||||
retriable := false
|
||||
if err != nil && retry.IsErrorRetriable(err) {
|
||||
retriable = true
|
||||
}
|
||||
return !retriable, err
|
||||
})
|
||||
if retryErr != nil {
|
||||
err = retryErr
|
||||
klog.V(2).Infof("azureDisk - update abort backoff: detach disk(%s, %s), err: %v", diskName, diskURI, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
klog.Errorf("azureDisk - detach disk(%s, %s) failed, err: %v", diskName, diskURI, err)
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - detach disk(%s, %s) succeeded", diskName, diskURI)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getNodeDataDisks invokes vmSet interfaces to get data disks for the node.
|
||||
func (c *controllerCommon) getNodeDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) {
|
||||
vmset, err := c.getNodeVMSet(nodeName, crt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vmset.GetDataDisks(nodeName, crt)
|
||||
}
|
||||
|
||||
// GetDiskLun finds the lun on the host that the vhd is attached to, given a vhd's diskName and diskURI.
|
||||
func (c *controllerCommon) GetDiskLun(diskName, diskURI string, nodeName types.NodeName) (int32, error) {
|
||||
// getNodeDataDisks need to fetch the cached data/fresh data if cache expired here
|
||||
// to ensure we get LUN based on latest entry.
|
||||
disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
klog.Errorf("error of getting data disks for node %q: %v", nodeName, err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
for _, disk := range disks {
|
||||
if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) ||
|
||||
(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) ||
|
||||
(disk.ManagedDisk != nil && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) {
|
||||
if disk.ToBeDetached != nil && *disk.ToBeDetached {
|
||||
klog.Warningf("azureDisk - find disk(ToBeDetached): lun %d name %q uri %q", *disk.Lun, diskName, diskURI)
|
||||
} else {
|
||||
// found the disk
|
||||
klog.V(2).Infof("azureDisk - find disk: lun %d name %q uri %q", *disk.Lun, diskName, diskURI)
|
||||
return *disk.Lun, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1, fmt.Errorf("cannot find Lun for disk %s", diskName)
|
||||
}
|
||||
|
||||
// GetNextDiskLun searches all vhd attachment on the host and find unused lun. Return -1 if all luns are used.
|
||||
func (c *controllerCommon) GetNextDiskLun(nodeName types.NodeName) (int32, error) {
|
||||
disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
klog.Errorf("error of getting data disks for node %q: %v", nodeName, err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
used := make([]bool, maxLUN)
|
||||
for _, disk := range disks {
|
||||
if disk.Lun != nil {
|
||||
used[*disk.Lun] = true
|
||||
}
|
||||
}
|
||||
for k, v := range used {
|
||||
if !v {
|
||||
return int32(k), nil
|
||||
}
|
||||
}
|
||||
return -1, fmt.Errorf("all luns are used")
|
||||
}
|
||||
|
||||
// DisksAreAttached checks if a list of volumes are attached to the node with the specified NodeName.
|
||||
func (c *controllerCommon) DisksAreAttached(diskNames []string, nodeName types.NodeName) (map[string]bool, error) {
|
||||
attached := make(map[string]bool)
|
||||
for _, diskName := range diskNames {
|
||||
attached[diskName] = false
|
||||
}
|
||||
|
||||
// doing stalled read for getNodeDataDisks to ensure we don't call ARM
|
||||
// for every reconcile call. The cache is invalidated after Attach/Detach
|
||||
// disk. So the new entry will be fetched and cached the first time reconcile
|
||||
// loop runs after the Attach/Disk OP which will reflect the latest model.
|
||||
disks, err := c.getNodeDataDisks(nodeName, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
// if host doesn't exist, no need to detach
|
||||
klog.Warningf("azureDisk - Cannot find node %q, DisksAreAttached will assume disks %v are not attached to it.",
|
||||
nodeName, diskNames)
|
||||
return attached, nil
|
||||
}
|
||||
|
||||
return attached, err
|
||||
}
|
||||
|
||||
for _, disk := range disks {
|
||||
for _, diskName := range diskNames {
|
||||
if disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName) {
|
||||
attached[diskName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attached, nil
|
||||
}
|
||||
|
||||
func filterDetachingDisks(unfilteredDisks []compute.DataDisk) []compute.DataDisk {
|
||||
filteredDisks := []compute.DataDisk{}
|
||||
for _, disk := range unfilteredDisks {
|
||||
if disk.ToBeDetached != nil && *disk.ToBeDetached {
|
||||
if disk.Name != nil {
|
||||
klog.V(2).Infof("Filtering disk: %s with ToBeDetached flag set.", *disk.Name)
|
||||
}
|
||||
} else {
|
||||
filteredDisks = append(filteredDisks, disk)
|
||||
}
|
||||
}
|
||||
return filteredDisks
|
||||
}
|
||||
|
||||
func (c *controllerCommon) filterNonExistingDisks(ctx context.Context, unfilteredDisks []compute.DataDisk) []compute.DataDisk {
|
||||
filteredDisks := []compute.DataDisk{}
|
||||
for _, disk := range unfilteredDisks {
|
||||
filter := false
|
||||
if disk.ManagedDisk != nil && disk.ManagedDisk.ID != nil {
|
||||
diskURI := *disk.ManagedDisk.ID
|
||||
exist, err := c.cloud.checkDiskExists(ctx, diskURI)
|
||||
if err != nil {
|
||||
klog.Errorf("checkDiskExists(%s) failed with error: %v", diskURI, err)
|
||||
} else {
|
||||
// only filter disk when checkDiskExists returns <false, nil>
|
||||
filter = !exist
|
||||
if filter {
|
||||
klog.Errorf("disk(%s) does not exist, removed from data disk list", diskURI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !filter {
|
||||
filteredDisks = append(filteredDisks, disk)
|
||||
}
|
||||
}
|
||||
return filteredDisks
|
||||
}
|
||||
|
||||
func (c *controllerCommon) checkDiskExists(ctx context.Context, diskURI string) (bool, error) {
|
||||
diskName := path.Base(diskURI)
|
||||
resourceGroup, err := getResourceGroupFromDiskURI(diskURI)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, rerr := c.cloud.DisksClient.Get(ctx, resourceGroup, diskName); rerr != nil {
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, rerr.Error()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getValidCreationData(subscriptionID, resourceGroup, sourceResourceID, sourceType string) (compute.CreationData, error) {
|
||||
if sourceResourceID == "" {
|
||||
return compute.CreationData{
|
||||
CreateOption: compute.Empty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
switch sourceType {
|
||||
case sourceSnapshot:
|
||||
if match := diskSnapshotPathRE.FindString(sourceResourceID); match == "" {
|
||||
sourceResourceID = fmt.Sprintf(diskSnapshotPath, subscriptionID, resourceGroup, sourceResourceID)
|
||||
}
|
||||
|
||||
case sourceVolume:
|
||||
if match := managedDiskPathRE.FindString(sourceResourceID); match == "" {
|
||||
sourceResourceID = fmt.Sprintf(managedDiskPath, subscriptionID, resourceGroup, sourceResourceID)
|
||||
}
|
||||
default:
|
||||
return compute.CreationData{
|
||||
CreateOption: compute.Empty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
splits := strings.Split(sourceResourceID, "/")
|
||||
if len(splits) > 9 {
|
||||
if sourceType == sourceSnapshot {
|
||||
return compute.CreationData{}, fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", sourceResourceID, diskSnapshotPathRE)
|
||||
}
|
||||
return compute.CreationData{}, fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", sourceResourceID, managedDiskPathRE)
|
||||
}
|
||||
return compute.CreationData{
|
||||
CreateOption: compute.Copy,
|
||||
SourceResourceID: &sourceResourceID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isInstanceNotFoundError(err error) bool {
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if strings.Contains(errMsg, strings.ToLower(vmssVMNotActiveErrorMessage)) {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(errMsg, errStatusCode400) && strings.Contains(errMsg, errInvalidParameter) && strings.Contains(errMsg, errTargetInstanceIds)
|
||||
}
|
||||
@@ -1,824 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestCommonAttachDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
maxShare := int32(1)
|
||||
goodInstanceID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/%s", "vm1")
|
||||
diskEncryptionSetID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/%s", "diskEncryptionSet-name")
|
||||
testTags := make(map[string]*string)
|
||||
testTags[WriteAcceleratorEnabled] = pointer.String("true")
|
||||
testCases := []struct {
|
||||
desc string
|
||||
vmList map[string]string
|
||||
nodeName types.NodeName
|
||||
isDataDisksFull bool
|
||||
isBadDiskURI bool
|
||||
isDiskUsed bool
|
||||
existedDisk compute.Disk
|
||||
expectedLun int32
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "LUN -1 and error shall be returned if there's no such instance corresponding to given nodeName",
|
||||
nodeName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "LUN -1 and error shall be returned if there's no available LUN for instance",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
isDataDisksFull: true,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "correct LUN and no error shall be returned if everything is good",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name"),
|
||||
DiskProperties: &compute.DiskProperties{
|
||||
Encryption: &compute.Encryption{DiskEncryptionSetID: &diskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey},
|
||||
DiskSizeGB: pointer.Int32(4096),
|
||||
},
|
||||
Tags: testTags},
|
||||
expectedLun: 1,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if there's invalid disk uri",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
isBadDiskURI: true,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if attach an already attached disk with good ManagedBy instance id",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name"), ManagedBy: pointer.String(goodInstanceID), DiskProperties: &compute.DiskProperties{MaxShares: &maxShare}},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if attach an already attached disk with bad ManagedBy instance id",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name"), ManagedBy: pointer.String("test"), DiskProperties: &compute.DiskProperties{MaxShares: &maxShare}},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if there's no matching disk",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name-1")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
if test.isBadDiskURI {
|
||||
diskURI = fmt.Sprintf("/baduri/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(testCloud, test.vmList, test.isDataDisksFull)
|
||||
if test.isDiskUsed {
|
||||
vm0 := setTestVirtualMachines(testCloud, map[string]string{"vm0": "PowerState/Running"}, test.isDataDisksFull)[0]
|
||||
expectedVMs = append(expectedVMs, vm0)
|
||||
}
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
if len(expectedVMs) == 0 {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "disk-name").Return(test.existedDisk, nil).AnyTimes()
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("disk-name")).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
lun, err := common.AttachDisk(true, "", diskURI, test.nodeName, compute.CachingTypesReadOnly)
|
||||
assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonAttachDiskWithVMSS(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
vmList map[string]string
|
||||
vmssList []string
|
||||
nodeName types.NodeName
|
||||
isVMSS bool
|
||||
isManagedBy bool
|
||||
isManagedDisk bool
|
||||
isDataDisksFull bool
|
||||
existedDisk compute.Disk
|
||||
expectedLun int32
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if convert vmSet to scaleSet failed",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
isVMSS: false,
|
||||
isManagedBy: false,
|
||||
isManagedDisk: false,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if convert vmSet to scaleSet success but node is not managed by availability set",
|
||||
vmssList: []string{"vmss-vm-000001"},
|
||||
nodeName: "vmss1",
|
||||
isVMSS: true,
|
||||
isManagedBy: false,
|
||||
isManagedDisk: false,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
testCloud.VMType = vmTypeVMSS
|
||||
if test.isVMSS {
|
||||
if test.isManagedBy {
|
||||
testCloud.DisableAvailabilitySetNodes = false
|
||||
testVMSSName := "vmss"
|
||||
expectedVMSS := compute.VirtualMachineScaleSet{Name: pointer.String(testVMSSName)}
|
||||
mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes()
|
||||
|
||||
expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, testVMSSName, "", 0, test.vmssList, "", false)
|
||||
mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, testVMSSName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes()
|
||||
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMsClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachine{}, nil).AnyTimes()
|
||||
} else {
|
||||
testCloud.DisableAvailabilitySetNodes = true
|
||||
}
|
||||
ss, err := newScaleSet(testCloud)
|
||||
assert.NoError(t, err)
|
||||
testCloud.VMSet = ss
|
||||
}
|
||||
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
if !test.isVMSS {
|
||||
expectedVMs := setTestVirtualMachines(testCloud, test.vmList, test.isDataDisksFull)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
if len(expectedVMs) == 0 {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "disk-name").Return(test.existedDisk, nil).AnyTimes()
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("disk-name")).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
}
|
||||
|
||||
lun, err := common.AttachDisk(test.isManagedDisk, "test", diskURI, test.nodeName, compute.CachingTypesReadOnly)
|
||||
assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonDetachDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
vmList map[string]string
|
||||
nodeName types.NodeName
|
||||
diskName string
|
||||
isErrorRetriable bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "error should not be returned if there's no such instance corresponding to given nodeName",
|
||||
nodeName: "vm1",
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error should be returned if vmset detach failed with isErrorRetriable error",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
isErrorRetriable: true,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if there's no matching disk according to given diskName",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
diskName: "disk2",
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if the disk exists",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
diskName: "disk1",
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/disk-name",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup)
|
||||
expectedVMs := setTestVirtualMachines(testCloud, test.vmList, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
if len(expectedVMs) == 0 {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
}
|
||||
if test.isErrorRetriable {
|
||||
testCloud.CloudProviderBackoff = true
|
||||
testCloud.ResourceRequestBackoff = wait.Backoff{Steps: 1}
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusBadRequest, Retriable: true, RawError: fmt.Errorf("Retriable: true")}).AnyTimes()
|
||||
} else {
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
err := common.DetachDisk(test.diskName, diskURI, test.nodeName)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDiskLun(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskName string
|
||||
diskURI string
|
||||
expectedLun int32
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "LUN -1 and error shall be returned if diskName != disk.Name or diskURI != disk.Vhd.URI",
|
||||
diskName: "disk2",
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "correct LUN and no error shall be returned if diskName = disk.Name",
|
||||
diskName: "disk1",
|
||||
expectedLun: 0,
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
|
||||
lun, err := common.GetDiskLun(test.diskName, test.diskURI, "vm1")
|
||||
assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextDiskLun(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
isDataDisksFull bool
|
||||
expectedLun int32
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "the minimal LUN shall be returned if there's enough room for extra disks",
|
||||
isDataDisksFull: false,
|
||||
expectedLun: 1,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "LUN -1 and error shall be returned if there's no available LUN",
|
||||
isDataDisksFull: true,
|
||||
expectedLun: -1,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, test.isDataDisksFull)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
|
||||
lun, err := common.GetNextDiskLun("vm1")
|
||||
assert.Equal(t, test.expectedLun, lun, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisksAreAttached(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskNames []string
|
||||
nodeName types.NodeName
|
||||
expectedAttached map[string]bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if there's no such instance corresponding to given nodeName",
|
||||
diskNames: []string{"disk1"},
|
||||
nodeName: "vm2",
|
||||
expectedAttached: map[string]bool{"disk1": false},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "proper attach map shall be returned if everything is good",
|
||||
diskNames: []string{"disk1", "disk2"},
|
||||
nodeName: "vm1",
|
||||
expectedAttached: map[string]bool{"disk1": true, "disk2": false},
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
attached, err := common.DisksAreAttached(test.diskNames, test.nodeName)
|
||||
assert.Equal(t, test.expectedAttached, attached, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredDetachingDisks(t *testing.T) {
|
||||
|
||||
disks := []compute.DataDisk{
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName1"),
|
||||
ToBeDetached: pointer.BoolPtr(false),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.StringPtr("ManagedID"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName2"),
|
||||
ToBeDetached: pointer.BoolPtr(true),
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName3"),
|
||||
ToBeDetached: nil,
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName4"),
|
||||
ToBeDetached: nil,
|
||||
},
|
||||
}
|
||||
|
||||
filteredDisks := filterDetachingDisks(disks)
|
||||
assert.Equal(t, 3, len(filteredDisks))
|
||||
assert.Equal(t, "DiskName1", *filteredDisks[0].Name)
|
||||
assert.Equal(t, "ManagedID", *filteredDisks[0].ManagedDisk.ID)
|
||||
assert.Equal(t, "DiskName3", *filteredDisks[1].Name)
|
||||
|
||||
disks = []compute.DataDisk{}
|
||||
filteredDisks = filterDetachingDisks(disks)
|
||||
assert.Equal(t, 0, len(filteredDisks))
|
||||
}
|
||||
|
||||
func TestGetValidCreationData(t *testing.T) {
|
||||
sourceResourceSnapshotID := "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx"
|
||||
sourceResourceVolumeID := "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/xxx"
|
||||
|
||||
tests := []struct {
|
||||
subscriptionID string
|
||||
resourceGroup string
|
||||
sourceResourceID string
|
||||
sourceType string
|
||||
expected1 compute.CreationData
|
||||
expected2 error
|
||||
}{
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "",
|
||||
sourceType: "",
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Empty,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Copy,
|
||||
SourceResourceID: &sourceResourceSnapshotID,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "xxx",
|
||||
resourceGroup: "xxx",
|
||||
sourceResourceID: "xxx",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Copy,
|
||||
SourceResourceID: &sourceResourceSnapshotID,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/23/providers/Microsoft.Compute/disks/name",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{},
|
||||
expected2: fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots//subscriptions/23/providers/Microsoft.Compute/disks/name", diskSnapshotPathRE),
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "http://test.com/vhds/name",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{},
|
||||
expected2: fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots/http://test.com/vhds/name", diskSnapshotPathRE),
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/xxx/snapshots/xxx",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{},
|
||||
expected2: fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/snapshots//subscriptions/xxx/snapshots/xxx", diskSnapshotPathRE),
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx/snapshots/xxx/snapshots/xxx",
|
||||
sourceType: sourceSnapshot,
|
||||
expected1: compute.CreationData{},
|
||||
expected2: fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx/snapshots/xxx/snapshots/xxx", diskSnapshotPathRE),
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "xxx",
|
||||
sourceType: "",
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Empty,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/xxx",
|
||||
sourceType: sourceVolume,
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Copy,
|
||||
SourceResourceID: &sourceResourceVolumeID,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "xxx",
|
||||
resourceGroup: "xxx",
|
||||
sourceResourceID: "xxx",
|
||||
sourceType: sourceVolume,
|
||||
expected1: compute.CreationData{
|
||||
CreateOption: compute.Copy,
|
||||
SourceResourceID: &sourceResourceVolumeID,
|
||||
},
|
||||
expected2: nil,
|
||||
},
|
||||
{
|
||||
subscriptionID: "",
|
||||
resourceGroup: "",
|
||||
sourceResourceID: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx",
|
||||
sourceType: sourceVolume,
|
||||
expected1: compute.CreationData{},
|
||||
expected2: fmt.Errorf("sourceResourceID(%s) is invalid, correct format: %s", "/subscriptions//resourceGroups//providers/Microsoft.Compute/disks//subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/snapshots/xxx", managedDiskPathRE),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result, err := getValidCreationData(test.subscriptionID, test.resourceGroup, test.sourceResourceID, test.sourceType)
|
||||
if !reflect.DeepEqual(result, test.expected1) || !reflect.DeepEqual(err, test.expected2) {
|
||||
t.Errorf("input sourceResourceID: %v, sourceType: %v, getValidCreationData result: %v, expected1 : %v, err: %v, expected2: %v", test.sourceResourceID, test.sourceType, result, test.expected1, err, test.expected2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiskExists(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
// create a new disk before running test
|
||||
newDiskName := "newdisk"
|
||||
newDiskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, newDiskName)
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, newDiskName).Return(compute.Disk{}, nil).AnyTimes()
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), gomock.Not(testCloud.ResourceGroup), gomock.Any()).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
testCases := []struct {
|
||||
diskURI string
|
||||
expectedResult bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
diskURI: "incorrect disk URI format",
|
||||
expectedResult: false,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
diskURI: "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Compute/disks/non-existing-disk",
|
||||
expectedResult: false,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
diskURI: newDiskURI,
|
||||
expectedResult: true,
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
exist, err := common.checkDiskExists(ctx, test.diskURI)
|
||||
assert.Equal(t, test.expectedResult, exist, "TestCase[%d]", i, exist)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d], return error: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterNonExistingDisks(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
// create a new disk before running test
|
||||
diskURIPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup)
|
||||
newDiskName := "newdisk"
|
||||
newDiskURI := diskURIPrefix + newDiskName
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, newDiskName).Return(compute.Disk{}, nil).AnyTimes()
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not(newDiskName)).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
disks := []compute.DataDisk{
|
||||
{
|
||||
Name: &newDiskName,
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: &newDiskURI,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName2"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.StringPtr(diskURIPrefix + "DiskName2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName3"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.StringPtr(diskURIPrefix + "DiskName3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.StringPtr("DiskName4"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.StringPtr(diskURIPrefix + "DiskName4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
filteredDisks := common.filterNonExistingDisks(ctx, disks)
|
||||
assert.Equal(t, 1, len(filteredDisks))
|
||||
assert.Equal(t, newDiskName, *filteredDisks[0].Name)
|
||||
|
||||
disks = []compute.DataDisk{}
|
||||
filteredDisks = filterDetachingDisks(disks)
|
||||
assert.Equal(t, 0, len(filteredDisks))
|
||||
}
|
||||
|
||||
func TestFilterNonExistingDisksWithSpecialHTTPStatusCode(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
common := &controllerCommon{
|
||||
location: testCloud.Location,
|
||||
storageEndpointSuffix: testCloud.Environment.StorageEndpointSuffix,
|
||||
resourceGroup: testCloud.ResourceGroup,
|
||||
subscriptionID: testCloud.SubscriptionID,
|
||||
cloud: testCloud,
|
||||
vmLockMap: newLockMap(),
|
||||
}
|
||||
// create a new disk before running test
|
||||
diskURIPrefix := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup)
|
||||
newDiskName := "specialdisk"
|
||||
newDiskURI := diskURIPrefix + newDiskName
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Eq(newDiskName)).Return(compute.Disk{}, &retry.Error{HTTPStatusCode: http.StatusBadRequest, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
disks := []compute.DataDisk{
|
||||
{
|
||||
Name: &newDiskName,
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: &newDiskURI,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
filteredDisks := common.filterNonExistingDisks(ctx, disks)
|
||||
assert.Equal(t, 1, len(filteredDisks))
|
||||
assert.Equal(t, newDiskName, *filteredDisks[0].Name)
|
||||
}
|
||||
|
||||
func TestIsInstanceNotFoundError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
errMsg string
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
errMsg: "",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
errMsg: "other error",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
errMsg: "The provided instanceId 857 is not an active Virtual Machine Scale Set VM instanceId.",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
errMsg: `compute.VirtualMachineScaleSetVMsClient#Update: Failure sending request: StatusCode=400 -- Original Error: Code="InvalidParameter" Message="The provided instanceId 1181 is not an active Virtual Machine Scale Set VM instanceId." Target="instanceIds"`,
|
||||
expectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
result := isInstanceNotFoundError(fmt.Errorf(test.errMsg))
|
||||
assert.Equal(t, test.expectedResult, result, "TestCase[%d]", i, result)
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// AttachDisk attaches a vhd to vm
|
||||
// the vhd must exist, can be identified by diskName, diskURI, and lun.
|
||||
func (as *availabilitySet) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error {
|
||||
vm, err := as.getVirtualMachine(nodeName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vmName := mapNodeNameToVMName(nodeName)
|
||||
nodeResourceGroup, err := as.GetNodeResourceGroup(vmName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks))
|
||||
copy(disks, *vm.StorageProfile.DataDisks)
|
||||
|
||||
if isManagedDisk {
|
||||
managedDisk := &compute.ManagedDiskParameters{ID: &diskURI}
|
||||
if diskEncryptionSetID == "" {
|
||||
if vm.StorageProfile.OsDisk != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID != nil {
|
||||
// set diskEncryptionSet as value of os disk by default
|
||||
diskEncryptionSetID = *vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID
|
||||
}
|
||||
}
|
||||
if diskEncryptionSetID != "" {
|
||||
managedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: &diskEncryptionSetID}
|
||||
}
|
||||
disks = append(disks,
|
||||
compute.DataDisk{
|
||||
Name: &diskName,
|
||||
Lun: &lun,
|
||||
Caching: cachingMode,
|
||||
CreateOption: "attach",
|
||||
ManagedDisk: managedDisk,
|
||||
WriteAcceleratorEnabled: pointer.Bool(writeAcceleratorEnabled),
|
||||
})
|
||||
} else {
|
||||
disks = append(disks,
|
||||
compute.DataDisk{
|
||||
Name: &diskName,
|
||||
Vhd: &compute.VirtualHardDisk{
|
||||
URI: &diskURI,
|
||||
},
|
||||
Lun: &lun,
|
||||
Caching: cachingMode,
|
||||
CreateOption: "attach",
|
||||
})
|
||||
}
|
||||
|
||||
newVM := compute.VirtualMachineUpdate{
|
||||
VirtualMachineProperties: &compute.VirtualMachineProperties{
|
||||
StorageProfile: &compute.StorageProfile{
|
||||
DataDisks: &disks,
|
||||
},
|
||||
},
|
||||
}
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) with DiskEncryptionSetID(%s)", nodeResourceGroup, vmName, diskName, diskURI, diskEncryptionSetID)
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
// Invalidate the cache right after updating
|
||||
defer as.cloud.vmCache.Delete(vmName)
|
||||
|
||||
rerr := as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "attach_disk")
|
||||
if rerr != nil {
|
||||
klog.Errorf("azureDisk - attach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, vmName, rerr)
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, vmName)
|
||||
disks := as.filterNonExistingDisks(ctx, *newVM.VirtualMachineProperties.StorageProfile.DataDisks)
|
||||
newVM.VirtualMachineProperties.StorageProfile.DataDisks = &disks
|
||||
rerr = as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "attach_disk")
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) returned with %v", nodeResourceGroup, vmName, diskName, diskURI, rerr)
|
||||
if rerr != nil {
|
||||
return rerr.Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetachDisk detaches a disk from host
|
||||
// the vhd can be identified by diskName or diskURI
|
||||
func (as *availabilitySet) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error {
|
||||
vm, err := as.getVirtualMachine(nodeName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
// if host doesn't exist, no need to detach
|
||||
klog.Warningf("azureDisk - cannot find node %s, skip detaching disk(%s, %s)", nodeName, diskName, diskURI)
|
||||
return nil
|
||||
}
|
||||
|
||||
vmName := mapNodeNameToVMName(nodeName)
|
||||
nodeResourceGroup, err := as.GetNodeResourceGroup(vmName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks))
|
||||
copy(disks, *vm.StorageProfile.DataDisks)
|
||||
|
||||
bFoundDisk := false
|
||||
for i, disk := range disks {
|
||||
if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) ||
|
||||
(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) ||
|
||||
(disk.ManagedDisk != nil && diskURI != "" && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) {
|
||||
// found the disk
|
||||
klog.V(2).Infof("azureDisk - detach disk: name %q uri %q", diskName, diskURI)
|
||||
if strings.EqualFold(as.cloud.Environment.Name, AzureStackCloudName) {
|
||||
disks = append(disks[:i], disks[i+1:]...)
|
||||
} else {
|
||||
disks[i].ToBeDetached = pointer.Bool(true)
|
||||
}
|
||||
bFoundDisk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !bFoundDisk {
|
||||
// only log here, next action is to update VM status with original meta data
|
||||
klog.Errorf("detach azure disk: disk %s not found, diskURI: %s", diskName, diskURI)
|
||||
}
|
||||
|
||||
newVM := compute.VirtualMachineUpdate{
|
||||
VirtualMachineProperties: &compute.VirtualMachineProperties{
|
||||
StorageProfile: &compute.StorageProfile{
|
||||
DataDisks: &disks,
|
||||
},
|
||||
},
|
||||
}
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s)", nodeResourceGroup, vmName, diskName, diskURI)
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
// Invalidate the cache right after updating
|
||||
defer as.cloud.vmCache.Delete(vmName)
|
||||
|
||||
rerr := as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "detach_disk")
|
||||
if rerr != nil {
|
||||
klog.Errorf("azureDisk - detach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, vmName, rerr)
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, vmName)
|
||||
disks := as.filterNonExistingDisks(ctx, *vm.StorageProfile.DataDisks)
|
||||
newVM.VirtualMachineProperties.StorageProfile.DataDisks = &disks
|
||||
rerr = as.VirtualMachinesClient.Update(ctx, nodeResourceGroup, vmName, newVM, "detach_disk")
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s) returned with %v", nodeResourceGroup, vmName, diskName, diskURI, rerr)
|
||||
if rerr != nil {
|
||||
return rerr.Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDataDisks gets a list of data disks attached to the node.
|
||||
func (as *availabilitySet) GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) {
|
||||
vm, err := as.getVirtualMachine(nodeName, crt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vm.StorageProfile.DataDisks == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return *vm.StorageProfile.DataDisks, nil
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeCacheTTL = 2 * time.Second
|
||||
)
|
||||
|
||||
func TestStandardAttachDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
nodeName types.NodeName
|
||||
isManagedDisk bool
|
||||
isAttachFail bool
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if there's no corresponding vms",
|
||||
nodeName: "vm2",
|
||||
isManagedDisk: true,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything's good",
|
||||
nodeName: "vm1",
|
||||
isManagedDisk: true,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything's good with non managed disk",
|
||||
nodeName: "vm1",
|
||||
isManagedDisk: false,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if update attach disk failed",
|
||||
nodeName: "vm1",
|
||||
isManagedDisk: true,
|
||||
isAttachFail: true,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
vmSet := testCloud.VMSet
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
vm.StorageProfile = &compute.StorageProfile{
|
||||
OsDisk: &compute.OSDisk{
|
||||
Name: pointer.String("osdisk1"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.String("ManagedID"),
|
||||
DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
|
||||
ID: pointer.String("DiskEncryptionSetID"),
|
||||
},
|
||||
},
|
||||
},
|
||||
DataDisks: &[]compute.DataDisk{},
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
if test.isAttachFail {
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
} else {
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
err := vmSet.AttachDisk(test.isManagedDisk, "",
|
||||
"uri", test.nodeName, 0, compute.CachingTypesReadOnly, "", false)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandardDetachDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
nodeName types.NodeName
|
||||
diskName string
|
||||
isDetachFail bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "no error shall be returned if there's no corresponding vm",
|
||||
nodeName: "vm2",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if there's no corresponding disk",
|
||||
nodeName: "vm1",
|
||||
diskName: "disk2",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if there's a corresponding disk",
|
||||
nodeName: "vm1",
|
||||
diskName: "disk1",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if detach disk failed",
|
||||
nodeName: "vm1",
|
||||
isDetachFail: true,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
vmSet := testCloud.VMSet
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
if test.isDetachFail {
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
} else {
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
err := vmSet.DetachDisk(test.diskName, "", test.nodeName)
|
||||
assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
if !test.expectedError && test.diskName != "" {
|
||||
dataDisks, err := vmSet.GetDataDisks(test.nodeName, azcache.CacheReadTypeDefault)
|
||||
assert.Equal(t, true, len(dataDisks) == 1, "TestCase[%d]: %s, err: %v", i, test.desc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDataDisks(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
var testCases = []struct {
|
||||
desc string
|
||||
nodeName types.NodeName
|
||||
isDataDiskNull bool
|
||||
expectedDataDisks []compute.DataDisk
|
||||
expectedError bool
|
||||
crt azcache.AzureCacheReadType
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if there's no corresponding vm",
|
||||
nodeName: "vm2",
|
||||
expectedDataDisks: nil,
|
||||
expectedError: true,
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
{
|
||||
desc: "correct list of data disks shall be returned if everything is good",
|
||||
nodeName: "vm1",
|
||||
expectedDataDisks: []compute.DataDisk{
|
||||
{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
{
|
||||
desc: "correct list of data disks shall be returned if everything is good",
|
||||
nodeName: "vm1",
|
||||
expectedDataDisks: []compute.DataDisk{
|
||||
{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
crt: azcache.CacheReadTypeUnsafe,
|
||||
},
|
||||
{
|
||||
desc: "nil shall be returned if DataDisk is null",
|
||||
nodeName: "vm1",
|
||||
isDataDiskNull: true,
|
||||
expectedDataDisks: nil,
|
||||
expectedError: false,
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
}
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
vmSet := testCloud.VMSet
|
||||
expectedVMs := setTestVirtualMachines(testCloud, map[string]string{"vm1": "PowerState/Running"}, false)
|
||||
mockVMsClient := testCloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
if test.isDataDiskNull {
|
||||
vm.StorageProfile = &compute.StorageProfile{}
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, gomock.Not("vm1"), gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
dataDisks, err := vmSet.GetDataDisks(test.nodeName, test.crt)
|
||||
assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
|
||||
if test.crt == azcache.CacheReadTypeUnsafe {
|
||||
time.Sleep(fakeCacheTTL)
|
||||
dataDisks, err := vmSet.GetDataDisks(test.nodeName, test.crt)
|
||||
assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedError, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// AttachDisk attaches a vhd to vm
|
||||
// the vhd must exist, can be identified by diskName, diskURI, and lun.
|
||||
func (ss *scaleSet) AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error {
|
||||
vmName := mapNodeNameToVMName(nodeName)
|
||||
ssName, instanceID, vm, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeResourceGroup, err := ss.GetNodeResourceGroup(vmName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := []compute.DataDisk{}
|
||||
if vm.StorageProfile != nil && vm.StorageProfile.DataDisks != nil {
|
||||
disks = make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks))
|
||||
copy(disks, *vm.StorageProfile.DataDisks)
|
||||
}
|
||||
if isManagedDisk {
|
||||
managedDisk := &compute.ManagedDiskParameters{ID: &diskURI}
|
||||
if diskEncryptionSetID == "" {
|
||||
if vm.StorageProfile.OsDisk != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet != nil &&
|
||||
vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID != nil {
|
||||
// set diskEncryptionSet as value of os disk by default
|
||||
diskEncryptionSetID = *vm.StorageProfile.OsDisk.ManagedDisk.DiskEncryptionSet.ID
|
||||
}
|
||||
}
|
||||
if diskEncryptionSetID != "" {
|
||||
managedDisk.DiskEncryptionSet = &compute.DiskEncryptionSetParameters{ID: &diskEncryptionSetID}
|
||||
}
|
||||
disks = append(disks,
|
||||
compute.DataDisk{
|
||||
Name: &diskName,
|
||||
Lun: &lun,
|
||||
Caching: compute.CachingTypes(cachingMode),
|
||||
CreateOption: "attach",
|
||||
ManagedDisk: managedDisk,
|
||||
WriteAcceleratorEnabled: pointer.Bool(writeAcceleratorEnabled),
|
||||
})
|
||||
} else {
|
||||
disks = append(disks,
|
||||
compute.DataDisk{
|
||||
Name: &diskName,
|
||||
Vhd: &compute.VirtualHardDisk{
|
||||
URI: &diskURI,
|
||||
},
|
||||
Lun: &lun,
|
||||
Caching: compute.CachingTypes(cachingMode),
|
||||
CreateOption: "attach",
|
||||
})
|
||||
}
|
||||
newVM := compute.VirtualMachineScaleSetVM{
|
||||
VirtualMachineScaleSetVMProperties: &compute.VirtualMachineScaleSetVMProperties{
|
||||
StorageProfile: &compute.StorageProfile{
|
||||
DataDisks: &disks,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
// Invalidate the cache right after updating
|
||||
defer ss.deleteCacheForNode(vmName)
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) with DiskEncryptionSetID(%s)", nodeResourceGroup, nodeName, diskName, diskURI, diskEncryptionSetID)
|
||||
rerr := ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "attach_disk")
|
||||
if rerr != nil {
|
||||
klog.Errorf("azureDisk - attach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, nodeName, rerr)
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, nodeName)
|
||||
disks := ss.filterNonExistingDisks(ctx, *newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks)
|
||||
newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks = &disks
|
||||
rerr = ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "attach_disk")
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - attach disk(%s, %s) returned with %v", nodeResourceGroup, nodeName, diskName, diskURI, rerr)
|
||||
if rerr != nil {
|
||||
return rerr.Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetachDisk detaches a disk from host
|
||||
// the vhd can be identified by diskName or diskURI
|
||||
func (ss *scaleSet) DetachDisk(diskName, diskURI string, nodeName types.NodeName) error {
|
||||
vmName := mapNodeNameToVMName(nodeName)
|
||||
ssName, instanceID, vm, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeResourceGroup, err := ss.GetNodeResourceGroup(vmName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := []compute.DataDisk{}
|
||||
if vm.StorageProfile != nil && vm.StorageProfile.DataDisks != nil {
|
||||
disks = make([]compute.DataDisk, len(*vm.StorageProfile.DataDisks))
|
||||
copy(disks, *vm.StorageProfile.DataDisks)
|
||||
}
|
||||
bFoundDisk := false
|
||||
for i, disk := range disks {
|
||||
if disk.Lun != nil && (disk.Name != nil && diskName != "" && strings.EqualFold(*disk.Name, diskName)) ||
|
||||
(disk.Vhd != nil && disk.Vhd.URI != nil && diskURI != "" && strings.EqualFold(*disk.Vhd.URI, diskURI)) ||
|
||||
(disk.ManagedDisk != nil && diskURI != "" && strings.EqualFold(*disk.ManagedDisk.ID, diskURI)) {
|
||||
// found the disk
|
||||
klog.V(2).Infof("azureDisk - detach disk: name %q uri %q", diskName, diskURI)
|
||||
if strings.EqualFold(ss.cloud.Environment.Name, AzureStackCloudName) {
|
||||
disks = append(disks[:i], disks[i+1:]...)
|
||||
} else {
|
||||
disks[i].ToBeDetached = pointer.Bool(true)
|
||||
}
|
||||
bFoundDisk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !bFoundDisk {
|
||||
// only log here, next action is to update VM status with original meta data
|
||||
klog.Errorf("detach azure disk: disk %s not found, diskURI: %s", diskName, diskURI)
|
||||
}
|
||||
|
||||
newVM := compute.VirtualMachineScaleSetVM{
|
||||
VirtualMachineScaleSetVMProperties: &compute.VirtualMachineScaleSetVMProperties{
|
||||
StorageProfile: &compute.StorageProfile{
|
||||
DataDisks: &disks,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
// Invalidate the cache right after updating
|
||||
defer ss.deleteCacheForNode(vmName)
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s)", nodeResourceGroup, nodeName, diskName, diskURI)
|
||||
rerr := ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "detach_disk")
|
||||
if rerr != nil {
|
||||
klog.Errorf("azureDisk - detach disk(%s, %s) on rg(%s) vm(%s) failed, err: %v", diskName, diskURI, nodeResourceGroup, nodeName, rerr)
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.Errorf("azureDisk - begin to filterNonExistingDisks(%s, %s) on rg(%s) vm(%s)", diskName, diskURI, nodeResourceGroup, nodeName)
|
||||
disks := ss.filterNonExistingDisks(ctx, *newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks)
|
||||
newVM.VirtualMachineScaleSetVMProperties.StorageProfile.DataDisks = &disks
|
||||
rerr = ss.VirtualMachineScaleSetVMsClient.Update(ctx, nodeResourceGroup, ssName, instanceID, newVM, "detach_disk")
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - update(%s): vm(%s) - detach disk(%s, %s) returned with %v", nodeResourceGroup, nodeName, diskName, diskURI, rerr)
|
||||
if rerr != nil {
|
||||
return rerr.Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDataDisks gets a list of data disks attached to the node.
|
||||
func (ss *scaleSet) GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error) {
|
||||
_, _, vm, err := ss.getVmssVM(string(nodeName), crt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vm.StorageProfile == nil || vm.StorageProfile.DataDisks == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return *vm.StorageProfile.DataDisks, nil
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestAttachDiskWithVMSS(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
fakeStatusNotFoundVMSSName := types.NodeName("FakeStatusNotFoundVMSSName")
|
||||
testCases := []struct {
|
||||
desc string
|
||||
vmList map[string]string
|
||||
vmssVMList []string
|
||||
vmssName types.NodeName
|
||||
vmssvmName types.NodeName
|
||||
isManagedDisk bool
|
||||
existedDisk compute.Disk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if it is invalid vmss name",
|
||||
vmssVMList: []string{"vmss-vm-000001"},
|
||||
vmssName: "vm1",
|
||||
vmssvmName: "vm1",
|
||||
isManagedDisk: false,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("not a vmss instance"),
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything is good with managed disk",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: "vmss00",
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
isManagedDisk: true,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything is good with non-managed disk",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: "vmss00",
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
isManagedDisk: false,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if response StatusNotFound",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: fakeStatusNotFoundVMSSName,
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
isManagedDisk: false,
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name")},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 404, RawError: %w", cloudprovider.InstanceNotFound),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
scaleSetName := string(test.vmssName)
|
||||
ss, err := newTestScaleSet(ctrl)
|
||||
assert.NoError(t, err, test.desc)
|
||||
testCloud := ss.cloud
|
||||
testCloud.PrimaryScaleSetName = scaleSetName
|
||||
expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false)
|
||||
mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes()
|
||||
mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1)
|
||||
mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1)
|
||||
|
||||
expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, test.vmssVMList, "succeeded", false)
|
||||
for _, vmssvm := range expectedVMSSVMs {
|
||||
vmssvm.StorageProfile = &compute.StorageProfile{
|
||||
OsDisk: &compute.OSDisk{
|
||||
Name: pointer.String("osdisk1"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.String("ManagedID"),
|
||||
DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
|
||||
ID: pointer.String("DiskEncryptionSetID"),
|
||||
},
|
||||
},
|
||||
},
|
||||
DataDisks: &[]compute.DataDisk{},
|
||||
}
|
||||
}
|
||||
mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes()
|
||||
if scaleSetName == string(fakeStatusNotFoundVMSSName) {
|
||||
mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
} else {
|
||||
mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
|
||||
err = ss.AttachDisk(test.isManagedDisk, "disk-name", diskURI, test.vmssvmName, 0, compute.CachingTypesReadWrite, "", true)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetachDiskWithVMSS(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
fakeStatusNotFoundVMSSName := types.NodeName("FakeStatusNotFoundVMSSName")
|
||||
diskName := "disk-name"
|
||||
testCases := []struct {
|
||||
desc string
|
||||
vmList map[string]string
|
||||
vmssVMList []string
|
||||
vmssName types.NodeName
|
||||
vmssvmName types.NodeName
|
||||
existedDisk compute.Disk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if it is invalid vmss name",
|
||||
vmssVMList: []string{"vmss-vm-000001"},
|
||||
vmssName: "vm1",
|
||||
vmssvmName: "vm1",
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName)},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("not a vmss instance"),
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything is good",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: "vmss00",
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName)},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if response StatusNotFound",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: fakeStatusNotFoundVMSSName,
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName)},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 404, RawError: %w", cloudprovider.InstanceNotFound),
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything is good and the attaching disk does not match data disk",
|
||||
vmssVMList: []string{"vmss00-vm-000000", "vmss00-vm-000001", "vmss00-vm-000002"},
|
||||
vmssName: "vmss00",
|
||||
vmssvmName: "vmss00-vm-000000",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk-name-err")},
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
scaleSetName := string(test.vmssName)
|
||||
ss, err := newTestScaleSet(ctrl)
|
||||
assert.NoError(t, err, test.desc)
|
||||
testCloud := ss.cloud
|
||||
testCloud.PrimaryScaleSetName = scaleSetName
|
||||
expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false)
|
||||
mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes()
|
||||
mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1)
|
||||
mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1)
|
||||
|
||||
expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, test.vmssVMList, "succeeded", false)
|
||||
for _, vmssvm := range expectedVMSSVMs {
|
||||
vmssvm.StorageProfile = &compute.StorageProfile{
|
||||
OsDisk: &compute.OSDisk{
|
||||
Name: pointer.String("osdisk1"),
|
||||
ManagedDisk: &compute.ManagedDiskParameters{
|
||||
ID: pointer.String("ManagedID"),
|
||||
DiskEncryptionSet: &compute.DiskEncryptionSetParameters{
|
||||
ID: pointer.String("DiskEncryptionSetID"),
|
||||
},
|
||||
},
|
||||
},
|
||||
DataDisks: &[]compute.DataDisk{{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String(diskName),
|
||||
}},
|
||||
}
|
||||
}
|
||||
mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes()
|
||||
if scaleSetName == string(fakeStatusNotFoundVMSSName) {
|
||||
mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(&retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
} else {
|
||||
mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
err = ss.DetachDisk(*test.existedDisk.Name, diskName, test.vmssvmName)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, err: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
|
||||
if !test.expectedErr {
|
||||
dataDisks, err := ss.GetDataDisks(test.vmssvmName, azcache.CacheReadTypeDefault)
|
||||
assert.Equal(t, true, len(dataDisks) == 1, "TestCase[%d]: %s, actual data disk num: %d, err: %v", i, test.desc, len(dataDisks), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDataDisksWithVMSS(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
var testCases = []struct {
|
||||
desc string
|
||||
nodeName types.NodeName
|
||||
isDataDiskNull bool
|
||||
expectedDataDisks []compute.DataDisk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
crt azcache.AzureCacheReadType
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if there's no corresponding vm",
|
||||
nodeName: "vmss00-vm-000001",
|
||||
expectedDataDisks: nil,
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("instance not found"),
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
{
|
||||
desc: "correct list of data disks shall be returned if everything is good",
|
||||
nodeName: "vmss00-vm-000000",
|
||||
expectedDataDisks: []compute.DataDisk{
|
||||
{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
},
|
||||
},
|
||||
expectedErr: false,
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
{
|
||||
desc: "correct list of data disks shall be returned if everything is good",
|
||||
nodeName: "vmss00-vm-000000",
|
||||
expectedDataDisks: []compute.DataDisk{
|
||||
{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
},
|
||||
},
|
||||
expectedErr: false,
|
||||
crt: azcache.CacheReadTypeUnsafe,
|
||||
},
|
||||
{
|
||||
desc: "nil shall be returned if DataDisk is null",
|
||||
nodeName: "vmss00-vm-000000",
|
||||
isDataDiskNull: true,
|
||||
expectedDataDisks: nil,
|
||||
expectedErr: false,
|
||||
crt: azcache.CacheReadTypeDefault,
|
||||
},
|
||||
}
|
||||
for i, test := range testCases {
|
||||
scaleSetName := string(test.nodeName)
|
||||
ss, err := newTestScaleSet(ctrl)
|
||||
assert.NoError(t, err, test.desc)
|
||||
testCloud := ss.cloud
|
||||
testCloud.PrimaryScaleSetName = scaleSetName
|
||||
expectedVMSS := buildTestVMSSWithLB(scaleSetName, "vmss00-vm-", []string{testLBBackendpoolID0}, false)
|
||||
mockVMSSClient := testCloud.VirtualMachineScaleSetsClient.(*mockvmssclient.MockInterface)
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup).Return([]compute.VirtualMachineScaleSet{expectedVMSS}, nil).AnyTimes()
|
||||
mockVMSSClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, scaleSetName).Return(expectedVMSS, nil).MaxTimes(1)
|
||||
mockVMSSClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(nil).MaxTimes(1)
|
||||
|
||||
expectedVMSSVMs, _, _ := buildTestVirtualMachineEnv(testCloud, scaleSetName, "", 0, []string{"vmss00-vm-000000"}, "succeeded", false)
|
||||
if !test.isDataDiskNull {
|
||||
for _, vmssvm := range expectedVMSSVMs {
|
||||
vmssvm.StorageProfile = &compute.StorageProfile{
|
||||
DataDisks: &[]compute.DataDisk{{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
}},
|
||||
}
|
||||
}
|
||||
}
|
||||
mockVMSSVMClient := testCloud.VirtualMachineScaleSetVMsClient.(*mockvmssvmclient.MockInterface)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any()).Return(expectedVMSSVMs, nil).AnyTimes()
|
||||
mockVMSSVMClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, scaleSetName, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
dataDisks, err := ss.GetDataDisks(test.nodeName, test.crt)
|
||||
assert.Equal(t, test.expectedDataDisks, dataDisks, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/legacy-cloud-providers/azure/auth"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/loadbalancerclient/mockloadbalancerclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routeclient/mockrouteclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/securitygroupclient/mocksecuritygroupclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/subnetclient/mocksubnetclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient"
|
||||
)
|
||||
|
||||
var (
|
||||
errPreconditionFailedEtagMismatch = fmt.Errorf("PreconditionFailedEtagMismatch")
|
||||
)
|
||||
|
||||
// GetTestCloud returns a fake azure cloud for unit tests in Azure related CSI drivers
|
||||
func GetTestCloud(ctrl *gomock.Controller) (az *Cloud) {
|
||||
az = &Cloud{
|
||||
Config: Config{
|
||||
AzureAuthConfig: auth.AzureAuthConfig{
|
||||
TenantID: "tenant",
|
||||
SubscriptionID: "subscription",
|
||||
},
|
||||
ResourceGroup: "rg",
|
||||
VnetResourceGroup: "rg",
|
||||
RouteTableResourceGroup: "rg",
|
||||
SecurityGroupResourceGroup: "rg",
|
||||
Location: "westus",
|
||||
VnetName: "vnet",
|
||||
SubnetName: "subnet",
|
||||
SecurityGroupName: "nsg",
|
||||
RouteTableName: "rt",
|
||||
PrimaryAvailabilitySetName: "as",
|
||||
PrimaryScaleSetName: "vmss",
|
||||
MaximumLoadBalancerRuleCount: 250,
|
||||
VMType: vmTypeStandard,
|
||||
},
|
||||
nodeZones: map[string]sets.String{},
|
||||
nodeInformerSynced: func() bool { return true },
|
||||
nodeResourceGroups: map[string]string{},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
excludeLoadBalancerNodes: sets.NewString(),
|
||||
routeCIDRs: map[string]string{},
|
||||
eventRecorder: &record.FakeRecorder{},
|
||||
}
|
||||
az.DisksClient = mockdiskclient.NewMockInterface(ctrl)
|
||||
az.InterfacesClient = mockinterfaceclient.NewMockInterface(ctrl)
|
||||
az.LoadBalancerClient = mockloadbalancerclient.NewMockInterface(ctrl)
|
||||
az.PublicIPAddressesClient = mockpublicipclient.NewMockInterface(ctrl)
|
||||
az.RoutesClient = mockrouteclient.NewMockInterface(ctrl)
|
||||
az.RouteTablesClient = mockroutetableclient.NewMockInterface(ctrl)
|
||||
az.SecurityGroupsClient = mocksecuritygroupclient.NewMockInterface(ctrl)
|
||||
az.SubnetsClient = mocksubnetclient.NewMockInterface(ctrl)
|
||||
az.VirtualMachineScaleSetsClient = mockvmssclient.NewMockInterface(ctrl)
|
||||
az.VirtualMachineScaleSetVMsClient = mockvmssvmclient.NewMockInterface(ctrl)
|
||||
az.VirtualMachinesClient = mockvmclient.NewMockInterface(ctrl)
|
||||
az.VMSet = newAvailabilitySet(az)
|
||||
az.vmCache, _ = az.newVMCache()
|
||||
az.lbCache, _ = az.newLBCache()
|
||||
az.nsgCache, _ = az.newNSGCache()
|
||||
az.rtCache, _ = az.newRouteTableCache()
|
||||
|
||||
common := &controllerCommon{cloud: az, resourceGroup: "rg", location: "westus"}
|
||||
az.controllerCommon = common
|
||||
az.ManagedDiskController = &ManagedDiskController{common: common}
|
||||
|
||||
return az
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/fileclient"
|
||||
)
|
||||
|
||||
// create file share
|
||||
func (az *Cloud) createFileShare(resourceGroupName, accountName string, shareOptions *fileclient.ShareOptions) error {
|
||||
return az.FileClient.CreateFileShare(resourceGroupName, accountName, shareOptions)
|
||||
}
|
||||
|
||||
func (az *Cloud) deleteFileShare(resourceGroupName, accountName, name string) error {
|
||||
return az.FileClient.DeleteFileShare(resourceGroupName, accountName, name)
|
||||
}
|
||||
|
||||
func (az *Cloud) resizeFileShare(resourceGroupName, accountName, name string, sizeGiB int) error {
|
||||
return az.FileClient.ResizeFileShare(resourceGroupName, accountName, name, sizeGiB)
|
||||
}
|
||||
|
||||
func (az *Cloud) getFileShare(resourceGroupName, accountName, name string) (storage.FileShare, error) {
|
||||
return az.FileClient.GetFileShare(resourceGroupName, accountName, name)
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataCacheTTL = time.Minute
|
||||
metadataCacheKey = "InstanceMetadata"
|
||||
imdsInstanceAPIVersion = "2019-03-11"
|
||||
imdsLoadBalancerAPIVersion = "2020-10-01"
|
||||
imdsServer = "http://169.254.169.254"
|
||||
imdsInstanceURI = "/metadata/instance"
|
||||
imdsLoadBalancerURI = "/metadata/loadbalancer"
|
||||
)
|
||||
|
||||
// NetworkMetadata contains metadata about an instance's network
|
||||
type NetworkMetadata struct {
|
||||
Interface []NetworkInterface `json:"interface"`
|
||||
}
|
||||
|
||||
// NetworkInterface represents an instances network interface.
|
||||
type NetworkInterface struct {
|
||||
IPV4 NetworkData `json:"ipv4"`
|
||||
IPV6 NetworkData `json:"ipv6"`
|
||||
MAC string `json:"macAddress"`
|
||||
}
|
||||
|
||||
// NetworkData contains IP information for a network.
|
||||
type NetworkData struct {
|
||||
IPAddress []IPAddress `json:"ipAddress"`
|
||||
Subnet []Subnet `json:"subnet"`
|
||||
}
|
||||
|
||||
// IPAddress represents IP address information.
|
||||
type IPAddress struct {
|
||||
PrivateIP string `json:"privateIpAddress"`
|
||||
PublicIP string `json:"publicIpAddress"`
|
||||
}
|
||||
|
||||
// Subnet represents subnet information.
|
||||
type Subnet struct {
|
||||
Address string `json:"address"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// ComputeMetadata represents compute information
|
||||
type ComputeMetadata struct {
|
||||
Environment string `json:"azEnvironment,omitempty"`
|
||||
SKU string `json:"sku,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
VMSize string `json:"vmSize,omitempty"`
|
||||
OSType string `json:"osType,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
FaultDomain string `json:"platformFaultDomain,omitempty"`
|
||||
UpdateDomain string `json:"platformUpdateDomain,omitempty"`
|
||||
ResourceGroup string `json:"resourceGroupName,omitempty"`
|
||||
VMScaleSetName string `json:"vmScaleSetName,omitempty"`
|
||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceMetadata represents instance information.
|
||||
type InstanceMetadata struct {
|
||||
Compute *ComputeMetadata `json:"compute,omitempty"`
|
||||
Network *NetworkMetadata `json:"network,omitempty"`
|
||||
}
|
||||
|
||||
// PublicIPMetadata represents the public IP metadata.
|
||||
type PublicIPMetadata struct {
|
||||
FrontendIPAddress string `json:"frontendIpAddress,omitempty"`
|
||||
PrivateIPAddress string `json:"privateIpAddress,omitempty"`
|
||||
}
|
||||
|
||||
// LoadbalancerProfile represents load balancer profile in IMDS.
|
||||
type LoadbalancerProfile struct {
|
||||
PublicIPAddresses []PublicIPMetadata `json:"publicIpAddresses,omitempty"`
|
||||
}
|
||||
|
||||
// LoadBalancerMetadata represents load balancer metadata.
|
||||
type LoadBalancerMetadata struct {
|
||||
LoadBalancer *LoadbalancerProfile `json:"loadbalancer,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceMetadataService knows how to query the Azure instance metadata server.
|
||||
type InstanceMetadataService struct {
|
||||
imdsServer string
|
||||
imsCache *azcache.TimedCache
|
||||
}
|
||||
|
||||
// NewInstanceMetadataService creates an instance of the InstanceMetadataService accessor object.
|
||||
func NewInstanceMetadataService(imdsServer string) (*InstanceMetadataService, error) {
|
||||
ims := &InstanceMetadataService{
|
||||
imdsServer: imdsServer,
|
||||
}
|
||||
|
||||
imsCache, err := azcache.NewTimedcache(metadataCacheTTL, ims.getMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ims.imsCache = imsCache
|
||||
return ims, nil
|
||||
}
|
||||
|
||||
func (ims *InstanceMetadataService) getMetadata(key string) (interface{}, error) {
|
||||
instanceMetadata, err := ims.getInstanceMetadata(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if instanceMetadata.Network != nil && len(instanceMetadata.Network.Interface) > 0 {
|
||||
netInterface := instanceMetadata.Network.Interface[0]
|
||||
if (len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PublicIP) > 0) ||
|
||||
(len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PublicIP) > 0) {
|
||||
// Return if public IP address has already part of instance metadata.
|
||||
return instanceMetadata, nil
|
||||
}
|
||||
|
||||
loadBalancerMetadata, err := ims.getLoadBalancerMetadata()
|
||||
if err != nil || loadBalancerMetadata == nil || loadBalancerMetadata.LoadBalancer == nil {
|
||||
// Log a warning since loadbalancer metadata may not be available when the VM
|
||||
// is not in standard LoadBalancer backend address pool.
|
||||
klog.V(4).Infof("Warning: failed to get loadbalancer metadata: %v", err)
|
||||
return instanceMetadata, nil
|
||||
}
|
||||
|
||||
publicIPs := loadBalancerMetadata.LoadBalancer.PublicIPAddresses
|
||||
if len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PrivateIP) > 0 {
|
||||
for _, pip := range publicIPs {
|
||||
if pip.PrivateIPAddress == netInterface.IPV4.IPAddress[0].PrivateIP {
|
||||
netInterface.IPV4.IPAddress[0].PublicIP = pip.FrontendIPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PrivateIP) > 0 {
|
||||
for _, pip := range publicIPs {
|
||||
if pip.PrivateIPAddress == netInterface.IPV6.IPAddress[0].PrivateIP {
|
||||
netInterface.IPV6.IPAddress[0].PublicIP = pip.FrontendIPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instanceMetadata, nil
|
||||
}
|
||||
|
||||
func (ims *InstanceMetadataService) getInstanceMetadata(key string) (*InstanceMetadata, error) {
|
||||
req, err := http.NewRequest("GET", ims.imdsServer+imdsInstanceURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Metadata", "True")
|
||||
req.Header.Add("User-Agent", "golang/kubernetes-cloud-provider")
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("format", "json")
|
||||
q.Add("api-version", imdsInstanceAPIVersion)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failure of getting instance metadata with response %q", resp.Status)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := InstanceMetadata{}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
func (ims *InstanceMetadataService) getLoadBalancerMetadata() (*LoadBalancerMetadata, error) {
|
||||
req, err := http.NewRequest("GET", ims.imdsServer+imdsLoadBalancerURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Metadata", "True")
|
||||
req.Header.Add("User-Agent", "golang/kubernetes-cloud-provider")
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("format", "json")
|
||||
q.Add("api-version", imdsLoadBalancerAPIVersion)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failure of getting loadbalancer metadata with response %q", resp.Status)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := LoadBalancerMetadata{}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
// GetMetadata gets instance metadata from cache.
|
||||
// crt determines if we can get data from stalled cache/need fresh if cache expired.
|
||||
func (ims *InstanceMetadataService) GetMetadata(crt azcache.AzureCacheReadType) (*InstanceMetadata, error) {
|
||||
cache, err := ims.imsCache.Get(metadataCacheKey, crt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache shouldn't be nil, but added a check in case something is wrong.
|
||||
if cache == nil {
|
||||
return nil, fmt.Errorf("failure of getting instance metadata")
|
||||
}
|
||||
|
||||
if metadata, ok := cache.(*InstanceMetadata); ok {
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failure of getting instance metadata")
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
)
|
||||
|
||||
const (
|
||||
vmPowerStatePrefix = "PowerState/"
|
||||
vmPowerStateStopped = "stopped"
|
||||
vmPowerStateDeallocated = "deallocated"
|
||||
vmPowerStateDeallocating = "deallocating"
|
||||
|
||||
// nodeNameEnvironmentName is the environment variable name for getting node name.
|
||||
// It is only used for out-of-tree cloud provider.
|
||||
nodeNameEnvironmentName = "NODE_NAME"
|
||||
)
|
||||
|
||||
var (
|
||||
errNodeNotInitialized = fmt.Errorf("providerID is empty, the node is not initialized yet")
|
||||
)
|
||||
|
||||
func (az *Cloud) addressGetter(nodeName types.NodeName) ([]v1.NodeAddress, error) {
|
||||
ip, publicIP, err := az.getIPForMachine(nodeName)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("NodeAddresses(%s) abort backoff: %v", nodeName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addresses := []v1.NodeAddress{
|
||||
{Type: v1.NodeInternalIP, Address: ip},
|
||||
{Type: v1.NodeHostName, Address: string(nodeName)},
|
||||
}
|
||||
if len(publicIP) > 0 {
|
||||
addresses = append(addresses, v1.NodeAddress{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: publicIP,
|
||||
})
|
||||
}
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
// NodeAddresses returns the addresses of the specified instance.
|
||||
func (az *Cloud) NodeAddresses(ctx context.Context, name types.NodeName) ([]v1.NodeAddress, error) {
|
||||
// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
unmanaged, err := az.IsNodeUnmanaged(string(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unmanaged {
|
||||
klog.V(4).Infof("NodeAddresses: omitting unmanaged node %q", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if az.UseInstanceMetadata {
|
||||
metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if metadata.Compute == nil || metadata.Network == nil {
|
||||
return nil, fmt.Errorf("failure of getting instance metadata")
|
||||
}
|
||||
|
||||
isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Not local instance, get addresses from Azure ARM API.
|
||||
if !isLocalInstance {
|
||||
if az.VMSet != nil {
|
||||
return az.addressGetter(name)
|
||||
}
|
||||
|
||||
// vmSet == nil indicates credentials are not provided.
|
||||
return nil, fmt.Errorf("no credentials provided for Azure cloud provider")
|
||||
}
|
||||
|
||||
return az.getLocalInstanceNodeAddresses(metadata.Network.Interface, string(name))
|
||||
}
|
||||
|
||||
return az.addressGetter(name)
|
||||
}
|
||||
|
||||
func (az *Cloud) getLocalInstanceNodeAddresses(netInterfaces []NetworkInterface, nodeName string) ([]v1.NodeAddress, error) {
|
||||
if len(netInterfaces) == 0 {
|
||||
return nil, fmt.Errorf("no interface is found for the instance")
|
||||
}
|
||||
|
||||
// Use ip address got from instance metadata.
|
||||
netInterface := netInterfaces[0]
|
||||
addresses := []v1.NodeAddress{
|
||||
{Type: v1.NodeHostName, Address: nodeName},
|
||||
}
|
||||
if len(netInterface.IPV4.IPAddress) > 0 && len(netInterface.IPV4.IPAddress[0].PrivateIP) > 0 {
|
||||
address := netInterface.IPV4.IPAddress[0]
|
||||
addresses = append(addresses, v1.NodeAddress{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: address.PrivateIP,
|
||||
})
|
||||
if len(address.PublicIP) > 0 {
|
||||
addresses = append(addresses, v1.NodeAddress{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: address.PublicIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(netInterface.IPV6.IPAddress) > 0 && len(netInterface.IPV6.IPAddress[0].PrivateIP) > 0 {
|
||||
address := netInterface.IPV6.IPAddress[0]
|
||||
addresses = append(addresses, v1.NodeAddress{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: address.PrivateIP,
|
||||
})
|
||||
if len(address.PublicIP) > 0 {
|
||||
addresses = append(addresses, v1.NodeAddress{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: address.PublicIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(addresses) == 1 {
|
||||
// No IP addresses is got from instance metadata service, clean up cache and report errors.
|
||||
az.metadata.imsCache.Delete(metadataCacheKey)
|
||||
return nil, fmt.Errorf("get empty IP addresses from instance metadata service")
|
||||
}
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
// NodeAddressesByProviderID returns the node addresses of an instances with the specified unique providerID
|
||||
// This method will not be called from the node that is requesting this ID. i.e. metadata service
|
||||
// and other local methods cannot be used here
|
||||
func (az *Cloud) NodeAddressesByProviderID(ctx context.Context, providerID string) ([]v1.NodeAddress, error) {
|
||||
if providerID == "" {
|
||||
return nil, errNodeNotInitialized
|
||||
}
|
||||
|
||||
// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
if az.IsNodeUnmanagedByProviderID(providerID) {
|
||||
klog.V(4).Infof("NodeAddressesByProviderID: omitting unmanaged node %q", providerID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
name, err := az.VMSet.GetNodeNameByProviderID(providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return az.NodeAddresses(ctx, name)
|
||||
}
|
||||
|
||||
// InstanceExistsByProviderID returns true if the instance with the given provider id still exists and is running.
|
||||
// If false is returned with no error, the instance will be immediately deleted by the cloud controller manager.
|
||||
func (az *Cloud) InstanceExistsByProviderID(ctx context.Context, providerID string) (bool, error) {
|
||||
if providerID == "" {
|
||||
return false, errNodeNotInitialized
|
||||
}
|
||||
|
||||
// Returns true for unmanaged nodes because azure cloud provider always assumes them exists.
|
||||
if az.IsNodeUnmanagedByProviderID(providerID) {
|
||||
klog.V(4).Infof("InstanceExistsByProviderID: assuming unmanaged node %q exists", providerID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
name, err := az.VMSet.GetNodeNameByProviderID(providerID)
|
||||
if err != nil {
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, err = az.InstanceID(ctx, name)
|
||||
if err != nil {
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// InstanceShutdownByProviderID returns true if the instance is in safe state to detach volumes
|
||||
func (az *Cloud) InstanceShutdownByProviderID(ctx context.Context, providerID string) (bool, error) {
|
||||
if providerID == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
nodeName, err := az.VMSet.GetNodeNameByProviderID(providerID)
|
||||
if err != nil {
|
||||
// Returns false, so the controller manager will continue to check InstanceExistsByProviderID().
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
powerStatus, err := az.VMSet.GetPowerStatusByNodeName(string(nodeName))
|
||||
if err != nil {
|
||||
// Returns false, so the controller manager will continue to check InstanceExistsByProviderID().
|
||||
if err == cloudprovider.InstanceNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
klog.V(3).Infof("InstanceShutdownByProviderID gets power status %q for node %q", powerStatus, nodeName)
|
||||
|
||||
provisioningState, err := az.VMSet.GetProvisioningStateByNodeName(string(nodeName))
|
||||
if err != nil {
|
||||
// Returns false, so the controller manager will continue to check InstanceExistsByProviderID().
|
||||
if errors.Is(err, cloudprovider.InstanceNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
klog.V(3).Infof("InstanceShutdownByProviderID gets provisioning state %q for node %q", provisioningState, nodeName)
|
||||
|
||||
status := strings.ToLower(powerStatus)
|
||||
provisioningSucceeded := strings.EqualFold(strings.ToLower(provisioningState), strings.ToLower(string(compute.ProvisioningStateSucceeded)))
|
||||
return provisioningSucceeded && (status == vmPowerStateStopped || status == vmPowerStateDeallocated || status == vmPowerStateDeallocating), nil
|
||||
}
|
||||
|
||||
func (az *Cloud) isCurrentInstance(name types.NodeName, metadataVMName string) (bool, error) {
|
||||
var err error
|
||||
nodeName := mapNodeNameToVMName(name)
|
||||
|
||||
// VMSS vmName is not same with hostname, use hostname instead.
|
||||
if az.VMType == vmTypeVMSS {
|
||||
metadataVMName, err = os.Hostname()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Use name from env variable "NODE_NAME" if it is set.
|
||||
nodeNameEnv := os.Getenv(nodeNameEnvironmentName)
|
||||
if nodeNameEnv != "" {
|
||||
metadataVMName = nodeNameEnv
|
||||
}
|
||||
}
|
||||
|
||||
metadataVMName = strings.ToLower(metadataVMName)
|
||||
return metadataVMName == nodeName, nil
|
||||
}
|
||||
|
||||
// InstanceID returns the cloud provider ID of the specified instance.
|
||||
// Note that if the instance does not exist or is no longer running, we must return ("", cloudprovider.InstanceNotFound)
|
||||
func (az *Cloud) InstanceID(ctx context.Context, name types.NodeName) (string, error) {
|
||||
nodeName := mapNodeNameToVMName(name)
|
||||
unmanaged, err := az.IsNodeUnmanaged(nodeName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if unmanaged {
|
||||
// InstanceID is same with nodeName for unmanaged nodes.
|
||||
klog.V(4).Infof("InstanceID: getting ID %q for unmanaged node %q", name, name)
|
||||
return nodeName, nil
|
||||
}
|
||||
|
||||
if az.UseInstanceMetadata {
|
||||
metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if metadata.Compute == nil {
|
||||
return "", fmt.Errorf("failure of getting instance metadata")
|
||||
}
|
||||
|
||||
isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Not local instance, get instanceID from Azure ARM API.
|
||||
if !isLocalInstance {
|
||||
if az.VMSet != nil {
|
||||
return az.VMSet.GetInstanceIDByNodeName(nodeName)
|
||||
}
|
||||
|
||||
// vmSet == nil indicates credentials are not provided.
|
||||
return "", fmt.Errorf("no credentials provided for Azure cloud provider")
|
||||
}
|
||||
return az.getLocalInstanceProviderID(metadata, nodeName)
|
||||
}
|
||||
|
||||
return az.VMSet.GetInstanceIDByNodeName(nodeName)
|
||||
}
|
||||
|
||||
func (az *Cloud) getLocalInstanceProviderID(metadata *InstanceMetadata, nodeName string) (string, error) {
|
||||
// Get resource group name and subscription ID.
|
||||
resourceGroup := strings.ToLower(metadata.Compute.ResourceGroup)
|
||||
subscriptionID := strings.ToLower(metadata.Compute.SubscriptionID)
|
||||
|
||||
// Compose instanceID based on nodeName for standard instance.
|
||||
if metadata.Compute.VMScaleSetName == "" {
|
||||
return az.getStandardMachineID(subscriptionID, resourceGroup, nodeName), nil
|
||||
}
|
||||
|
||||
// Get scale set name and instanceID from vmName for vmss.
|
||||
ssName, instanceID, err := extractVmssVMName(metadata.Compute.Name)
|
||||
if err != nil {
|
||||
if err == ErrorNotVmssInstance {
|
||||
// Compose machineID for standard Node.
|
||||
return az.getStandardMachineID(subscriptionID, resourceGroup, nodeName), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
// Compose instanceID based on ssName and instanceID for vmss instance.
|
||||
return az.getVmssMachineID(subscriptionID, resourceGroup, ssName, instanceID), nil
|
||||
}
|
||||
|
||||
// InstanceTypeByProviderID returns the cloudprovider instance type of the node with the specified unique providerID
|
||||
// This method will not be called from the node that is requesting this ID. i.e. metadata service
|
||||
// and other local methods cannot be used here
|
||||
func (az *Cloud) InstanceTypeByProviderID(ctx context.Context, providerID string) (string, error) {
|
||||
if providerID == "" {
|
||||
return "", errNodeNotInitialized
|
||||
}
|
||||
|
||||
// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
if az.IsNodeUnmanagedByProviderID(providerID) {
|
||||
klog.V(4).Infof("InstanceTypeByProviderID: omitting unmanaged node %q", providerID)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
name, err := az.VMSet.GetNodeNameByProviderID(providerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return az.InstanceType(ctx, name)
|
||||
}
|
||||
|
||||
// InstanceType returns the type of the specified instance.
|
||||
// Note that if the instance does not exist or is no longer running, we must return ("", cloudprovider.InstanceNotFound)
|
||||
// (Implementer Note): This is used by kubelet. Kubelet will label the node. Real log from kubelet:
|
||||
//
|
||||
// Adding node label from cloud provider: beta.kubernetes.io/instance-type=[value]
|
||||
func (az *Cloud) InstanceType(ctx context.Context, name types.NodeName) (string, error) {
|
||||
// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
unmanaged, err := az.IsNodeUnmanaged(string(name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if unmanaged {
|
||||
klog.V(4).Infof("InstanceType: omitting unmanaged node %q", name)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if az.UseInstanceMetadata {
|
||||
metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if metadata.Compute == nil {
|
||||
return "", fmt.Errorf("failure of getting instance metadata")
|
||||
}
|
||||
|
||||
isLocalInstance, err := az.isCurrentInstance(name, metadata.Compute.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !isLocalInstance {
|
||||
if az.VMSet != nil {
|
||||
return az.VMSet.GetInstanceTypeByNodeName(string(name))
|
||||
}
|
||||
|
||||
// vmSet == nil indicates credentials are not provided.
|
||||
return "", fmt.Errorf("no credentials provided for Azure cloud provider")
|
||||
}
|
||||
|
||||
if metadata.Compute.VMSize != "" {
|
||||
return metadata.Compute.VMSize, nil
|
||||
}
|
||||
}
|
||||
|
||||
return az.VMSet.GetInstanceTypeByNodeName(string(name))
|
||||
}
|
||||
|
||||
// AddSSHKeyToAllInstances adds an SSH public key as a legal identity for all instances
|
||||
// expected format for the key is standard ssh-keygen format: <protocol> <blob>
|
||||
func (az *Cloud) AddSSHKeyToAllInstances(ctx context.Context, user string, keyData []byte) error {
|
||||
return cloudprovider.NotImplemented
|
||||
}
|
||||
|
||||
// CurrentNodeName returns the name of the node we are currently running on.
|
||||
// On Azure this is the hostname, so we just return the hostname.
|
||||
func (az *Cloud) CurrentNodeName(ctx context.Context, hostname string) (types.NodeName, error) {
|
||||
return types.NodeName(hostname), nil
|
||||
}
|
||||
|
||||
// mapNodeNameToVMName maps a k8s NodeName to an Azure VM Name
|
||||
// This is a simple string cast.
|
||||
func mapNodeNameToVMName(nodeName types.NodeName) string {
|
||||
return string(nodeName)
|
||||
}
|
||||
@@ -1,851 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/interfaceclient/mockinterfaceclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/publicipclient/mockpublicipclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// setTestVirtualMachines sets test virtual machine with powerstate.
|
||||
func setTestVirtualMachines(c *Cloud, vmList map[string]string, isDataDisksFull bool) []compute.VirtualMachine {
|
||||
expectedVMs := make([]compute.VirtualMachine, 0)
|
||||
|
||||
for nodeName, powerState := range vmList {
|
||||
instanceID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/%s", nodeName)
|
||||
vm := compute.VirtualMachine{
|
||||
Name: &nodeName,
|
||||
ID: &instanceID,
|
||||
Location: &c.Location,
|
||||
}
|
||||
status := []compute.InstanceViewStatus{
|
||||
{
|
||||
Code: pointer.String(powerState),
|
||||
},
|
||||
{
|
||||
Code: pointer.String("ProvisioningState/succeeded"),
|
||||
},
|
||||
}
|
||||
vm.VirtualMachineProperties = &compute.VirtualMachineProperties{
|
||||
ProvisioningState: pointer.String(string(compute.ProvisioningStateSucceeded)),
|
||||
HardwareProfile: &compute.HardwareProfile{
|
||||
VMSize: compute.VirtualMachineSizeTypesStandardA0,
|
||||
},
|
||||
InstanceView: &compute.VirtualMachineInstanceView{
|
||||
Statuses: &status,
|
||||
},
|
||||
StorageProfile: &compute.StorageProfile{
|
||||
DataDisks: &[]compute.DataDisk{},
|
||||
},
|
||||
}
|
||||
if !isDataDisksFull {
|
||||
vm.StorageProfile.DataDisks = &[]compute.DataDisk{{
|
||||
Lun: pointer.Int32(0),
|
||||
Name: pointer.String("disk1"),
|
||||
}}
|
||||
} else {
|
||||
dataDisks := make([]compute.DataDisk, maxLUN)
|
||||
for i := 0; i < maxLUN; i++ {
|
||||
dataDisks[i] = compute.DataDisk{Lun: pointer.Int32(int32(i))}
|
||||
}
|
||||
vm.StorageProfile.DataDisks = &dataDisks
|
||||
}
|
||||
|
||||
expectedVMs = append(expectedVMs, vm)
|
||||
}
|
||||
|
||||
return expectedVMs
|
||||
}
|
||||
|
||||
func TestInstanceID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := GetTestCloud(ctrl)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
vmList []string
|
||||
nodeName string
|
||||
vmssName string
|
||||
metadataName string
|
||||
metadataTemplate string
|
||||
vmType string
|
||||
expectedID string
|
||||
useInstanceMetadata bool
|
||||
useCustomImsCache bool
|
||||
nilVMSet bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "InstanceID should get instanceID if node's name are equal to metadataName",
|
||||
vmList: []string{"vm1"},
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
},
|
||||
{
|
||||
name: "InstanceID should get vmss instanceID from local if node's name are equal to metadataName and metadata.Compute.VMScaleSetName is not null",
|
||||
vmList: []string{"vmss1_0"},
|
||||
vmssName: "vmss1",
|
||||
nodeName: "vmss1_0",
|
||||
metadataName: "vmss1_0",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/0",
|
||||
},
|
||||
{
|
||||
name: "InstanceID should get standard instanceID from local if node's name are equal to metadataName and format of nodeName is not compliance with vmss instance",
|
||||
vmList: []string{"vmss1-0"},
|
||||
vmssName: "vmss1",
|
||||
nodeName: "vmss1-0",
|
||||
metadataName: "vmss1-0",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vmss1-0",
|
||||
},
|
||||
{
|
||||
name: "InstanceID should get instanceID from Azure API if node is not local instance",
|
||||
vmList: []string{"vm2"},
|
||||
nodeName: "vm2",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2",
|
||||
},
|
||||
{
|
||||
name: "InstanceID should get instanceID from Azure API if cloud.UseInstanceMetadata is false",
|
||||
vmList: []string{"vm2"},
|
||||
nodeName: "vm2",
|
||||
metadataName: "vm2",
|
||||
vmType: vmTypeStandard,
|
||||
expectedID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2",
|
||||
},
|
||||
{
|
||||
name: "InstanceID should report error if node doesn't exist",
|
||||
vmList: []string{"vm1"},
|
||||
nodeName: "vm3",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("instance not found"),
|
||||
},
|
||||
{
|
||||
name: "InstanceID should report error if metadata.Compute is nil",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
metadataTemplate: `{"network":{"interface":[]}}`,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("failure of getting instance metadata"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if cloud.VMSet is nil",
|
||||
nodeName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
nilVMSet: true,
|
||||
expectedErrMsg: fmt.Errorf("no credentials provided for Azure cloud provider"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if invoking GetMetadata returns error",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useCustomImsCache: true,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("getError"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
if test.nilVMSet {
|
||||
cloud.VMSet = nil
|
||||
} else {
|
||||
cloud.VMSet = newAvailabilitySet(cloud)
|
||||
}
|
||||
cloud.Config.VMType = test.vmType
|
||||
cloud.Config.UseInstanceMetadata = test.useInstanceMetadata
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if test.metadataTemplate != "" {
|
||||
fmt.Fprintf(w, test.metadataTemplate)
|
||||
} else {
|
||||
fmt.Fprintf(w, "{\"compute\":{\"name\":\"%s\",\"VMScaleSetName\":\"%s\",\"subscriptionId\":\"subscription\",\"resourceGroupName\":\"rg\"}}", test.metadataName, test.vmssName)
|
||||
}
|
||||
}))
|
||||
go func() {
|
||||
http.Serve(listener, mux)
|
||||
}()
|
||||
defer listener.Close()
|
||||
|
||||
cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
if test.useCustomImsCache {
|
||||
cloud.metadata.imsCache, err = azcache.NewTimedcache(metadataCacheTTL, func(key string) (interface{}, error) {
|
||||
return nil, fmt.Errorf("getError")
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
}
|
||||
vmListWithPowerState := make(map[string]string)
|
||||
for _, vm := range test.vmList {
|
||||
vmListWithPowerState[vm] = ""
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(cloud, vmListWithPowerState, false)
|
||||
mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm3", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), cloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
instanceID, err := cloud.InstanceID(context.Background(), types.NodeName(test.nodeName))
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
assert.Equal(t, test.expectedID, instanceID, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceShutdownByProviderID(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
vmList map[string]string
|
||||
nodeName string
|
||||
providerID string
|
||||
provisioningState string
|
||||
expected bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Running status",
|
||||
vmList: map[string]string{"vm1": "PowerState/Running"},
|
||||
nodeName: "vm1",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return true if the vm is in PowerState/Deallocated status",
|
||||
vmList: map[string]string{"vm2": "PowerState/Deallocated"},
|
||||
nodeName: "vm2",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Deallocating status",
|
||||
vmList: map[string]string{"vm3": "PowerState/Deallocating"},
|
||||
nodeName: "vm3",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm3",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Starting status",
|
||||
vmList: map[string]string{"vm4": "PowerState/Starting"},
|
||||
nodeName: "vm4",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm4",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return true if the vm is in PowerState/Stopped status",
|
||||
vmList: map[string]string{"vm5": "PowerState/Stopped"},
|
||||
nodeName: "vm5",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm5",
|
||||
expected: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Stopping status",
|
||||
vmList: map[string]string{"vm6": "PowerState/Stopping"},
|
||||
nodeName: "vm6",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm6",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Unknown status",
|
||||
vmList: map[string]string{"vm7": "PowerState/Unknown"},
|
||||
nodeName: "vm7",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm7",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if node doesn't exist",
|
||||
vmList: map[string]string{"vm1": "PowerState/running"},
|
||||
nodeName: "vm8",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm8",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should return false if the vm is in PowerState/Stopped state with Creating provisioning state",
|
||||
vmList: map[string]string{"vm9": "PowerState/Stopped"},
|
||||
nodeName: "vm9",
|
||||
provisioningState: "Creating",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm9",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should report error if providerID is null",
|
||||
nodeName: "vmm",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceShutdownByProviderID should report error if providerID is invalid",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/VM/vm10",
|
||||
nodeName: "vm10",
|
||||
expected: false,
|
||||
expectedErrMsg: fmt.Errorf("error splitting providerID"),
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
for _, test := range testcases {
|
||||
cloud := GetTestCloud(ctrl)
|
||||
expectedVMs := setTestVirtualMachines(cloud, test.vmList, false)
|
||||
if test.provisioningState != "" {
|
||||
expectedVMs[0].ProvisioningState = pointer.String(test.provisioningState)
|
||||
}
|
||||
mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, test.nodeName, gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
hasShutdown, err := cloud.InstanceShutdownByProviderID(context.Background(), test.providerID)
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
assert.Equal(t, test.expected, hasShutdown, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAddresses(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cloud := GetTestCloud(ctrl)
|
||||
|
||||
expectedVM := compute.VirtualMachine{
|
||||
VirtualMachineProperties: &compute.VirtualMachineProperties{
|
||||
NetworkProfile: &compute.NetworkProfile{
|
||||
NetworkInterfaces: &[]compute.NetworkInterfaceReference{
|
||||
{
|
||||
NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{
|
||||
Primary: pointer.Bool(true),
|
||||
},
|
||||
ID: pointer.String("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/networkInterfaces/nic"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedPIP := network.PublicIPAddress{
|
||||
ID: pointer.String("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip1"),
|
||||
PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
|
||||
IPAddress: pointer.String("192.168.1.12"),
|
||||
},
|
||||
}
|
||||
|
||||
expectedInterface := network.Interface{
|
||||
InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
|
||||
IPConfigurations: &[]network.InterfaceIPConfiguration{
|
||||
{
|
||||
InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
|
||||
PrivateIPAddress: pointer.String("172.1.0.3"),
|
||||
PublicIPAddress: &expectedPIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedNodeAddress := []v1.NodeAddress{
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "172.1.0.3",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeHostName,
|
||||
Address: "vm1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: "192.168.1.12",
|
||||
},
|
||||
}
|
||||
metadataTemplate := `{"compute":{"name":"%s"},"network":{"interface":[{"ipv4":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]},"ipv6":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]}}]}}`
|
||||
loadbalancerTemplate := `{"loadbalancer": {"publicIpAddresses": [{"frontendIpAddress": "%s","privateIpAddress": "%s"},{"frontendIpAddress": "%s","privateIpAddress": "%s"}]}}`
|
||||
testcases := []struct {
|
||||
name string
|
||||
nodeName string
|
||||
metadataName string
|
||||
metadataTemplate string
|
||||
vmType string
|
||||
ipV4 string
|
||||
ipV6 string
|
||||
ipV4Public string
|
||||
ipV6Public string
|
||||
loadBalancerSku string
|
||||
expectedAddress []v1.NodeAddress
|
||||
useInstanceMetadata bool
|
||||
useCustomImsCache bool
|
||||
nilVMSet bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "NodeAddresses should report error if metadata.Network is nil",
|
||||
metadataTemplate: `{"compute":{"name":"vm1"}}`,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("failure of getting instance metadata"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if metadata.Compute is nil",
|
||||
metadataTemplate: `{"network":{"interface":[]}}`,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("failure of getting instance metadata"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if metadata.Network.Interface is nil",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
metadataTemplate: `{"compute":{"name":"vm1"},"network":{}}`,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("no interface is found for the instance"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error when invoke GetMetadata",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useCustomImsCache: true,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("getError"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if cloud.VMSet is nil",
|
||||
nodeName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
nilVMSet: true,
|
||||
expectedErrMsg: fmt.Errorf("no credentials provided for Azure cloud provider"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error when IPs are empty",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: fmt.Errorf("get empty IP addresses from instance metadata service"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should report error if node don't exist",
|
||||
nodeName: "vm2",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedErrMsg: wait.ErrWaitTimeout,
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should get IP addresses from Azure API if node's name isn't equal to metadataName",
|
||||
nodeName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
useInstanceMetadata: true,
|
||||
expectedAddress: expectedNodeAddress,
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should get IP addresses from Azure API if useInstanceMetadata is false",
|
||||
nodeName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
expectedAddress: expectedNodeAddress,
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should get IP addresses from local IMDS if node's name is equal to metadataName",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
ipV4: "10.240.0.1",
|
||||
ipV4Public: "192.168.1.12",
|
||||
ipV6: "1111:11111:00:00:1111:1111:000:111",
|
||||
ipV6Public: "2222:22221:00:00:2222:2222:000:111",
|
||||
loadBalancerSku: "basic",
|
||||
useInstanceMetadata: true,
|
||||
expectedAddress: []v1.NodeAddress{
|
||||
{
|
||||
Type: v1.NodeHostName,
|
||||
Address: "vm1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "10.240.0.1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: "192.168.1.12",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "1111:11111:00:00:1111:1111:000:111",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: "2222:22221:00:00:2222:2222:000:111",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NodeAddresses should get IP addresses from local IMDS for standard LoadBalancer if node's name is equal to metadataName",
|
||||
nodeName: "vm1",
|
||||
metadataName: "vm1",
|
||||
vmType: vmTypeStandard,
|
||||
ipV4: "10.240.0.1",
|
||||
ipV4Public: "192.168.1.12",
|
||||
ipV6: "1111:11111:00:00:1111:1111:000:111",
|
||||
ipV6Public: "2222:22221:00:00:2222:2222:000:111",
|
||||
loadBalancerSku: "standard",
|
||||
useInstanceMetadata: true,
|
||||
expectedAddress: []v1.NodeAddress{
|
||||
{
|
||||
Type: v1.NodeHostName,
|
||||
Address: "vm1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "10.240.0.1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: "192.168.1.12",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "1111:11111:00:00:1111:1111:000:111",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeExternalIP,
|
||||
Address: "2222:22221:00:00:2222:2222:000:111",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
if test.nilVMSet {
|
||||
cloud.VMSet = nil
|
||||
} else {
|
||||
cloud.VMSet = newAvailabilitySet(cloud)
|
||||
}
|
||||
cloud.Config.VMType = test.vmType
|
||||
cloud.Config.UseInstanceMetadata = test.useInstanceMetadata
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.RequestURI, imdsLoadBalancerURI) {
|
||||
fmt.Fprintf(w, loadbalancerTemplate, test.ipV4Public, test.ipV4, test.ipV6Public, test.ipV6)
|
||||
return
|
||||
}
|
||||
|
||||
if test.metadataTemplate != "" {
|
||||
fmt.Fprintf(w, test.metadataTemplate)
|
||||
} else {
|
||||
if test.loadBalancerSku == "standard" {
|
||||
fmt.Fprintf(w, metadataTemplate, test.metadataName, test.ipV4, "", test.ipV6, "")
|
||||
} else {
|
||||
fmt.Fprintf(w, metadataTemplate, test.metadataName, test.ipV4, test.ipV4Public, test.ipV6, test.ipV6Public)
|
||||
}
|
||||
}
|
||||
}))
|
||||
go func() {
|
||||
http.Serve(listener, mux)
|
||||
}()
|
||||
defer listener.Close()
|
||||
|
||||
cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
if test.useCustomImsCache {
|
||||
cloud.metadata.imsCache, err = azcache.NewTimedcache(metadataCacheTTL, func(key string) (interface{}, error) {
|
||||
return nil, fmt.Errorf("getError")
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
}
|
||||
mockVMClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm1", gomock.Any()).Return(expectedVM, nil).AnyTimes()
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm2", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
|
||||
mockPublicIPAddressesClient := cloud.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
|
||||
mockPublicIPAddressesClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "pip1", gomock.Any()).Return(expectedPIP, nil).AnyTimes()
|
||||
|
||||
mockInterfaceClient := cloud.InterfacesClient.(*mockinterfaceclient.MockInterface)
|
||||
mockInterfaceClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "nic", gomock.Any()).Return(expectedInterface, nil).AnyTimes()
|
||||
|
||||
ipAddresses, err := cloud.NodeAddresses(context.Background(), types.NodeName(test.nodeName))
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
assert.Equal(t, test.expectedAddress, ipAddresses, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceExistsByProviderID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cloud := GetTestCloud(ctrl)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
vmList []string
|
||||
nodeName string
|
||||
providerID string
|
||||
expected bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return true if node exists",
|
||||
vmList: []string{"vm2"},
|
||||
nodeName: "vm2",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return true if node is unmanaged",
|
||||
providerID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return false if node doesn't exist",
|
||||
vmList: []string{"vm1"},
|
||||
nodeName: "vm3",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm3",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should report error if providerID is invalid",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachine/vm3",
|
||||
expected: false,
|
||||
expectedErrMsg: fmt.Errorf("error splitting providerID"),
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should report error if providerID is null",
|
||||
expected: false,
|
||||
expectedErrMsg: fmt.Errorf("providerID is empty, the node is not initialized yet"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
vmListWithPowerState := make(map[string]string)
|
||||
for _, vm := range test.vmList {
|
||||
vmListWithPowerState[vm] = ""
|
||||
}
|
||||
expectedVMs := setTestVirtualMachines(cloud, vmListWithPowerState, false)
|
||||
mockVMsClient := cloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
for _, vm := range expectedVMs {
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, *vm.Name, gomock.Any()).Return(vm, nil).AnyTimes()
|
||||
}
|
||||
mockVMsClient.EXPECT().Get(gomock.Any(), cloud.ResourceGroup, "vm3", gomock.Any()).Return(compute.VirtualMachine{}, &retry.Error{HTTPStatusCode: http.StatusNotFound, RawError: cloudprovider.InstanceNotFound}).AnyTimes()
|
||||
mockVMsClient.EXPECT().Update(gomock.Any(), cloud.ResourceGroup, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
exist, err := cloud.InstanceExistsByProviderID(context.Background(), test.providerID)
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
assert.Equal(t, test.expected, exist, test.name)
|
||||
}
|
||||
|
||||
vmssTestCases := []struct {
|
||||
name string
|
||||
providerID string
|
||||
scaleSet string
|
||||
vmList []string
|
||||
expected bool
|
||||
rerr *retry.Error
|
||||
}{
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return true if VMSS and VM exist",
|
||||
providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmssee6c2/virtualMachines/0",
|
||||
scaleSet: "vmssee6c2",
|
||||
vmList: []string{"vmssee6c2000000"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return false if VMSS exist but VM doesn't",
|
||||
providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmssee6c2/virtualMachines/0",
|
||||
scaleSet: "vmssee6c2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "InstanceExistsByProviderID should return false if VMSS doesn't exist",
|
||||
providerID: "azure:///subscriptions/script/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/missing-vmss/virtualMachines/0",
|
||||
rerr: &retry.Error{HTTPStatusCode: 404},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range vmssTestCases {
|
||||
ss, err := newTestScaleSet(ctrl)
|
||||
assert.NoError(t, err, test.name)
|
||||
cloud.VMSet = ss
|
||||
|
||||
mockVMSSClient := mockvmssclient.NewMockInterface(ctrl)
|
||||
mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl)
|
||||
ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient
|
||||
ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient
|
||||
|
||||
expectedScaleSet := buildTestVMSS(test.scaleSet, test.scaleSet)
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, test.rerr).AnyTimes()
|
||||
|
||||
expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, test.scaleSet, "", 0, test.vmList, "succeeded", false)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, test.rerr).AnyTimes()
|
||||
|
||||
mockVMsClient := ss.cloud.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMsClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachine{}, nil).AnyTimes()
|
||||
|
||||
exist, _ := cloud.InstanceExistsByProviderID(context.Background(), test.providerID)
|
||||
assert.Equal(t, test.expected, exist, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAddressesByProviderID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cloud := GetTestCloud(ctrl)
|
||||
cloud.Config.UseInstanceMetadata = true
|
||||
metadataTemplate := `{"compute":{"name":"%s"},"network":{"interface":[{"ipv4":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]},"ipv6":{"ipAddress":[{"privateIpAddress":"%s","publicIpAddress":"%s"}]}}]}}`
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
nodeName string
|
||||
ipV4 string
|
||||
ipV6 string
|
||||
ipV4Public string
|
||||
ipV6Public string
|
||||
providerID string
|
||||
expectedAddress []v1.NodeAddress
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "NodeAddressesByProviderID should get both ipV4 and ipV6 private addresses",
|
||||
nodeName: "vm1",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
ipV4: "10.240.0.1",
|
||||
ipV6: "1111:11111:00:00:1111:1111:000:111",
|
||||
expectedAddress: []v1.NodeAddress{
|
||||
{
|
||||
Type: v1.NodeHostName,
|
||||
Address: "vm1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "10.240.0.1",
|
||||
},
|
||||
{
|
||||
Type: v1.NodeInternalIP,
|
||||
Address: "1111:11111:00:00:1111:1111:000:111",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NodeAddressesByProviderID should report error when IPs are empty",
|
||||
nodeName: "vm1",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
expectedErrMsg: fmt.Errorf("get empty IP addresses from instance metadata service"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddressesByProviderID should return nil if node is unmanaged",
|
||||
providerID: "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1",
|
||||
},
|
||||
{
|
||||
name: "NodeAddressesByProviderID should report error if providerID is invalid",
|
||||
providerID: "azure:///subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/virtualMachine/vm3",
|
||||
expectedErrMsg: fmt.Errorf("error splitting providerID"),
|
||||
},
|
||||
{
|
||||
name: "NodeAddressesByProviderID should report error if providerID is null",
|
||||
expectedErrMsg: fmt.Errorf("providerID is empty, the node is not initialized yet"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, metadataTemplate, test.nodeName, test.ipV4, test.ipV4Public, test.ipV6, test.ipV6Public)
|
||||
}))
|
||||
go func() {
|
||||
http.Serve(listener, mux)
|
||||
}()
|
||||
defer listener.Close()
|
||||
|
||||
cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
ipAddresses, err := cloud.NodeAddressesByProviderID(context.Background(), test.providerID)
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
assert.Equal(t, test.expectedAddress, ipAddresses, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentNodeName(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cloud := GetTestCloud(ctrl)
|
||||
|
||||
hostname := "testvm"
|
||||
nodeName, err := cloud.CurrentNodeName(context.Background(), hostname)
|
||||
assert.Equal(t, types.NodeName(hostname), nodeName)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,399 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
kwait "k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudvolume "k8s.io/cloud-provider/volume"
|
||||
volumehelpers "k8s.io/cloud-provider/volume/helpers"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const (
|
||||
// default IOPS Caps & Throughput Cap (MBps) per https://docs.microsoft.com/en-us/azure/virtual-machines/linux/disks-ultra-ssd
|
||||
defaultDiskIOPSReadWrite = 500
|
||||
defaultDiskMBpsReadWrite = 100
|
||||
|
||||
diskEncryptionSetIDFormat = "/subscriptions/{subs-id}/resourceGroups/{rg-name}/providers/Microsoft.Compute/diskEncryptionSets/{diskEncryptionSet-name}"
|
||||
)
|
||||
|
||||
// ManagedDiskController : managed disk controller struct
|
||||
type ManagedDiskController struct {
|
||||
common *controllerCommon
|
||||
}
|
||||
|
||||
// ManagedDiskOptions specifies the options of managed disks.
|
||||
type ManagedDiskOptions struct {
|
||||
// The name of the disk.
|
||||
DiskName string
|
||||
// The size in GB.
|
||||
SizeGB int
|
||||
// The name of PVC.
|
||||
PVCName string
|
||||
// The name of resource group.
|
||||
ResourceGroup string
|
||||
// The AvailabilityZone to create the disk.
|
||||
AvailabilityZone string
|
||||
// The tags of the disk.
|
||||
Tags map[string]string
|
||||
// The SKU of storage account.
|
||||
StorageAccountType compute.DiskStorageAccountTypes
|
||||
// IOPS Caps for UltraSSD disk
|
||||
DiskIOPSReadWrite string
|
||||
// Throughput Cap (MBps) for UltraSSD disk
|
||||
DiskMBpsReadWrite string
|
||||
// if SourceResourceID is not empty, then it's a disk copy operation(for snapshot)
|
||||
SourceResourceID string
|
||||
// The type of source
|
||||
SourceType string
|
||||
// ResourceId of the disk encryption set to use for enabling encryption at rest.
|
||||
DiskEncryptionSetID string
|
||||
// The maximum number of VMs that can attach to the disk at the same time. Value greater than one indicates a disk that can be mounted on multiple VMs at the same time.
|
||||
MaxShares int32
|
||||
}
|
||||
|
||||
// CreateManagedDisk : create managed disk
|
||||
func (c *ManagedDiskController) CreateManagedDisk(options *ManagedDiskOptions) (string, error) {
|
||||
var err error
|
||||
klog.V(4).Infof("azureDisk - creating new managed Name:%s StorageAccountType:%s Size:%v", options.DiskName, options.StorageAccountType, options.SizeGB)
|
||||
|
||||
var createZones []string
|
||||
if len(options.AvailabilityZone) > 0 {
|
||||
requestedZone := c.common.cloud.GetZoneID(options.AvailabilityZone)
|
||||
if requestedZone != "" {
|
||||
createZones = append(createZones, requestedZone)
|
||||
}
|
||||
}
|
||||
|
||||
// insert original tags to newTags
|
||||
newTags := make(map[string]*string)
|
||||
azureDDTag := "kubernetes-azure-dd"
|
||||
newTags["created-by"] = &azureDDTag
|
||||
if options.Tags != nil {
|
||||
for k, v := range options.Tags {
|
||||
// Azure won't allow / (forward slash) in tags
|
||||
newKey := strings.Replace(k, "/", "-", -1)
|
||||
newValue := strings.Replace(v, "/", "-", -1)
|
||||
newTags[newKey] = &newValue
|
||||
}
|
||||
}
|
||||
|
||||
diskSizeGB := int32(options.SizeGB)
|
||||
diskSku := compute.DiskStorageAccountTypes(options.StorageAccountType)
|
||||
|
||||
creationData, err := getValidCreationData(c.common.subscriptionID, options.ResourceGroup, options.SourceResourceID, options.SourceType)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
diskProperties := compute.DiskProperties{
|
||||
DiskSizeGB: &diskSizeGB,
|
||||
CreationData: &creationData,
|
||||
}
|
||||
|
||||
if diskSku == compute.UltraSSDLRS {
|
||||
diskIOPSReadWrite := int64(defaultDiskIOPSReadWrite)
|
||||
if options.DiskIOPSReadWrite != "" {
|
||||
v, err := strconv.Atoi(options.DiskIOPSReadWrite)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("AzureDisk - failed to parse DiskIOPSReadWrite: %v", err)
|
||||
}
|
||||
diskIOPSReadWrite = int64(v)
|
||||
}
|
||||
diskProperties.DiskIOPSReadWrite = pointer.Int64(diskIOPSReadWrite)
|
||||
|
||||
diskMBpsReadWrite := int64(defaultDiskMBpsReadWrite)
|
||||
if options.DiskMBpsReadWrite != "" {
|
||||
v, err := strconv.Atoi(options.DiskMBpsReadWrite)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("AzureDisk - failed to parse DiskMBpsReadWrite: %v", err)
|
||||
}
|
||||
diskMBpsReadWrite = int64(v)
|
||||
}
|
||||
diskProperties.DiskMBpsReadWrite = pointer.Int64(diskMBpsReadWrite)
|
||||
} else {
|
||||
if options.DiskIOPSReadWrite != "" {
|
||||
return "", fmt.Errorf("AzureDisk - DiskIOPSReadWrite parameter is only applicable in UltraSSD_LRS disk type")
|
||||
}
|
||||
if options.DiskMBpsReadWrite != "" {
|
||||
return "", fmt.Errorf("AzureDisk - DiskMBpsReadWrite parameter is only applicable in UltraSSD_LRS disk type")
|
||||
}
|
||||
}
|
||||
|
||||
if options.DiskEncryptionSetID != "" {
|
||||
if strings.Index(strings.ToLower(options.DiskEncryptionSetID), "/subscriptions/") != 0 {
|
||||
return "", fmt.Errorf("AzureDisk - format of DiskEncryptionSetID(%s) is incorrect, correct format: %s", options.DiskEncryptionSetID, diskEncryptionSetIDFormat)
|
||||
}
|
||||
diskProperties.Encryption = &compute.Encryption{
|
||||
DiskEncryptionSetID: &options.DiskEncryptionSetID,
|
||||
Type: compute.EncryptionAtRestWithCustomerKey,
|
||||
}
|
||||
}
|
||||
|
||||
if options.MaxShares > 1 {
|
||||
diskProperties.MaxShares = &options.MaxShares
|
||||
}
|
||||
|
||||
model := compute.Disk{
|
||||
Location: &c.common.location,
|
||||
Tags: newTags,
|
||||
Sku: &compute.DiskSku{
|
||||
Name: diskSku,
|
||||
},
|
||||
DiskProperties: &diskProperties,
|
||||
}
|
||||
|
||||
if len(createZones) > 0 {
|
||||
model.Zones = &createZones
|
||||
}
|
||||
|
||||
if options.ResourceGroup == "" {
|
||||
options.ResourceGroup = c.common.resourceGroup
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
rerr := c.common.cloud.DisksClient.CreateOrUpdate(ctx, options.ResourceGroup, options.DiskName, model)
|
||||
if rerr != nil {
|
||||
return "", rerr.Error()
|
||||
}
|
||||
|
||||
diskID := ""
|
||||
|
||||
err = kwait.ExponentialBackoff(defaultBackOff, func() (bool, error) {
|
||||
provisionState, id, err := c.GetDisk(options.ResourceGroup, options.DiskName)
|
||||
diskID = id
|
||||
// We are waiting for provisioningState==Succeeded
|
||||
// We don't want to hand-off managed disks to k8s while they are
|
||||
//still being provisioned, this is to avoid some race conditions
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.ToLower(provisionState) == "succeeded" {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
klog.V(2).Infof("azureDisk - created new MD Name:%s StorageAccountType:%s Size:%v but was unable to confirm provisioningState in poll process", options.DiskName, options.StorageAccountType, options.SizeGB)
|
||||
} else {
|
||||
klog.V(2).Infof("azureDisk - created new MD Name:%s StorageAccountType:%s Size:%v", options.DiskName, options.StorageAccountType, options.SizeGB)
|
||||
}
|
||||
|
||||
return diskID, nil
|
||||
}
|
||||
|
||||
// DeleteManagedDisk : delete managed disk
|
||||
func (c *ManagedDiskController) DeleteManagedDisk(diskURI string) error {
|
||||
diskName := path.Base(diskURI)
|
||||
resourceGroup, err := getResourceGroupFromDiskURI(diskURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
if _, ok := c.common.diskAttachDetachMap.Load(strings.ToLower(diskURI)); ok {
|
||||
return fmt.Errorf("failed to delete disk(%s) since it's in attaching or detaching state", diskURI)
|
||||
}
|
||||
|
||||
disk, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.V(2).Infof("azureDisk - disk(%s) is already deleted", diskURI)
|
||||
return nil
|
||||
}
|
||||
return rerr.Error()
|
||||
}
|
||||
|
||||
if disk.ManagedBy != nil {
|
||||
return fmt.Errorf("disk(%s) already attached to node(%s), could not be deleted", diskURI, *disk.ManagedBy)
|
||||
}
|
||||
|
||||
rerr = c.common.cloud.DisksClient.Delete(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
if rerr.HTTPStatusCode == http.StatusNotFound {
|
||||
klog.V(2).Infof("azureDisk - disk(%s) is already deleted", diskURI)
|
||||
return nil
|
||||
}
|
||||
return rerr.Error()
|
||||
}
|
||||
// We don't need poll here, k8s will immediately stop referencing the disk
|
||||
// the disk will be eventually deleted - cleanly - by ARM
|
||||
|
||||
klog.V(2).Infof("azureDisk - deleted a managed disk: %s", diskURI)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDisk return: disk provisionState, diskID, error
|
||||
func (c *ManagedDiskController) GetDisk(resourceGroup, diskName string) (string, string, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
result, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
return "", "", rerr.Error()
|
||||
}
|
||||
|
||||
if result.DiskProperties != nil && (*result.DiskProperties).ProvisioningState != nil {
|
||||
return *(*result.DiskProperties).ProvisioningState, *result.ID, nil
|
||||
}
|
||||
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// ResizeDisk Expand the disk to new size
|
||||
func (c *ManagedDiskController) ResizeDisk(diskURI string, oldSize resource.Quantity, newSize resource.Quantity) (resource.Quantity, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
diskName := path.Base(diskURI)
|
||||
resourceGroup, err := getResourceGroupFromDiskURI(diskURI)
|
||||
if err != nil {
|
||||
return oldSize, err
|
||||
}
|
||||
|
||||
result, rerr := c.common.cloud.DisksClient.Get(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
return oldSize, rerr.Error()
|
||||
}
|
||||
|
||||
if result.DiskProperties == nil || result.DiskProperties.DiskSizeGB == nil {
|
||||
return oldSize, fmt.Errorf("DiskProperties of disk(%s) is nil", diskName)
|
||||
}
|
||||
|
||||
// Azure resizes in chunks of GiB (not GB)
|
||||
requestGiB, err := volumehelpers.RoundUpToGiBInt32(newSize)
|
||||
if err != nil {
|
||||
return oldSize, err
|
||||
}
|
||||
|
||||
newSizeQuant := resource.MustParse(fmt.Sprintf("%dGi", requestGiB))
|
||||
|
||||
klog.V(2).Infof("azureDisk - begin to resize disk(%s) with new size(%d), old size(%v)", diskName, requestGiB, oldSize)
|
||||
// If disk already of greater or equal size than requested we return
|
||||
if *result.DiskProperties.DiskSizeGB >= requestGiB {
|
||||
return newSizeQuant, nil
|
||||
}
|
||||
|
||||
if result.DiskProperties.DiskState != compute.Unattached {
|
||||
return oldSize, fmt.Errorf("azureDisk - disk resize is only supported on Unattached disk, current disk state: %s, already attached to %s", result.DiskProperties.DiskState, pointer.StringDeref(result.ManagedBy, ""))
|
||||
}
|
||||
|
||||
diskParameter := compute.DiskUpdate{
|
||||
DiskUpdateProperties: &compute.DiskUpdateProperties{
|
||||
DiskSizeGB: &requestGiB,
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel = getContextWithCancel()
|
||||
defer cancel()
|
||||
if rerr := c.common.cloud.DisksClient.Update(ctx, resourceGroup, diskName, diskParameter); rerr != nil {
|
||||
return oldSize, rerr.Error()
|
||||
}
|
||||
|
||||
klog.V(2).Infof("azureDisk - resize disk(%s) with new size(%d) completed", diskName, requestGiB)
|
||||
|
||||
return newSizeQuant, nil
|
||||
}
|
||||
|
||||
// get resource group name from a managed disk URI, e.g. return {group-name} according to
|
||||
// /subscriptions/{sub-id}/resourcegroups/{group-name}/providers/microsoft.compute/disks/{disk-id}
|
||||
// according to https://docs.microsoft.com/en-us/rest/api/compute/disks/get
|
||||
func getResourceGroupFromDiskURI(diskURI string) (string, error) {
|
||||
fields := strings.Split(diskURI, "/")
|
||||
if len(fields) != 9 || strings.ToLower(fields[3]) != "resourcegroups" {
|
||||
return "", fmt.Errorf("invalid disk URI: %s", diskURI)
|
||||
}
|
||||
return fields[4], nil
|
||||
}
|
||||
|
||||
// GetLabelsForVolume implements PVLabeler.GetLabelsForVolume
|
||||
func (c *Cloud) GetLabelsForVolume(ctx context.Context, pv *v1.PersistentVolume) (map[string]string, error) {
|
||||
// Ignore if not AzureDisk.
|
||||
if pv.Spec.AzureDisk == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Ignore any volumes that are being provisioned
|
||||
if pv.Spec.AzureDisk.DiskName == cloudvolume.ProvisionedVolumeName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return c.GetAzureDiskLabels(pv.Spec.AzureDisk.DataDiskURI)
|
||||
}
|
||||
|
||||
// GetAzureDiskLabels gets availability zone labels for Azuredisk.
|
||||
func (c *Cloud) GetAzureDiskLabels(diskURI string) (map[string]string, error) {
|
||||
// Get disk's resource group.
|
||||
diskName := path.Base(diskURI)
|
||||
resourceGroup, err := getResourceGroupFromDiskURI(diskURI)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to get resource group for AzureDisk %q: %v", diskName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels := map[string]string{
|
||||
v1.LabelTopologyRegion: c.Location,
|
||||
}
|
||||
// no azure credential is set, return nil
|
||||
if c.DisksClient == nil {
|
||||
return labels, nil
|
||||
}
|
||||
// Get information of the disk.
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
disk, rerr := c.DisksClient.Get(ctx, resourceGroup, diskName)
|
||||
if rerr != nil {
|
||||
klog.Errorf("Failed to get information for AzureDisk %q: %v", diskName, rerr)
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
// Check whether availability zone is specified.
|
||||
if disk.Zones == nil || len(*disk.Zones) == 0 {
|
||||
klog.V(4).Infof("Azure disk %q is not zoned", diskName)
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
zones := *disk.Zones
|
||||
zoneID, err := strconv.Atoi(zones[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse zone %v for AzureDisk %v: %v", zones, diskName, err)
|
||||
}
|
||||
|
||||
zone := c.makeZone(c.Location, zoneID)
|
||||
klog.V(4).Infof("Got zone %q for Azure disk %q", zone, diskName)
|
||||
labels[v1.LabelTopologyZone] = zone
|
||||
return labels, nil
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
cloudvolume "k8s.io/cloud-provider/volume"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/diskclient/mockdiskclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestCreateManagedDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
maxShare := int32(2)
|
||||
goodDiskEncryptionSetID := fmt.Sprintf("/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/%s", "diskEncryptionSet-name")
|
||||
badDiskEncryptionSetID := fmt.Sprintf("badDiskEncryptionSetID")
|
||||
testTags := make(map[string]*string)
|
||||
testTags[WriteAcceleratorEnabled] = pointer.String("true")
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskID string
|
||||
diskName string
|
||||
storageAccountType compute.DiskStorageAccountTypes
|
||||
diskIOPSReadWrite string
|
||||
diskMBPSReadWrite string
|
||||
diskEncryptionSetID string
|
||||
expectedDiskID string
|
||||
existedDisk compute.Disk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "disk Id and no error shall be returned if everything is good with UltraSSDLRS storage account",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.UltraSSDLRS,
|
||||
diskIOPSReadWrite: "100",
|
||||
diskMBPSReadWrite: "100",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "diskid1",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "disk Id and no error shall be returned if everything is good with StandardLRS storage account",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.StandardLRS,
|
||||
diskIOPSReadWrite: "",
|
||||
diskMBPSReadWrite: "",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "diskid1",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account but DiskIOPSReadWrite is invalid",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.UltraSSDLRS,
|
||||
diskIOPSReadWrite: "invalid",
|
||||
diskMBPSReadWrite: "100",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("AzureDisk - failed to parse DiskIOPSReadWrite: strconv.Atoi: parsing \"invalid\": invalid syntax"),
|
||||
},
|
||||
{
|
||||
desc: "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account but DiskMBPSReadWrite is invalid",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.UltraSSDLRS,
|
||||
diskIOPSReadWrite: "100",
|
||||
diskMBPSReadWrite: "invalid",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("AzureDisk - failed to parse DiskMBpsReadWrite: strconv.Atoi: parsing \"invalid\": invalid syntax"),
|
||||
},
|
||||
{
|
||||
desc: "empty diskid and an error shall be returned if everything is good with UltraSSDLRS storage account with bad Disk EncryptionSetID",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.UltraSSDLRS,
|
||||
diskIOPSReadWrite: "100",
|
||||
diskMBPSReadWrite: "100",
|
||||
diskEncryptionSetID: badDiskEncryptionSetID,
|
||||
expectedDiskID: "",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("AzureDisk - format of DiskEncryptionSetID(%s) is incorrect, correct format: %s", badDiskEncryptionSetID, diskEncryptionSetIDFormat),
|
||||
},
|
||||
{
|
||||
desc: "disk Id and no error shall be returned if everything is good with StandardLRS storage account with not empty diskIOPSReadWrite",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.StandardLRS,
|
||||
diskIOPSReadWrite: "100",
|
||||
diskMBPSReadWrite: "",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("AzureDisk - DiskIOPSReadWrite parameter is only applicable in UltraSSD_LRS disk type"),
|
||||
},
|
||||
{
|
||||
desc: "disk Id and no error shall be returned if everything is good with StandardLRS storage account with not empty diskMBPSReadWrite",
|
||||
diskID: "diskid1",
|
||||
diskName: "disk1",
|
||||
storageAccountType: compute.StandardLRS,
|
||||
diskIOPSReadWrite: "",
|
||||
diskMBPSReadWrite: "100",
|
||||
diskEncryptionSetID: goodDiskEncryptionSetID,
|
||||
expectedDiskID: "",
|
||||
existedDisk: compute.Disk{ID: pointer.String("diskid1"), Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{Encryption: &compute.Encryption{DiskEncryptionSetID: &goodDiskEncryptionSetID, Type: compute.EncryptionAtRestWithCustomerKey}, ProvisioningState: pointer.String("Succeeded")}, Tags: testTags},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("AzureDisk - DiskMBpsReadWrite parameter is only applicable in UltraSSD_LRS disk type"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
managedDiskController := testCloud.ManagedDiskController
|
||||
volumeOptions := &ManagedDiskOptions{
|
||||
DiskName: test.diskName,
|
||||
StorageAccountType: test.storageAccountType,
|
||||
ResourceGroup: "",
|
||||
SizeGB: 1,
|
||||
Tags: map[string]string{"tag1": "azure-tag1"},
|
||||
AvailabilityZone: "westus-testzone",
|
||||
DiskIOPSReadWrite: test.diskIOPSReadWrite,
|
||||
DiskMBpsReadWrite: test.diskMBPSReadWrite,
|
||||
DiskEncryptionSetID: test.diskEncryptionSetID,
|
||||
MaxShares: maxShare,
|
||||
}
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
//disk := getTestDisk(test.diskName)
|
||||
mockDisksClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes()
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes()
|
||||
|
||||
actualDiskID, err := managedDiskController.CreateManagedDisk(volumeOptions)
|
||||
assert.Equal(t, test.expectedDiskID, actualDiskID, "TestCase[%d]: %s", i, test.desc)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected error: %v, return error: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteManagedDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
fakeGetDiskFailed := "fakeGetDiskFailed"
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskName string
|
||||
diskState string
|
||||
existedDisk compute.Disk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "an error shall be returned if delete an attaching disk",
|
||||
diskName: "disk1",
|
||||
diskState: "attaching",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1")},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("failed to delete disk(/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1) since it's in attaching or detaching state"),
|
||||
},
|
||||
{
|
||||
desc: "no error shall be returned if everything is good",
|
||||
diskName: "disk1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1")},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if get disk failed",
|
||||
diskName: fakeGetDiskFailed,
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeGetDiskFailed)},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
managedDiskController := testCloud.ManagedDiskController
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
if test.diskState == "attaching" {
|
||||
managedDiskController.common.diskAttachDetachMap.Store(strings.ToLower(diskURI), test.diskState)
|
||||
}
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
if test.diskName == fakeGetDiskFailed {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes()
|
||||
} else {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes()
|
||||
}
|
||||
mockDisksClient.EXPECT().Delete(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(nil).AnyTimes()
|
||||
|
||||
err := managedDiskController.DeleteManagedDisk(diskURI)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
fakeGetDiskFailed := "fakeGetDiskFailed"
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskName string
|
||||
existedDisk compute.Disk
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
expectedProvisioningState string
|
||||
expectedDiskID string
|
||||
}{
|
||||
{
|
||||
desc: "no error shall be returned if get a normal disk without DiskProperties",
|
||||
diskName: "disk1",
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1")},
|
||||
expectedErr: false,
|
||||
expectedProvisioningState: "",
|
||||
expectedDiskID: "",
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if get disk failed",
|
||||
diskName: fakeGetDiskFailed,
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeGetDiskFailed)},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")),
|
||||
expectedProvisioningState: "",
|
||||
expectedDiskID: "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
managedDiskController := testCloud.ManagedDiskController
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
if test.diskName == fakeGetDiskFailed {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes()
|
||||
} else {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes()
|
||||
}
|
||||
|
||||
provisioningState, diskid, err := managedDiskController.GetDisk(testCloud.ResourceGroup, test.diskName)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
assert.Equal(t, test.expectedProvisioningState, provisioningState, "TestCase[%d]: %s, expected ProvisioningState: %v, return ProvisioningState: %v", i, test.desc, test.expectedProvisioningState, provisioningState)
|
||||
assert.Equal(t, test.expectedDiskID, diskid, "TestCase[%d]: %s, expected DiskID: %v, return DiskID: %v", i, test.desc, test.expectedDiskID, diskid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResizeDisk(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
diskSizeGB := int32(2)
|
||||
diskName := "disk1"
|
||||
fakeGetDiskFailed := "fakeGetDiskFailed"
|
||||
fakeCreateDiskFailed := "fakeCreateDiskFailed"
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskName string
|
||||
oldSize resource.Quantity
|
||||
newSize resource.Quantity
|
||||
existedDisk compute.Disk
|
||||
expectedQuantity resource.Quantity
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "new quantity and no error shall be returned if everything is good",
|
||||
diskName: diskName,
|
||||
oldSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}},
|
||||
expectedQuantity: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "new quantity and no error shall be returned if everything is good with DiskProperties is null",
|
||||
diskName: diskName,
|
||||
oldSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1")},
|
||||
expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("DiskProperties of disk(%s) is nil", diskName),
|
||||
},
|
||||
{
|
||||
desc: "new quantity and no error shall be returned if everything is good with disk already of greater or equal size than requested",
|
||||
diskName: diskName,
|
||||
oldSize: *resource.NewQuantity(1*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String("disk1"), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}},
|
||||
expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if everything is good but get disk failed",
|
||||
diskName: fakeGetDiskFailed,
|
||||
oldSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeGetDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}},
|
||||
expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")),
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if everything is good but create disk failed",
|
||||
diskName: fakeCreateDiskFailed,
|
||||
oldSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeCreateDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Unattached}},
|
||||
expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Create Disk failed")),
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if disk is not in Unattached state",
|
||||
diskName: fakeCreateDiskFailed,
|
||||
oldSize: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
newSize: *resource.NewQuantity(3*(1024*1024*1024), resource.BinarySI),
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeCreateDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB, DiskState: compute.Attached}},
|
||||
expectedQuantity: *resource.NewQuantity(2*(1024*1024*1024), resource.BinarySI),
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("azureDisk - disk resize is only supported on Unattached disk, current disk state: Attached, already attached to "),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
managedDiskController := testCloud.ManagedDiskController
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud.SubscriptionID, testCloud.ResourceGroup, *test.existedDisk.Name)
|
||||
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
if test.diskName == fakeGetDiskFailed {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes()
|
||||
} else {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes()
|
||||
}
|
||||
if test.diskName == fakeCreateDiskFailed {
|
||||
mockDisksClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(&retry.Error{RawError: fmt.Errorf("Create Disk failed")}).AnyTimes()
|
||||
} else {
|
||||
mockDisksClient.EXPECT().Update(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes()
|
||||
}
|
||||
|
||||
result, err := managedDiskController.ResizeDisk(diskURI, test.oldSize, test.newSize)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
assert.Equal(t, test.expectedQuantity.Value(), result.Value(), "TestCase[%d]: %s, expected Quantity: %v, return Quantity: %v", i, test.desc, test.expectedQuantity, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelsForVolume(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
testCloud0 := GetTestCloud(ctrl)
|
||||
diskName := "disk1"
|
||||
diskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud0.SubscriptionID, testCloud0.ResourceGroup, diskName)
|
||||
diskSizeGB := int32(30)
|
||||
fakeGetDiskFailed := "fakeGetDiskFailed"
|
||||
fakeGetDiskFailedDiskURI := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s",
|
||||
testCloud0.SubscriptionID, testCloud0.ResourceGroup, fakeGetDiskFailed)
|
||||
testCases := []struct {
|
||||
desc string
|
||||
diskName string
|
||||
pv *v1.PersistentVolume
|
||||
existedDisk compute.Disk
|
||||
expected map[string]string
|
||||
expectedErr bool
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
desc: "labels and no error shall be returned if everything is good",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: diskName,
|
||||
DataDiskURI: diskURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}},
|
||||
expected: map[string]string{
|
||||
v1.LabelTopologyRegion: testCloud0.Location,
|
||||
v1.LabelTopologyZone: testCloud0.makeZone(testCloud0.Location, 1),
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if everything is good with invalid zone",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: diskName,
|
||||
DataDiskURI: diskURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"invalid"}},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("failed to parse zone [invalid] for AzureDisk %v: %v", diskName, "strconv.Atoi: parsing \"invalid\": invalid syntax"),
|
||||
},
|
||||
{
|
||||
desc: "nil shall be returned if everything is good with null Zones",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: diskName,
|
||||
DataDiskURI: diskURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}},
|
||||
expected: map[string]string{
|
||||
v1.LabelTopologyRegion: testCloud0.Location,
|
||||
},
|
||||
expectedErr: false,
|
||||
expectedErrMsg: nil,
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if everything is good with get disk failed",
|
||||
diskName: fakeGetDiskFailed,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: fakeGetDiskFailed,
|
||||
DataDiskURI: fakeGetDiskFailedDiskURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(fakeGetDiskFailed), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: %w", fmt.Errorf("Get Disk failed")),
|
||||
},
|
||||
{
|
||||
desc: "an error shall be returned if everything is good with invalid DiskURI",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: diskName,
|
||||
DataDiskURI: "invalidDiskURI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}, Zones: &[]string{"1"}},
|
||||
expectedErr: true,
|
||||
expectedErrMsg: fmt.Errorf("invalid disk URI: invalidDiskURI"),
|
||||
},
|
||||
{
|
||||
desc: "nil shall be returned if everything is good but pv.Spec.AzureDisk.DiskName is cloudvolume.ProvisionedVolumeName",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{
|
||||
AzureDisk: &v1.AzureDiskVolumeSource{
|
||||
DiskName: cloudvolume.ProvisionedVolumeName,
|
||||
DataDiskURI: diskURI,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}},
|
||||
expected: nil,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
desc: "nil shall be returned if everything is good but pv.Spec.AzureDisk is nil",
|
||||
diskName: diskName,
|
||||
pv: &v1.PersistentVolume{
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: v1.PersistentVolumeSource{},
|
||||
},
|
||||
},
|
||||
existedDisk: compute.Disk{Name: pointer.String(diskName), DiskProperties: &compute.DiskProperties{DiskSizeGB: &diskSizeGB}},
|
||||
expected: nil,
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
testCloud := GetTestCloud(ctrl)
|
||||
mockDisksClient := testCloud.DisksClient.(*mockdiskclient.MockInterface)
|
||||
if test.diskName == fakeGetDiskFailed {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, &retry.Error{RawError: fmt.Errorf("Get Disk failed")}).AnyTimes()
|
||||
} else {
|
||||
mockDisksClient.EXPECT().Get(gomock.Any(), testCloud.ResourceGroup, test.diskName).Return(test.existedDisk, nil).AnyTimes()
|
||||
}
|
||||
mockDisksClient.EXPECT().CreateOrUpdate(gomock.Any(), testCloud.ResourceGroup, test.diskName, gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
result, err := testCloud.GetLabelsForVolume(context.TODO(), test.pv)
|
||||
assert.Equal(t, test.expected, result, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expected, result)
|
||||
assert.Equal(t, test.expectedErr, err != nil, "TestCase[%d]: %s, return error: %v", i, test.desc, err)
|
||||
assert.Equal(t, test.expectedErrMsg, err, "TestCase[%d]: %s, expected: %v, return: %v", i, test.desc, test.expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
)
|
||||
|
||||
// CloudProviderRateLimitConfig indicates the rate limit config for each clients.
|
||||
type CloudProviderRateLimitConfig struct {
|
||||
// The default rate limit config options.
|
||||
azclients.RateLimitConfig
|
||||
|
||||
// Rate limit config for each clients. Values would override default settings above.
|
||||
RouteRateLimit *azclients.RateLimitConfig `json:"routeRateLimit,omitempty" yaml:"routeRateLimit,omitempty"`
|
||||
SubnetsRateLimit *azclients.RateLimitConfig `json:"subnetsRateLimit,omitempty" yaml:"subnetsRateLimit,omitempty"`
|
||||
InterfaceRateLimit *azclients.RateLimitConfig `json:"interfaceRateLimit,omitempty" yaml:"interfaceRateLimit,omitempty"`
|
||||
RouteTableRateLimit *azclients.RateLimitConfig `json:"routeTableRateLimit,omitempty" yaml:"routeTableRateLimit,omitempty"`
|
||||
LoadBalancerRateLimit *azclients.RateLimitConfig `json:"loadBalancerRateLimit,omitempty" yaml:"loadBalancerRateLimit,omitempty"`
|
||||
PublicIPAddressRateLimit *azclients.RateLimitConfig `json:"publicIPAddressRateLimit,omitempty" yaml:"publicIPAddressRateLimit,omitempty"`
|
||||
SecurityGroupRateLimit *azclients.RateLimitConfig `json:"securityGroupRateLimit,omitempty" yaml:"securityGroupRateLimit,omitempty"`
|
||||
VirtualMachineRateLimit *azclients.RateLimitConfig `json:"virtualMachineRateLimit,omitempty" yaml:"virtualMachineRateLimit,omitempty"`
|
||||
StorageAccountRateLimit *azclients.RateLimitConfig `json:"storageAccountRateLimit,omitempty" yaml:"storageAccountRateLimit,omitempty"`
|
||||
DiskRateLimit *azclients.RateLimitConfig `json:"diskRateLimit,omitempty" yaml:"diskRateLimit,omitempty"`
|
||||
SnapshotRateLimit *azclients.RateLimitConfig `json:"snapshotRateLimit,omitempty" yaml:"snapshotRateLimit,omitempty"`
|
||||
VirtualMachineScaleSetRateLimit *azclients.RateLimitConfig `json:"virtualMachineScaleSetRateLimit,omitempty" yaml:"virtualMachineScaleSetRateLimit,omitempty"`
|
||||
VirtualMachineSizeRateLimit *azclients.RateLimitConfig `json:"virtualMachineSizesRateLimit,omitempty" yaml:"virtualMachineSizesRateLimit,omitempty"`
|
||||
}
|
||||
|
||||
// InitializeCloudProviderRateLimitConfig initializes rate limit configs.
|
||||
func InitializeCloudProviderRateLimitConfig(config *CloudProviderRateLimitConfig) {
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign read rate limit defaults if no configuration was passed in.
|
||||
if config.CloudProviderRateLimitQPS == 0 {
|
||||
config.CloudProviderRateLimitQPS = rateLimitQPSDefault
|
||||
}
|
||||
if config.CloudProviderRateLimitBucket == 0 {
|
||||
config.CloudProviderRateLimitBucket = rateLimitBucketDefault
|
||||
}
|
||||
// Assign write rate limit defaults if no configuration was passed in.
|
||||
if config.CloudProviderRateLimitQPSWrite == 0 {
|
||||
config.CloudProviderRateLimitQPSWrite = config.CloudProviderRateLimitQPS
|
||||
}
|
||||
if config.CloudProviderRateLimitBucketWrite == 0 {
|
||||
config.CloudProviderRateLimitBucketWrite = config.CloudProviderRateLimitBucket
|
||||
}
|
||||
|
||||
config.RouteRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.RouteRateLimit)
|
||||
config.SubnetsRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SubnetsRateLimit)
|
||||
config.InterfaceRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.InterfaceRateLimit)
|
||||
config.RouteTableRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.RouteTableRateLimit)
|
||||
config.LoadBalancerRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.LoadBalancerRateLimit)
|
||||
config.PublicIPAddressRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.PublicIPAddressRateLimit)
|
||||
config.SecurityGroupRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SecurityGroupRateLimit)
|
||||
config.VirtualMachineRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineRateLimit)
|
||||
config.StorageAccountRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.StorageAccountRateLimit)
|
||||
config.DiskRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.DiskRateLimit)
|
||||
config.SnapshotRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.SnapshotRateLimit)
|
||||
config.VirtualMachineScaleSetRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineScaleSetRateLimit)
|
||||
config.VirtualMachineSizeRateLimit = overrideDefaultRateLimitConfig(&config.RateLimitConfig, config.VirtualMachineSizeRateLimit)
|
||||
}
|
||||
|
||||
// overrideDefaultRateLimitConfig overrides the default CloudProviderRateLimitConfig.
|
||||
func overrideDefaultRateLimitConfig(defaults, config *azclients.RateLimitConfig) *azclients.RateLimitConfig {
|
||||
// If config not set, apply defaults.
|
||||
if config == nil {
|
||||
return defaults
|
||||
}
|
||||
|
||||
// Remain disabled if it's set explicitly.
|
||||
if !config.CloudProviderRateLimit {
|
||||
return &azclients.RateLimitConfig{CloudProviderRateLimit: false}
|
||||
}
|
||||
|
||||
// Apply default values.
|
||||
if config.CloudProviderRateLimitQPS == 0 {
|
||||
config.CloudProviderRateLimitQPS = defaults.CloudProviderRateLimitQPS
|
||||
}
|
||||
if config.CloudProviderRateLimitBucket == 0 {
|
||||
config.CloudProviderRateLimitBucket = defaults.CloudProviderRateLimitBucket
|
||||
}
|
||||
if config.CloudProviderRateLimitQPSWrite == 0 {
|
||||
config.CloudProviderRateLimitQPSWrite = defaults.CloudProviderRateLimitQPSWrite
|
||||
}
|
||||
if config.CloudProviderRateLimitBucketWrite == 0 {
|
||||
config.CloudProviderRateLimitBucketWrite = defaults.CloudProviderRateLimitBucketWrite
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/legacy-cloud-providers/azure/auth"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
testAzureConfig = `{
|
||||
"aadClientCertPassword": "aadClientCertPassword",
|
||||
"aadClientCertPath": "aadClientCertPath",
|
||||
"aadClientId": "aadClientId",
|
||||
"aadClientSecret": "aadClientSecret",
|
||||
"cloud":"AzurePublicCloud",
|
||||
"cloudProviderBackoff": true,
|
||||
"cloudProviderBackoffDuration": 1,
|
||||
"cloudProviderBackoffExponent": 1,
|
||||
"cloudProviderBackoffJitter": 1,
|
||||
"cloudProviderBackoffRetries": 1,
|
||||
"cloudProviderRatelimit": true,
|
||||
"cloudProviderRateLimitBucket": 1,
|
||||
"cloudProviderRateLimitBucketWrite": 1,
|
||||
"cloudProviderRateLimitQPS": 1,
|
||||
"cloudProviderRateLimitQPSWrite": 1,
|
||||
"virtualMachineScaleSetRateLimit": {
|
||||
"cloudProviderRatelimit": true,
|
||||
"cloudProviderRateLimitBucket": 2,
|
||||
"CloudProviderRateLimitBucketWrite": 2,
|
||||
"cloudProviderRateLimitQPS": 0,
|
||||
"CloudProviderRateLimitQPSWrite": 0
|
||||
},
|
||||
"loadBalancerRateLimit": {
|
||||
"cloudProviderRatelimit": false,
|
||||
},
|
||||
"networkResourceTenantId": "networkResourceTenantId",
|
||||
"networkResourceSubscriptionId": "networkResourceSubscriptionId",
|
||||
"availabilitySetNodesCacheTTLInSeconds": 100,
|
||||
"vmssCacheTTLInSeconds": 100,
|
||||
"vmssVirtualMachinesCacheTTLInSeconds": 100,
|
||||
"vmCacheTTLInSeconds": 100,
|
||||
"loadBalancerCacheTTLInSeconds": 100,
|
||||
"nsgCacheTTLInSeconds": 100,
|
||||
"routeTableCacheTTLInSeconds": 100,
|
||||
"location": "location",
|
||||
"maximumLoadBalancerRuleCount": 1,
|
||||
"primaryAvailabilitySetName": "primaryAvailabilitySetName",
|
||||
"primaryScaleSetName": "primaryScaleSetName",
|
||||
"resourceGroup": "resourceGroup",
|
||||
"routeTableName": "routeTableName",
|
||||
"routeTableResourceGroup": "routeTableResourceGroup",
|
||||
"securityGroupName": "securityGroupName",
|
||||
"securityGroupResourceGroup": "securityGroupResourceGroup",
|
||||
"subnetName": "subnetName",
|
||||
"subscriptionId": "subscriptionId",
|
||||
"tenantId": "tenantId",
|
||||
"useInstanceMetadata": true,
|
||||
"useManagedIdentityExtension": true,
|
||||
"vnetName": "vnetName",
|
||||
"vnetResourceGroup": "vnetResourceGroup",
|
||||
vmType: "standard"
|
||||
}`
|
||||
|
||||
testDefaultRateLimitConfig = azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitBucket: 1,
|
||||
CloudProviderRateLimitBucketWrite: 1,
|
||||
CloudProviderRateLimitQPS: 1,
|
||||
CloudProviderRateLimitQPSWrite: 1,
|
||||
}
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
expected := &Config{
|
||||
AzureAuthConfig: auth.AzureAuthConfig{
|
||||
AADClientCertPassword: "aadClientCertPassword",
|
||||
AADClientCertPath: "aadClientCertPath",
|
||||
AADClientID: "aadClientId",
|
||||
AADClientSecret: "aadClientSecret",
|
||||
Cloud: "AzurePublicCloud",
|
||||
SubscriptionID: "subscriptionId",
|
||||
TenantID: "tenantId",
|
||||
UseManagedIdentityExtension: true,
|
||||
NetworkResourceTenantID: "networkResourceTenantId",
|
||||
NetworkResourceSubscriptionID: "networkResourceSubscriptionId",
|
||||
},
|
||||
CloudProviderBackoff: true,
|
||||
CloudProviderBackoffDuration: 1,
|
||||
CloudProviderBackoffExponent: 1,
|
||||
CloudProviderBackoffJitter: 1,
|
||||
CloudProviderBackoffRetries: 1,
|
||||
CloudProviderRateLimitConfig: CloudProviderRateLimitConfig{
|
||||
RateLimitConfig: testDefaultRateLimitConfig,
|
||||
LoadBalancerRateLimit: &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: false,
|
||||
},
|
||||
VirtualMachineScaleSetRateLimit: &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitBucket: 2,
|
||||
CloudProviderRateLimitBucketWrite: 2,
|
||||
},
|
||||
},
|
||||
AvailabilitySetNodesCacheTTLInSeconds: 100,
|
||||
VmssCacheTTLInSeconds: 100,
|
||||
VmssVirtualMachinesCacheTTLInSeconds: 100,
|
||||
VMCacheTTLInSeconds: 100,
|
||||
LoadBalancerCacheTTLInSeconds: 100,
|
||||
NsgCacheTTLInSeconds: 100,
|
||||
RouteTableCacheTTLInSeconds: 100,
|
||||
Location: "location",
|
||||
MaximumLoadBalancerRuleCount: 1,
|
||||
PrimaryAvailabilitySetName: "primaryAvailabilitySetName",
|
||||
PrimaryScaleSetName: "primaryScaleSetName",
|
||||
ResourceGroup: "resourcegroup",
|
||||
RouteTableName: "routeTableName",
|
||||
RouteTableResourceGroup: "routeTableResourceGroup",
|
||||
SecurityGroupName: "securityGroupName",
|
||||
SecurityGroupResourceGroup: "securityGroupResourceGroup",
|
||||
SubnetName: "subnetName",
|
||||
UseInstanceMetadata: true,
|
||||
VMType: "standard",
|
||||
VnetName: "vnetName",
|
||||
VnetResourceGroup: "vnetResourceGroup",
|
||||
}
|
||||
|
||||
buffer := bytes.NewBufferString(testAzureConfig)
|
||||
config, err := parseConfig(buffer)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, config)
|
||||
}
|
||||
|
||||
func TestInitializeCloudProviderRateLimitConfig(t *testing.T) {
|
||||
buffer := bytes.NewBufferString(testAzureConfig)
|
||||
config, err := parseConfig(buffer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
InitializeCloudProviderRateLimitConfig(&config.CloudProviderRateLimitConfig)
|
||||
assert.Equal(t, config.LoadBalancerRateLimit, &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: false,
|
||||
})
|
||||
assert.Equal(t, config.VirtualMachineScaleSetRateLimit, &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitBucket: 2,
|
||||
CloudProviderRateLimitBucketWrite: 2,
|
||||
CloudProviderRateLimitQPS: 1,
|
||||
CloudProviderRateLimitQPSWrite: 1,
|
||||
})
|
||||
assert.Equal(t, config.VirtualMachineSizeRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.VirtualMachineRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.RouteRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.SubnetsRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.InterfaceRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.RouteTableRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.SecurityGroupRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.StorageAccountRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.DiskRateLimit, &testDefaultRateLimitConfig)
|
||||
assert.Equal(t, config.SnapshotRateLimit, &testDefaultRateLimitConfig)
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/metrics"
|
||||
utilnet "k8s.io/utils/net"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
// routeUpdateInterval defines the route reconciling interval.
|
||||
routeUpdateInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
// routeOperation defines the allowed operations for route updating.
|
||||
type routeOperation string
|
||||
|
||||
// copied to minimize the number of cross reference
|
||||
// and exceptions in publishing and allowed imports.
|
||||
const (
|
||||
routeNameFmt = "%s____%s"
|
||||
routeNameSeparator = "____"
|
||||
|
||||
// Route operations.
|
||||
routeOperationAdd routeOperation = "add"
|
||||
routeOperationDelete routeOperation = "delete"
|
||||
routeTableOperationUpdateTags routeOperation = "updateRouteTableTags"
|
||||
)
|
||||
|
||||
// delayedRouteOperation defines a delayed route operation which is used in delayedRouteUpdater.
|
||||
type delayedRouteOperation struct {
|
||||
route network.Route
|
||||
routeTableTags map[string]*string
|
||||
operation routeOperation
|
||||
result chan error
|
||||
}
|
||||
|
||||
// wait waits for the operation completion and returns the result.
|
||||
func (op *delayedRouteOperation) wait() error {
|
||||
return <-op.result
|
||||
}
|
||||
|
||||
// delayedRouteUpdater defines a delayed route updater, which batches all the
|
||||
// route updating operations within "interval" period.
|
||||
// Example usage:
|
||||
//
|
||||
// op, err := updater.addRouteOperation(routeOperationAdd, route)
|
||||
// err = op.wait()
|
||||
type delayedRouteUpdater struct {
|
||||
az *Cloud
|
||||
interval time.Duration
|
||||
|
||||
lock sync.Mutex
|
||||
routesToUpdate []*delayedRouteOperation
|
||||
}
|
||||
|
||||
// newDelayedRouteUpdater creates a new delayedRouteUpdater.
|
||||
func newDelayedRouteUpdater(az *Cloud, interval time.Duration) *delayedRouteUpdater {
|
||||
return &delayedRouteUpdater{
|
||||
az: az,
|
||||
interval: interval,
|
||||
routesToUpdate: make([]*delayedRouteOperation, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// run starts the updater reconciling loop.
|
||||
func (d *delayedRouteUpdater) run() {
|
||||
err := wait.PollImmediateInfinite(d.interval, func() (bool, error) {
|
||||
d.updateRoutes()
|
||||
return false, nil
|
||||
})
|
||||
if err != nil { // this should never happen, if it does, panic
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// updateRoutes invokes route table client to update all routes.
|
||||
func (d *delayedRouteUpdater) updateRoutes() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
// No need to do any updating.
|
||||
if len(d.routesToUpdate) == 0 {
|
||||
klog.V(6).Info("updateRoutes: nothing to update, returning")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
defer func() {
|
||||
// Notify all the goroutines.
|
||||
for _, rt := range d.routesToUpdate {
|
||||
rt.result <- err
|
||||
}
|
||||
// Clear all the jobs.
|
||||
d.routesToUpdate = make([]*delayedRouteOperation, 0)
|
||||
}()
|
||||
|
||||
var (
|
||||
routeTable network.RouteTable
|
||||
existsRouteTable bool
|
||||
)
|
||||
routeTable, existsRouteTable, err = d.az.getRouteTable(azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
klog.Errorf("getRouteTable() failed with error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// create route table if it doesn't exists yet.
|
||||
if !existsRouteTable {
|
||||
err = d.az.createRouteTable()
|
||||
if err != nil {
|
||||
klog.Errorf("createRouteTable() failed with error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
routeTable, _, err = d.az.getRouteTable(azcache.CacheReadTypeDefault)
|
||||
if err != nil {
|
||||
klog.Errorf("getRouteTable() failed with error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// reconcile routes.
|
||||
dirty, onlyUpdateTags := false, true
|
||||
routes := []network.Route{}
|
||||
if routeTable.Routes != nil {
|
||||
routes = *routeTable.Routes
|
||||
}
|
||||
|
||||
routes, dirty = d.cleanupOutdatedRoutes(routes)
|
||||
if dirty {
|
||||
onlyUpdateTags = false
|
||||
}
|
||||
|
||||
for _, rt := range d.routesToUpdate {
|
||||
if rt.operation == routeTableOperationUpdateTags {
|
||||
routeTable.Tags = rt.routeTableTags
|
||||
dirty = true
|
||||
continue
|
||||
}
|
||||
|
||||
routeMatch := false
|
||||
onlyUpdateTags = false
|
||||
for i, existingRoute := range routes {
|
||||
if strings.EqualFold(pointer.StringDeref(existingRoute.Name, ""), pointer.StringDeref(rt.route.Name, "")) {
|
||||
// delete the name-matched routes here (missing routes would be added later if the operation is add).
|
||||
routes = append(routes[:i], routes[i+1:]...)
|
||||
if existingRoute.RoutePropertiesFormat != nil &&
|
||||
rt.route.RoutePropertiesFormat != nil &&
|
||||
strings.EqualFold(pointer.StringDeref(existingRoute.AddressPrefix, ""), pointer.StringDeref(rt.route.AddressPrefix, "")) &&
|
||||
strings.EqualFold(pointer.StringDeref(existingRoute.NextHopIPAddress, ""), pointer.StringDeref(rt.route.NextHopIPAddress, "")) {
|
||||
routeMatch = true
|
||||
}
|
||||
if rt.operation == routeOperationDelete {
|
||||
dirty = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing routes if the operation is add.
|
||||
if rt.operation == routeOperationAdd {
|
||||
routes = append(routes, rt.route)
|
||||
if !routeMatch {
|
||||
dirty = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if dirty {
|
||||
if !onlyUpdateTags {
|
||||
klog.V(2).Infof("updateRoutes: updating routes")
|
||||
routeTable.Routes = &routes
|
||||
}
|
||||
err = d.az.CreateOrUpdateRouteTable(routeTable)
|
||||
if err != nil {
|
||||
klog.Errorf("CreateOrUpdateRouteTable() failed with error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupOutdatedRoutes deletes all non-dualstack routes when dualstack is enabled,
|
||||
// and deletes all dualstack routes when dualstack is not enabled.
|
||||
func (d *delayedRouteUpdater) cleanupOutdatedRoutes(existingRoutes []network.Route) (routes []network.Route, changed bool) {
|
||||
for i := len(existingRoutes) - 1; i >= 0; i-- {
|
||||
existingRouteName := pointer.StringDeref(existingRoutes[i].Name, "")
|
||||
split := strings.Split(existingRouteName, routeNameSeparator)
|
||||
|
||||
klog.V(4).Infof("cleanupOutdatedRoutes: checking route %s", existingRouteName)
|
||||
|
||||
// filter out unmanaged routes
|
||||
deleteRoute := false
|
||||
if d.az.nodeNames.Has(split[0]) {
|
||||
if d.az.ipv6DualStackEnabled && len(split) == 1 {
|
||||
klog.V(2).Infof("cleanupOutdatedRoutes: deleting outdated non-dualstack route %s", existingRouteName)
|
||||
deleteRoute = true
|
||||
} else if !d.az.ipv6DualStackEnabled && len(split) == 2 {
|
||||
klog.V(2).Infof("cleanupOutdatedRoutes: deleting outdated dualstack route %s", existingRouteName)
|
||||
deleteRoute = true
|
||||
}
|
||||
|
||||
if deleteRoute {
|
||||
existingRoutes = append(existingRoutes[:i], existingRoutes[i+1:]...)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return existingRoutes, changed
|
||||
}
|
||||
|
||||
// addRouteOperation adds the routeOperation to delayedRouteUpdater and returns a delayedRouteOperation.
|
||||
func (d *delayedRouteUpdater) addRouteOperation(operation routeOperation, route network.Route) (*delayedRouteOperation, error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
op := &delayedRouteOperation{
|
||||
route: route,
|
||||
operation: operation,
|
||||
result: make(chan error),
|
||||
}
|
||||
d.routesToUpdate = append(d.routesToUpdate, op)
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// addUpdateRouteTableTagsOperation adds a update route table tags operation to delayedRouteUpdater and returns a delayedRouteOperation.
|
||||
func (d *delayedRouteUpdater) addUpdateRouteTableTagsOperation(operation routeOperation, tags map[string]*string) (*delayedRouteOperation, error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
op := &delayedRouteOperation{
|
||||
routeTableTags: tags,
|
||||
operation: operation,
|
||||
result: make(chan error),
|
||||
}
|
||||
d.routesToUpdate = append(d.routesToUpdate, op)
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// ListRoutes lists all managed routes that belong to the specified clusterName
|
||||
func (az *Cloud) ListRoutes(ctx context.Context, clusterName string) ([]*cloudprovider.Route, error) {
|
||||
klog.V(10).Infof("ListRoutes: START clusterName=%q", clusterName)
|
||||
routeTable, existsRouteTable, err := az.getRouteTable(azcache.CacheReadTypeDefault)
|
||||
routes, err := processRoutes(az.ipv6DualStackEnabled, routeTable, existsRouteTable, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compose routes for unmanaged routes so that node controller won't retry creating routes for them.
|
||||
unmanagedNodes, err := az.GetUnmanagedNodes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
az.routeCIDRsLock.Lock()
|
||||
defer az.routeCIDRsLock.Unlock()
|
||||
for _, nodeName := range unmanagedNodes.List() {
|
||||
if cidr, ok := az.routeCIDRs[nodeName]; ok {
|
||||
routes = append(routes, &cloudprovider.Route{
|
||||
Name: nodeName,
|
||||
TargetNode: mapRouteNameToNodeName(az.ipv6DualStackEnabled, nodeName),
|
||||
DestinationCIDR: cidr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the route table is tagged as configured
|
||||
tags, changed := az.ensureRouteTableTagged(&routeTable)
|
||||
if changed {
|
||||
klog.V(2).Infof("ListRoutes: updating tags on route table %s", pointer.StringDeref(routeTable.Name, ""))
|
||||
op, err := az.routeUpdater.addUpdateRouteTableTagsOperation(routeTableOperationUpdateTags, tags)
|
||||
if err != nil {
|
||||
klog.Errorf("ListRoutes: failed to add route table operation with error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for operation complete.
|
||||
err = op.wait()
|
||||
if err != nil {
|
||||
klog.Errorf("ListRoutes: failed to update route table tags with error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// Injectable for testing
|
||||
func processRoutes(ipv6DualStackEnabled bool, routeTable network.RouteTable, exists bool, err error) ([]*cloudprovider.Route, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return []*cloudprovider.Route{}, nil
|
||||
}
|
||||
|
||||
var kubeRoutes []*cloudprovider.Route
|
||||
if routeTable.RouteTablePropertiesFormat != nil && routeTable.Routes != nil {
|
||||
kubeRoutes = make([]*cloudprovider.Route, len(*routeTable.Routes))
|
||||
for i, route := range *routeTable.Routes {
|
||||
instance := mapRouteNameToNodeName(ipv6DualStackEnabled, *route.Name)
|
||||
cidr := *route.AddressPrefix
|
||||
klog.V(10).Infof("ListRoutes: * instance=%q, cidr=%q", instance, cidr)
|
||||
|
||||
kubeRoutes[i] = &cloudprovider.Route{
|
||||
Name: *route.Name,
|
||||
TargetNode: instance,
|
||||
DestinationCIDR: cidr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(10).Info("ListRoutes: FINISH")
|
||||
return kubeRoutes, nil
|
||||
}
|
||||
|
||||
func (az *Cloud) createRouteTable() error {
|
||||
routeTable := network.RouteTable{
|
||||
Name: pointer.String(az.RouteTableName),
|
||||
Location: pointer.String(az.Location),
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
|
||||
}
|
||||
|
||||
klog.V(3).Infof("createRouteTableIfNotExists: creating routetable. routeTableName=%q", az.RouteTableName)
|
||||
err := az.CreateOrUpdateRouteTable(routeTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate the cache right after updating
|
||||
az.rtCache.Delete(az.RouteTableName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateRoute creates the described managed route
|
||||
// route.Name will be ignored, although the cloud-provider may use nameHint
|
||||
// to create a more user-meaningful name.
|
||||
func (az *Cloud) CreateRoute(ctx context.Context, clusterName string, nameHint string, kubeRoute *cloudprovider.Route) error {
|
||||
mc := metrics.NewMetricContext("routes", "create_route", az.ResourceGroup, az.SubscriptionID, "")
|
||||
isOperationSucceeded := false
|
||||
defer func() {
|
||||
mc.ObserveOperationWithResult(isOperationSucceeded)
|
||||
}()
|
||||
|
||||
// Returns for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
var targetIP string
|
||||
nodeName := string(kubeRoute.TargetNode)
|
||||
unmanaged, err := az.IsNodeUnmanaged(nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unmanaged {
|
||||
if az.ipv6DualStackEnabled {
|
||||
//TODO (khenidak) add support for unmanaged nodes when the feature reaches beta
|
||||
return fmt.Errorf("unmanaged nodes are not supported in dual stack mode")
|
||||
}
|
||||
klog.V(2).Infof("CreateRoute: omitting unmanaged node %q", kubeRoute.TargetNode)
|
||||
az.routeCIDRsLock.Lock()
|
||||
defer az.routeCIDRsLock.Unlock()
|
||||
az.routeCIDRs[nodeName] = kubeRoute.DestinationCIDR
|
||||
return nil
|
||||
}
|
||||
|
||||
CIDRv6 := utilnet.IsIPv6CIDRString(string(kubeRoute.DestinationCIDR))
|
||||
// if single stack IPv4 then get the IP for the primary ip config
|
||||
// single stack IPv6 is supported on dual stack host. So the IPv6 IP is secondary IP for both single stack IPv6 and dual stack
|
||||
// Get all private IPs for the machine and find the first one that matches the IPv6 family
|
||||
if !az.ipv6DualStackEnabled && !CIDRv6 {
|
||||
targetIP, _, err = az.getIPForMachine(kubeRoute.TargetNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// for dual stack and single stack IPv6 we need to select
|
||||
// a private ip that matches family of the cidr
|
||||
klog.V(4).Infof("CreateRoute: create route instance=%q cidr=%q is in dual stack mode", kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
|
||||
nodePrivateIPs, err := az.getPrivateIPsForMachine(kubeRoute.TargetNode)
|
||||
if nil != err {
|
||||
klog.V(3).Infof("CreateRoute: create route: failed(GetPrivateIPsByNodeName) instance=%q cidr=%q with error=%v", kubeRoute.TargetNode, kubeRoute.DestinationCIDR, err)
|
||||
return err
|
||||
}
|
||||
|
||||
targetIP, err = findFirstIPByFamily(nodePrivateIPs, CIDRv6)
|
||||
if nil != err {
|
||||
klog.V(3).Infof("CreateRoute: create route: failed(findFirstIpByFamily) instance=%q cidr=%q with error=%v", kubeRoute.TargetNode, kubeRoute.DestinationCIDR, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, string(kubeRoute.DestinationCIDR))
|
||||
route := network.Route{
|
||||
Name: pointer.String(routeName),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String(kubeRoute.DestinationCIDR),
|
||||
NextHopType: network.RouteNextHopTypeVirtualAppliance,
|
||||
NextHopIPAddress: pointer.String(targetIP),
|
||||
},
|
||||
}
|
||||
|
||||
klog.V(2).Infof("CreateRoute: creating route for clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
|
||||
op, err := az.routeUpdater.addRouteOperation(routeOperationAdd, route)
|
||||
if err != nil {
|
||||
klog.Errorf("CreateRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for operation complete.
|
||||
err = op.wait()
|
||||
if err != nil {
|
||||
klog.Errorf("CreateRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(2).Infof("CreateRoute: route created. clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
|
||||
isOperationSucceeded = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRoute deletes the specified managed route
|
||||
// Route should be as returned by ListRoutes
|
||||
func (az *Cloud) DeleteRoute(ctx context.Context, clusterName string, kubeRoute *cloudprovider.Route) error {
|
||||
mc := metrics.NewMetricContext("routes", "delete_route", az.ResourceGroup, az.SubscriptionID, "")
|
||||
isOperationSucceeded := false
|
||||
defer func() {
|
||||
mc.ObserveOperationWithResult(isOperationSucceeded)
|
||||
}()
|
||||
|
||||
// Returns for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
nodeName := string(kubeRoute.TargetNode)
|
||||
unmanaged, err := az.IsNodeUnmanaged(nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unmanaged {
|
||||
klog.V(2).Infof("DeleteRoute: omitting unmanaged node %q", kubeRoute.TargetNode)
|
||||
az.routeCIDRsLock.Lock()
|
||||
defer az.routeCIDRsLock.Unlock()
|
||||
delete(az.routeCIDRs, nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
routeName := mapNodeNameToRouteName(az.ipv6DualStackEnabled, kubeRoute.TargetNode, string(kubeRoute.DestinationCIDR))
|
||||
klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeName)
|
||||
route := network.Route{
|
||||
Name: pointer.String(routeName),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{},
|
||||
}
|
||||
op, err := az.routeUpdater.addRouteOperation(routeOperationDelete, route)
|
||||
if err != nil {
|
||||
klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for operation complete.
|
||||
err = op.wait()
|
||||
if err != nil {
|
||||
klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove outdated ipv4 routes as well
|
||||
if az.ipv6DualStackEnabled {
|
||||
routeNameWithoutIPV6Suffix := strings.Split(routeName, routeNameSeparator)[0]
|
||||
klog.V(2).Infof("DeleteRoute: deleting route. clusterName=%q instance=%q cidr=%q routeName=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR, routeNameWithoutIPV6Suffix)
|
||||
route := network.Route{
|
||||
Name: pointer.String(routeNameWithoutIPV6Suffix),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{},
|
||||
}
|
||||
op, err := az.routeUpdater.addRouteOperation(routeOperationDelete, route)
|
||||
if err != nil {
|
||||
klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for operation complete.
|
||||
err = op.wait()
|
||||
if err != nil {
|
||||
klog.Errorf("DeleteRoute failed for node %q with error: %v", kubeRoute.TargetNode, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(2).Infof("DeleteRoute: route deleted. clusterName=%q instance=%q cidr=%q", clusterName, kubeRoute.TargetNode, kubeRoute.DestinationCIDR)
|
||||
isOperationSucceeded = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This must be kept in sync with mapRouteNameToNodeName.
|
||||
// These two functions enable stashing the instance name in the route
|
||||
// and then retrieving it later when listing. This is needed because
|
||||
// Azure does not let you put tags/descriptions on the Route itself.
|
||||
func mapNodeNameToRouteName(ipv6DualStackEnabled bool, nodeName types.NodeName, cidr string) string {
|
||||
if !ipv6DualStackEnabled {
|
||||
return fmt.Sprintf("%s", nodeName)
|
||||
}
|
||||
return fmt.Sprintf(routeNameFmt, nodeName, cidrtoRfc1035(cidr))
|
||||
}
|
||||
|
||||
// Used with mapNodeNameToRouteName. See comment on mapNodeNameToRouteName.
|
||||
func mapRouteNameToNodeName(ipv6DualStackEnabled bool, routeName string) types.NodeName {
|
||||
if !ipv6DualStackEnabled {
|
||||
return types.NodeName(routeName)
|
||||
}
|
||||
parts := strings.Split(routeName, routeNameSeparator)
|
||||
nodeName := parts[0]
|
||||
return types.NodeName(nodeName)
|
||||
|
||||
}
|
||||
|
||||
// given a list of ips, return the first one
|
||||
// that matches the family requested
|
||||
// error if no match, or failure to parse
|
||||
// any of the ips
|
||||
func findFirstIPByFamily(ips []string, v6 bool) (string, error) {
|
||||
for _, ip := range ips {
|
||||
bIPv6 := utilnet.IsIPv6String(ip)
|
||||
if v6 == bIPv6 {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no match found matching the ipfamily requested")
|
||||
}
|
||||
|
||||
// strips : . /
|
||||
func cidrtoRfc1035(cidr string) string {
|
||||
cidr = strings.ReplaceAll(cidr, ":", "")
|
||||
cidr = strings.ReplaceAll(cidr, ".", "")
|
||||
cidr = strings.ReplaceAll(cidr, "/", "")
|
||||
return cidr
|
||||
}
|
||||
|
||||
// ensureRouteTableTagged ensures the route table is tagged as configured
|
||||
func (az *Cloud) ensureRouteTableTagged(rt *network.RouteTable) (map[string]*string, bool) {
|
||||
if az.Tags == "" {
|
||||
return nil, false
|
||||
}
|
||||
tags := parseTags(az.Tags)
|
||||
if rt.Tags == nil {
|
||||
rt.Tags = make(map[string]*string)
|
||||
}
|
||||
|
||||
tags, changed := reconcileTags(rt.Tags, tags)
|
||||
rt.Tags = tags
|
||||
|
||||
return rt.Tags, changed
|
||||
}
|
||||
@@ -1,813 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/routetableclient/mockroutetableclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/mockvmsets"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestDeleteRoute(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
|
||||
|
||||
cloud := &Cloud{
|
||||
RouteTablesClient: routeTableClient,
|
||||
Config: Config{
|
||||
RouteTableResourceGroup: "foo",
|
||||
RouteTableName: "bar",
|
||||
Location: "location",
|
||||
},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
nodeInformerSynced: func() bool { return true },
|
||||
}
|
||||
cache, _ := cloud.newRouteTableCache()
|
||||
cloud.rtCache = cache
|
||||
cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
|
||||
go cloud.routeUpdater.run()
|
||||
route := cloudprovider.Route{
|
||||
TargetNode: "node",
|
||||
DestinationCIDR: "1.2.3.4/24",
|
||||
}
|
||||
routeName := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR)
|
||||
routeTables := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: &routeName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
routeTablesAfterDeletion := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{},
|
||||
},
|
||||
}
|
||||
routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil)
|
||||
routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterDeletion, "").Return(nil)
|
||||
err := cloud.DeleteRoute(context.TODO(), "cluster", &route)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error deleting route: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// test delete route for unmanaged nodes.
|
||||
nodeName := "node1"
|
||||
nodeCIDR := "4.3.2.1/24"
|
||||
cloud.unmanagedNodes.Insert(nodeName)
|
||||
cloud.routeCIDRs = map[string]string{
|
||||
nodeName: nodeCIDR,
|
||||
}
|
||||
route1 := cloudprovider.Route{
|
||||
TargetNode: mapRouteNameToNodeName(false, nodeName),
|
||||
DestinationCIDR: nodeCIDR,
|
||||
}
|
||||
err = cloud.DeleteRoute(context.TODO(), "cluster", &route1)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error deleting route: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
cidr, found := cloud.routeCIDRs[nodeName]
|
||||
if found {
|
||||
t.Errorf("unexpected CIDR item (%q) for %s", cidr, nodeName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRoute(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
|
||||
mockVMSet := mockvmsets.NewMockVMSet(ctrl)
|
||||
|
||||
cloud := &Cloud{
|
||||
RouteTablesClient: routeTableClient,
|
||||
VMSet: mockVMSet,
|
||||
Config: Config{
|
||||
RouteTableResourceGroup: "foo",
|
||||
RouteTableName: "bar",
|
||||
Location: "location",
|
||||
},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
nodeInformerSynced: func() bool { return true },
|
||||
}
|
||||
cache, _ := cloud.newRouteTableCache()
|
||||
cloud.rtCache = cache
|
||||
cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
|
||||
go cloud.routeUpdater.run()
|
||||
|
||||
route := cloudprovider.Route{TargetNode: "node", DestinationCIDR: "1.2.3.4/24"}
|
||||
nodePrivateIP := "2.4.6.8"
|
||||
networkRoute := &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("node"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/24"),
|
||||
NextHopIPAddress: &nodePrivateIP,
|
||||
NextHopType: network.RouteNextHopTypeVirtualAppliance,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
routeTableName string
|
||||
initialRoute *[]network.Route
|
||||
updatedRoute *[]network.Route
|
||||
hasUnmanagedNodes bool
|
||||
nodeInformerNotSynced bool
|
||||
ipv6DualStackEnabled bool
|
||||
routeTableNotExist bool
|
||||
unmanagedNodeName string
|
||||
routeCIDRs map[string]string
|
||||
expectedRouteCIDRs map[string]string
|
||||
|
||||
getIPError error
|
||||
getErr *retry.Error
|
||||
secondGetErr *retry.Error
|
||||
createOrUpdateErr *retry.Error
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "CreateRoute should create route if route doesn't exist",
|
||||
routeTableName: "rt1",
|
||||
updatedRoute: networkRoute,
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if error occurs when invoke CreateOrUpdateRouteTable",
|
||||
routeTableName: "rt2",
|
||||
updatedRoute: networkRoute,
|
||||
createOrUpdateErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
RawError: fmt.Errorf("CreateOrUpdate error"),
|
||||
},
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("CreateOrUpdate error")),
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should do nothing if route already exists",
|
||||
routeTableName: "rt3",
|
||||
initialRoute: networkRoute,
|
||||
updatedRoute: networkRoute,
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if error occurs when invoke createRouteTable",
|
||||
routeTableName: "rt4",
|
||||
getErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
RawError: cloudprovider.InstanceNotFound,
|
||||
},
|
||||
createOrUpdateErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
RawError: fmt.Errorf("CreateOrUpdate error"),
|
||||
},
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("CreateOrUpdate error")),
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if error occurs when invoke getRouteTable for the second time",
|
||||
routeTableName: "rt5",
|
||||
getErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
RawError: cloudprovider.InstanceNotFound,
|
||||
},
|
||||
secondGetErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
RawError: fmt.Errorf("Get error"),
|
||||
},
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")),
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if error occurs when invoke routeTableClient.Get",
|
||||
routeTableName: "rt6",
|
||||
getErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
RawError: fmt.Errorf("Get error"),
|
||||
},
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")),
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if error occurs when invoke GetIPByNodeName",
|
||||
routeTableName: "rt7",
|
||||
getIPError: fmt.Errorf("getIP error"),
|
||||
expectedErrMsg: wait.ErrWaitTimeout,
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should add route to cloud.RouteCIDRs if node is unmanaged",
|
||||
routeTableName: "rt8",
|
||||
hasUnmanagedNodes: true,
|
||||
unmanagedNodeName: "node",
|
||||
routeCIDRs: map[string]string{},
|
||||
expectedRouteCIDRs: map[string]string{"node": "1.2.3.4/24"},
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if node is unmanaged and cloud.ipv6DualStackEnabled is true",
|
||||
hasUnmanagedNodes: true,
|
||||
ipv6DualStackEnabled: true,
|
||||
unmanagedNodeName: "node",
|
||||
expectedErrMsg: fmt.Errorf("unmanaged nodes are not supported in dual stack mode"),
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should create route if cloud.ipv6DualStackEnabled is true and route doesn't exist",
|
||||
routeTableName: "rt9",
|
||||
updatedRoute: &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("node____123424"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/24"),
|
||||
NextHopIPAddress: &nodePrivateIP,
|
||||
NextHopType: network.RouteNextHopTypeVirtualAppliance,
|
||||
},
|
||||
},
|
||||
},
|
||||
ipv6DualStackEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "CreateRoute should report error if node informer is not synced",
|
||||
nodeInformerNotSynced: true,
|
||||
expectedErrMsg: fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
initialTable := network.RouteTable{
|
||||
Name: pointer.String(test.routeTableName),
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: test.initialRoute,
|
||||
},
|
||||
}
|
||||
updatedTable := network.RouteTable{
|
||||
Name: pointer.String(test.routeTableName),
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: test.updatedRoute,
|
||||
},
|
||||
}
|
||||
|
||||
cloud.RouteTableName = test.routeTableName
|
||||
cloud.ipv6DualStackEnabled = test.ipv6DualStackEnabled
|
||||
if test.hasUnmanagedNodes {
|
||||
cloud.unmanagedNodes.Insert(test.unmanagedNodeName)
|
||||
cloud.routeCIDRs = test.routeCIDRs
|
||||
} else {
|
||||
cloud.unmanagedNodes = sets.NewString()
|
||||
cloud.routeCIDRs = nil
|
||||
}
|
||||
if test.nodeInformerNotSynced {
|
||||
cloud.nodeInformerSynced = func() bool { return false }
|
||||
} else {
|
||||
cloud.nodeInformerSynced = func() bool { return true }
|
||||
}
|
||||
|
||||
mockVMSet.EXPECT().GetIPByNodeName(gomock.Any()).Return(nodePrivateIP, "", test.getIPError).MaxTimes(1)
|
||||
mockVMSet.EXPECT().GetPrivateIPsByNodeName("node").Return([]string{nodePrivateIP, "10.10.10.10"}, nil).MaxTimes(1)
|
||||
routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.getErr).MaxTimes(1)
|
||||
routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, updatedTable, "").Return(test.createOrUpdateErr).MaxTimes(1)
|
||||
|
||||
//Here is the second invocation when route table doesn't exist
|
||||
routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(initialTable, test.secondGetErr).MaxTimes(1)
|
||||
|
||||
err := cloud.CreateRoute(context.TODO(), "cluster", "unused", &route)
|
||||
assert.Equal(t, cloud.routeCIDRs, test.expectedRouteCIDRs, test.name)
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRouteTable(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
|
||||
|
||||
cloud := &Cloud{
|
||||
RouteTablesClient: routeTableClient,
|
||||
Config: Config{
|
||||
RouteTableResourceGroup: "foo",
|
||||
RouteTableName: "bar",
|
||||
Location: "location",
|
||||
},
|
||||
}
|
||||
cache, _ := cloud.newRouteTableCache()
|
||||
cloud.rtCache = cache
|
||||
|
||||
expectedTable := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
|
||||
}
|
||||
routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, expectedTable, "").Return(nil)
|
||||
err := cloud.createRouteTable()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error in creating route table: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessRoutes(t *testing.T) {
|
||||
tests := []struct {
|
||||
rt network.RouteTable
|
||||
exists bool
|
||||
err error
|
||||
expectErr bool
|
||||
expectedError string
|
||||
expectedRoute []cloudprovider.Route
|
||||
name string
|
||||
}{
|
||||
{
|
||||
err: fmt.Errorf("test error"),
|
||||
expectErr: true,
|
||||
expectedError: "test error",
|
||||
},
|
||||
{
|
||||
exists: false,
|
||||
name: "doesn't exist",
|
||||
},
|
||||
{
|
||||
rt: network.RouteTable{},
|
||||
exists: true,
|
||||
name: "nil routes",
|
||||
},
|
||||
{
|
||||
rt: network.RouteTable{
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{},
|
||||
},
|
||||
exists: true,
|
||||
name: "no routes",
|
||||
},
|
||||
{
|
||||
rt: network.RouteTable{
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("name"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/16"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
exists: true,
|
||||
expectedRoute: []cloudprovider.Route{
|
||||
{
|
||||
Name: "name",
|
||||
TargetNode: mapRouteNameToNodeName(false, "name"),
|
||||
DestinationCIDR: "1.2.3.4/16",
|
||||
},
|
||||
},
|
||||
name: "one route",
|
||||
},
|
||||
{
|
||||
rt: network.RouteTable{
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("name"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/16"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: pointer.String("name2"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("5.6.7.8/16"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
exists: true,
|
||||
expectedRoute: []cloudprovider.Route{
|
||||
{
|
||||
Name: "name",
|
||||
TargetNode: mapRouteNameToNodeName(false, "name"),
|
||||
DestinationCIDR: "1.2.3.4/16",
|
||||
},
|
||||
{
|
||||
Name: "name2",
|
||||
TargetNode: mapRouteNameToNodeName(false, "name2"),
|
||||
DestinationCIDR: "5.6.7.8/16",
|
||||
},
|
||||
},
|
||||
name: "more routes",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
routes, err := processRoutes(false, test.rt, test.exists, test.err)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("%s: unexpected non-error", test.name)
|
||||
continue
|
||||
}
|
||||
if err.Error() != test.expectedError {
|
||||
t.Errorf("%s: Expected error: %v, saw error: %v", test.name, test.expectedError, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("%s; unexpected error: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
if len(routes) != len(test.expectedRoute) {
|
||||
t.Errorf("%s: Unexpected difference: %#v vs %#v", test.name, routes, test.expectedRoute)
|
||||
continue
|
||||
}
|
||||
for ix := range test.expectedRoute {
|
||||
if !reflect.DeepEqual(test.expectedRoute[ix], *routes[ix]) {
|
||||
t.Errorf("%s: Unexpected difference: %#v vs %#v", test.name, test.expectedRoute[ix], *routes[ix])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindFirstIPByFamily(t *testing.T) {
|
||||
firstIPv4 := "10.0.0.1"
|
||||
firstIPv6 := "2001:1234:5678:9abc::9"
|
||||
testIPs := []string{
|
||||
firstIPv4,
|
||||
"11.0.0.1",
|
||||
firstIPv6,
|
||||
"fda4:6dee:effc:62a0:0:0:0:0",
|
||||
}
|
||||
testCases := []struct {
|
||||
ipv6 bool
|
||||
ips []string
|
||||
expectedIP string
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
ipv6: true,
|
||||
ips: testIPs,
|
||||
expectedIP: firstIPv6,
|
||||
},
|
||||
{
|
||||
ipv6: false,
|
||||
ips: testIPs,
|
||||
expectedIP: firstIPv4,
|
||||
},
|
||||
{
|
||||
ipv6: true,
|
||||
ips: []string{"10.0.0.1"},
|
||||
expectedErrMsg: fmt.Errorf("no match found matching the ipfamily requested"),
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
ip, err := findFirstIPByFamily(test.ips, test.ipv6)
|
||||
assert.Equal(t, test.expectedErrMsg, err)
|
||||
assert.Equal(t, test.expectedIP, ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteNameFuncs(t *testing.T) {
|
||||
v4CIDR := "10.0.0.1/16"
|
||||
v6CIDR := "fd3e:5f02:6ec0:30ba::/64"
|
||||
nodeName := "thisNode"
|
||||
testCases := []struct {
|
||||
ipv6DualStackEnabled bool
|
||||
}{
|
||||
{
|
||||
ipv6DualStackEnabled: true,
|
||||
},
|
||||
{
|
||||
ipv6DualStackEnabled: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
routeName := mapNodeNameToRouteName(test.ipv6DualStackEnabled, types.NodeName(nodeName), v4CIDR)
|
||||
outNodeName := mapRouteNameToNodeName(test.ipv6DualStackEnabled, routeName)
|
||||
assert.Equal(t, string(outNodeName), nodeName)
|
||||
|
||||
routeName = mapNodeNameToRouteName(test.ipv6DualStackEnabled, types.NodeName(nodeName), v6CIDR)
|
||||
outNodeName = mapRouteNameToNodeName(test.ipv6DualStackEnabled, routeName)
|
||||
assert.Equal(t, string(outNodeName), nodeName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoutes(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
|
||||
mockVMSet := mockvmsets.NewMockVMSet(ctrl)
|
||||
|
||||
cloud := &Cloud{
|
||||
RouteTablesClient: routeTableClient,
|
||||
VMSet: mockVMSet,
|
||||
Config: Config{
|
||||
RouteTableResourceGroup: "foo",
|
||||
RouteTableName: "bar",
|
||||
Location: "location",
|
||||
},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
nodeInformerSynced: func() bool { return true },
|
||||
}
|
||||
cache, _ := cloud.newRouteTableCache()
|
||||
cloud.rtCache = cache
|
||||
cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
|
||||
go cloud.routeUpdater.run()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
routeTableName string
|
||||
routeTable network.RouteTable
|
||||
hasUnmanagedNodes bool
|
||||
nodeInformerNotSynced bool
|
||||
unmanagedNodeName string
|
||||
routeCIDRs map[string]string
|
||||
expectedRoutes []*cloudprovider.Route
|
||||
getErr *retry.Error
|
||||
expectedErrMsg error
|
||||
}{
|
||||
{
|
||||
name: "ListRoutes should return correct routes",
|
||||
routeTableName: "rt1",
|
||||
routeTable: network.RouteTable{
|
||||
Name: pointer.String("rt1"),
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("node"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRoutes: []*cloudprovider.Route{
|
||||
{
|
||||
Name: "node",
|
||||
TargetNode: mapRouteNameToNodeName(false, "node"),
|
||||
DestinationCIDR: "1.2.3.4/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ListRoutes should return correct routes if there's unmanaged nodes",
|
||||
routeTableName: "rt2",
|
||||
hasUnmanagedNodes: true,
|
||||
unmanagedNodeName: "unmanaged-node",
|
||||
routeCIDRs: map[string]string{"unmanaged-node": "2.2.3.4/24"},
|
||||
routeTable: network.RouteTable{
|
||||
Name: pointer.String("rt2"),
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: pointer.String("node"),
|
||||
RoutePropertiesFormat: &network.RoutePropertiesFormat{
|
||||
AddressPrefix: pointer.String("1.2.3.4/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRoutes: []*cloudprovider.Route{
|
||||
{
|
||||
Name: "node",
|
||||
TargetNode: mapRouteNameToNodeName(false, "node"),
|
||||
DestinationCIDR: "1.2.3.4/24",
|
||||
},
|
||||
{
|
||||
Name: "unmanaged-node",
|
||||
TargetNode: mapRouteNameToNodeName(false, "unmanaged-node"),
|
||||
DestinationCIDR: "2.2.3.4/24",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ListRoutes should return nil if routeTable don't exist",
|
||||
routeTableName: "rt3",
|
||||
routeTable: network.RouteTable{},
|
||||
getErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
RawError: cloudprovider.InstanceNotFound,
|
||||
},
|
||||
expectedRoutes: []*cloudprovider.Route{},
|
||||
},
|
||||
{
|
||||
name: "ListRoutes should report error if error occurs when invoke routeTableClient.Get",
|
||||
routeTableName: "rt4",
|
||||
routeTable: network.RouteTable{},
|
||||
getErr: &retry.Error{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
RawError: fmt.Errorf("Get error"),
|
||||
},
|
||||
expectedErrMsg: fmt.Errorf("Retriable: false, RetryAfter: 0s, HTTPStatusCode: 500, RawError: %w", fmt.Errorf("Get error")),
|
||||
},
|
||||
{
|
||||
name: "ListRoutes should report error if node informer is not synced",
|
||||
routeTableName: "rt5",
|
||||
nodeInformerNotSynced: true,
|
||||
routeTable: network.RouteTable{},
|
||||
expectedErrMsg: fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
if test.hasUnmanagedNodes {
|
||||
cloud.unmanagedNodes.Insert(test.unmanagedNodeName)
|
||||
cloud.routeCIDRs = test.routeCIDRs
|
||||
} else {
|
||||
cloud.unmanagedNodes = sets.NewString()
|
||||
cloud.routeCIDRs = nil
|
||||
}
|
||||
|
||||
if test.nodeInformerNotSynced {
|
||||
cloud.nodeInformerSynced = func() bool { return false }
|
||||
} else {
|
||||
cloud.nodeInformerSynced = func() bool { return true }
|
||||
}
|
||||
|
||||
cloud.RouteTableName = test.routeTableName
|
||||
routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, test.routeTableName, "").Return(test.routeTable, test.getErr)
|
||||
|
||||
routes, err := cloud.ListRoutes(context.TODO(), "cluster")
|
||||
assert.Equal(t, test.expectedRoutes, routes, test.name)
|
||||
assert.Equal(t, test.expectedErrMsg, err, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupOutdatedRoutes(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
for _, testCase := range []struct {
|
||||
description string
|
||||
existingRoutes, expectedRoutes []network.Route
|
||||
existingNodeNames sets.String
|
||||
expectedChanged, enableIPV6DualStack bool
|
||||
}{
|
||||
{
|
||||
description: "cleanupOutdatedRoutes should delete outdated non-dualstack routes when dualstack is enabled",
|
||||
existingRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
expectedRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
},
|
||||
existingNodeNames: sets.NewString("aks-node1-vmss000000"),
|
||||
enableIPV6DualStack: true,
|
||||
expectedChanged: true,
|
||||
},
|
||||
{
|
||||
description: "cleanupOutdatedRoutes should delete outdated dualstack routes when dualstack is disabled",
|
||||
existingRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
expectedRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
existingNodeNames: sets.NewString("aks-node1-vmss000000"),
|
||||
expectedChanged: true,
|
||||
},
|
||||
{
|
||||
description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is enabled",
|
||||
existingRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
expectedRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
existingNodeNames: sets.NewString("aks-node1-vmss000001"),
|
||||
enableIPV6DualStack: true,
|
||||
},
|
||||
{
|
||||
description: "cleanupOutdatedRoutes should not delete unmanaged routes when dualstack is disabled",
|
||||
existingRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
expectedRoutes: []network.Route{
|
||||
{Name: pointer.String("aks-node1-vmss000000____xxx")},
|
||||
{Name: pointer.String("aks-node1-vmss000000")},
|
||||
},
|
||||
existingNodeNames: sets.NewString("aks-node1-vmss000001"),
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
cloud := &Cloud{
|
||||
ipv6DualStackEnabled: testCase.enableIPV6DualStack,
|
||||
nodeNames: testCase.existingNodeNames,
|
||||
}
|
||||
|
||||
d := &delayedRouteUpdater{
|
||||
az: cloud,
|
||||
}
|
||||
|
||||
routes, changed := d.cleanupOutdatedRoutes(testCase.existingRoutes)
|
||||
assert.Equal(t, testCase.expectedChanged, changed)
|
||||
assert.Equal(t, testCase.expectedRoutes, routes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRouteDualStack(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
routeTableClient := mockroutetableclient.NewMockInterface(ctrl)
|
||||
|
||||
cloud := &Cloud{
|
||||
RouteTablesClient: routeTableClient,
|
||||
Config: Config{
|
||||
RouteTableResourceGroup: "foo",
|
||||
RouteTableName: "bar",
|
||||
Location: "location",
|
||||
},
|
||||
unmanagedNodes: sets.NewString(),
|
||||
nodeInformerSynced: func() bool { return true },
|
||||
ipv6DualStackEnabled: true,
|
||||
}
|
||||
cache, _ := cloud.newRouteTableCache()
|
||||
cloud.rtCache = cache
|
||||
cloud.routeUpdater = newDelayedRouteUpdater(cloud, 100*time.Millisecond)
|
||||
go cloud.routeUpdater.run()
|
||||
|
||||
route := cloudprovider.Route{
|
||||
TargetNode: "node",
|
||||
DestinationCIDR: "1.2.3.4/24",
|
||||
}
|
||||
routeName := mapNodeNameToRouteName(true, route.TargetNode, route.DestinationCIDR)
|
||||
routeNameIPV4 := mapNodeNameToRouteName(false, route.TargetNode, route.DestinationCIDR)
|
||||
routeTables := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: &routeName,
|
||||
},
|
||||
{
|
||||
Name: &routeNameIPV4,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
routeTablesAfterFirstDeletion := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{
|
||||
{
|
||||
Name: &routeNameIPV4,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
routeTablesAfterSecondDeletion := network.RouteTable{
|
||||
Name: &cloud.RouteTableName,
|
||||
Location: &cloud.Location,
|
||||
RouteTablePropertiesFormat: &network.RouteTablePropertiesFormat{
|
||||
Routes: &[]network.Route{},
|
||||
},
|
||||
}
|
||||
routeTableClient.EXPECT().Get(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, "").Return(routeTables, nil).AnyTimes()
|
||||
routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterFirstDeletion, "").Return(nil)
|
||||
routeTableClient.EXPECT().CreateOrUpdate(gomock.Any(), cloud.RouteTableResourceGroup, cloud.RouteTableName, routeTablesAfterSecondDeletion, "").Return(nil)
|
||||
err := cloud.DeleteRoute(context.TODO(), "cluster", &route)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error deleting route: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/fileclient"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultStorageAccountType = string(storage.StandardLRS)
|
||||
defaultStorageAccountKind = storage.StorageV2
|
||||
fileShareAccountNamePrefix = "f"
|
||||
sharedDiskAccountNamePrefix = "ds"
|
||||
dedicatedDiskAccountNamePrefix = "dd"
|
||||
)
|
||||
|
||||
// CreateFileShare creates a file share, using a matching storage account type, account kind, etc.
|
||||
// storage account will be created if specified account is not found
|
||||
func (az *Cloud) CreateFileShare(accountOptions *AccountOptions, shareOptions *fileclient.ShareOptions) (string, string, error) {
|
||||
if accountOptions == nil {
|
||||
return "", "", fmt.Errorf("account options is nil")
|
||||
}
|
||||
if shareOptions == nil {
|
||||
return "", "", fmt.Errorf("share options is nil")
|
||||
}
|
||||
if accountOptions.ResourceGroup == "" {
|
||||
accountOptions.ResourceGroup = az.resourceGroup
|
||||
}
|
||||
|
||||
accountOptions.EnableHTTPSTrafficOnly = true
|
||||
if shareOptions.Protocol == storage.NFS {
|
||||
accountOptions.EnableHTTPSTrafficOnly = false
|
||||
}
|
||||
|
||||
accountName, accountKey, err := az.EnsureStorageAccount(accountOptions, fileShareAccountNamePrefix)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("could not get storage key for storage account %s: %v", accountOptions.Name, err)
|
||||
}
|
||||
|
||||
if err := az.createFileShare(accountOptions.ResourceGroup, accountName, shareOptions); err != nil {
|
||||
return "", "", fmt.Errorf("failed to create share %s in account %s: %v", shareOptions.Name, accountName, err)
|
||||
}
|
||||
klog.V(4).Infof("created share %s in account %s", shareOptions.Name, accountOptions.Name)
|
||||
return accountName, accountKey, nil
|
||||
}
|
||||
|
||||
// DeleteFileShare deletes a file share using storage account name and key
|
||||
func (az *Cloud) DeleteFileShare(resourceGroup, accountName, shareName string) error {
|
||||
if err := az.deleteFileShare(resourceGroup, accountName, shareName); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.V(4).Infof("share %s deleted", shareName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResizeFileShare resizes a file share
|
||||
func (az *Cloud) ResizeFileShare(resourceGroup, accountName, name string, sizeGiB int) error {
|
||||
return az.resizeFileShare(resourceGroup, accountName, name, sizeGiB)
|
||||
}
|
||||
|
||||
// GetFileShare gets a file share
|
||||
func (az *Cloud) GetFileShare(resourceGroupName, accountName, name string) (storage.FileShare, error) {
|
||||
return az.getFileShare(resourceGroupName, accountName, name)
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/fileclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/fileclient/mockfileclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient"
|
||||
)
|
||||
|
||||
func TestCreateFileShare(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{controllerCommon: &controllerCommon{resourceGroup: "rg"}}
|
||||
name := "baz"
|
||||
sku := "sku"
|
||||
kind := "StorageV2"
|
||||
location := "centralus"
|
||||
value := "foo key"
|
||||
bogus := "bogus"
|
||||
|
||||
tests := []struct {
|
||||
rg string
|
||||
name string
|
||||
acct string
|
||||
acctType string
|
||||
acctKind string
|
||||
loc string
|
||||
gb int
|
||||
accounts []storage.Account
|
||||
keys storage.AccountListKeysResult
|
||||
err error
|
||||
|
||||
expectErr bool
|
||||
expectAcct string
|
||||
expectKey string
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
acct: "bar",
|
||||
acctType: "type",
|
||||
acctKind: "StorageV2",
|
||||
loc: "eastus",
|
||||
gb: 10,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
acct: "",
|
||||
acctType: "type",
|
||||
acctKind: "StorageV2",
|
||||
loc: "eastus",
|
||||
gb: 10,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
acct: "",
|
||||
acctType: sku,
|
||||
acctKind: kind,
|
||||
loc: location,
|
||||
gb: 10,
|
||||
accounts: []storage.Account{
|
||||
{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Kind: storage.Kind(kind), Location: &location},
|
||||
},
|
||||
keys: storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{Value: &value},
|
||||
},
|
||||
},
|
||||
expectAcct: "baz",
|
||||
expectKey: "key",
|
||||
},
|
||||
{
|
||||
rg: "rg",
|
||||
name: "foo",
|
||||
acct: "",
|
||||
acctType: sku,
|
||||
acctKind: kind,
|
||||
loc: location,
|
||||
gb: 10,
|
||||
accounts: []storage.Account{
|
||||
{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Kind: storage.Kind(kind), Location: &location},
|
||||
},
|
||||
keys: storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{Value: &value},
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf("create fileshare error"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
acct: "",
|
||||
acctType: sku,
|
||||
acctKind: kind,
|
||||
loc: location,
|
||||
gb: 10,
|
||||
accounts: []storage.Account{
|
||||
{Name: &bogus, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Location: &location},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "foo",
|
||||
acct: "",
|
||||
acctType: sku,
|
||||
acctKind: kind,
|
||||
loc: location,
|
||||
gb: 10,
|
||||
accounts: []storage.Account{
|
||||
{Name: &name, Sku: &storage.Sku{Name: storage.SkuName(sku)}, Location: &bogus},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockFileClient := mockfileclient.NewMockInterface(ctrl)
|
||||
cloud.FileClient = mockFileClient
|
||||
mockFileClient.EXPECT().CreateFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.err).AnyTimes()
|
||||
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
mockStorageAccountsClient.EXPECT().ListKeys(gomock.Any(), "rg", gomock.Any()).Return(test.keys, nil).AnyTimes()
|
||||
mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(test.accounts, nil).AnyTimes()
|
||||
mockStorageAccountsClient.EXPECT().Create(gomock.Any(), "rg", gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
|
||||
mockAccount := &AccountOptions{
|
||||
Name: test.acct,
|
||||
Type: test.acctType,
|
||||
Kind: test.acctKind,
|
||||
ResourceGroup: test.rg,
|
||||
Location: test.loc,
|
||||
}
|
||||
|
||||
mockFileShare := &fileclient.ShareOptions{
|
||||
Name: test.name,
|
||||
Protocol: storage.SMB,
|
||||
RequestGiB: test.gb,
|
||||
}
|
||||
|
||||
account, key, err := cloud.CreateFileShare(mockAccount, mockFileShare)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
if test.expectAcct != account {
|
||||
t.Errorf("Expected: %s, got %s", test.expectAcct, account)
|
||||
}
|
||||
if test.expectKey != key {
|
||||
t.Errorf("Expected: %s, got %s", test.expectKey, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFileShare(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
tests := []struct {
|
||||
rg string
|
||||
acct string
|
||||
name string
|
||||
|
||||
err error
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
rg: "rg",
|
||||
acct: "bar",
|
||||
name: "foo",
|
||||
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
rg: "rg",
|
||||
acct: "bar",
|
||||
name: "",
|
||||
|
||||
err: fmt.Errorf("delete fileshare error"),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockFileClient := mockfileclient.NewMockInterface(ctrl)
|
||||
cloud.FileClient = mockFileClient
|
||||
mockFileClient.EXPECT().DeleteFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(test.err).Times(1)
|
||||
|
||||
err := cloud.DeleteFileShare(test.rg, test.acct, test.name)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResizeFileShare(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
mockFileClient := mockfileclient.NewMockInterface(ctrl)
|
||||
mockFileClient.EXPECT().ResizeFileShare(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
cloud.FileClient = mockFileClient
|
||||
|
||||
tests := []struct {
|
||||
rg string
|
||||
acct string
|
||||
name string
|
||||
gb int
|
||||
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
rg: "rg",
|
||||
acct: "bar",
|
||||
name: "foo",
|
||||
gb: 10,
|
||||
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
|
||||
err := cloud.ResizeFileShare(test.rg, test.acct, test.name, test.gb)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFileShare(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
mockFileClient := mockfileclient.NewMockInterface(ctrl)
|
||||
mockFileClient.EXPECT().GetFileShare(gomock.Any(), gomock.Any(), gomock.Any()).Return(storage.FileShare{}, nil).AnyTimes()
|
||||
cloud.FileClient = mockFileClient
|
||||
|
||||
tests := []struct {
|
||||
rg string
|
||||
acct string
|
||||
name string
|
||||
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
rg: "rg",
|
||||
acct: "bar",
|
||||
name: "foo",
|
||||
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
|
||||
_, err := cloud.GetFileShare(test.rg, test.acct, test.name)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// AccountOptions contains the fields which are used to create storage account.
|
||||
type AccountOptions struct {
|
||||
Name, Type, Kind, ResourceGroup, Location string
|
||||
EnableHTTPSTrafficOnly bool
|
||||
Tags map[string]string
|
||||
VirtualNetworkResourceIDs []string
|
||||
}
|
||||
|
||||
type accountWithLocation struct {
|
||||
Name, StorageType, Location string
|
||||
}
|
||||
|
||||
// getStorageAccounts get matching storage accounts
|
||||
func (az *Cloud) getStorageAccounts(accountOptions *AccountOptions) ([]accountWithLocation, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
result, rerr := az.StorageAccountClient.ListByResourceGroup(ctx, accountOptions.ResourceGroup)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
accounts := []accountWithLocation{}
|
||||
for _, acct := range result {
|
||||
if acct.Name != nil && acct.Location != nil && acct.Sku != nil {
|
||||
storageType := string((*acct.Sku).Name)
|
||||
if accountOptions.Type != "" && !strings.EqualFold(accountOptions.Type, storageType) {
|
||||
continue
|
||||
}
|
||||
|
||||
if accountOptions.Kind != "" && !strings.EqualFold(accountOptions.Kind, string(acct.Kind)) {
|
||||
continue
|
||||
}
|
||||
|
||||
location := *acct.Location
|
||||
if accountOptions.Location != "" && !strings.EqualFold(accountOptions.Location, location) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(accountOptions.VirtualNetworkResourceIDs) > 0 {
|
||||
if acct.AccountProperties == nil || acct.AccountProperties.NetworkRuleSet == nil ||
|
||||
acct.AccountProperties.NetworkRuleSet.VirtualNetworkRules == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, subnetID := range accountOptions.VirtualNetworkResourceIDs {
|
||||
for _, rule := range *acct.AccountProperties.NetworkRuleSet.VirtualNetworkRules {
|
||||
if strings.EqualFold(pointer.StringDeref(rule.VirtualNetworkResourceID, ""), subnetID) && rule.Action == storage.Allow {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
accounts = append(accounts, accountWithLocation{Name: *acct.Name, StorageType: storageType, Location: location})
|
||||
}
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetStorageAccesskey gets the storage account access key
|
||||
func (az *Cloud) GetStorageAccesskey(account, resourceGroup string) (string, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
result, rerr := az.StorageAccountClient.ListKeys(ctx, resourceGroup, account)
|
||||
if rerr != nil {
|
||||
return "", rerr.Error()
|
||||
}
|
||||
if result.Keys == nil {
|
||||
return "", fmt.Errorf("empty keys")
|
||||
}
|
||||
|
||||
for _, k := range *result.Keys {
|
||||
if k.Value != nil && *k.Value != "" {
|
||||
v := *k.Value
|
||||
if ind := strings.LastIndex(v, " "); ind >= 0 {
|
||||
v = v[(ind + 1):]
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no valid keys")
|
||||
}
|
||||
|
||||
// EnsureStorageAccount search storage account, create one storage account(with genAccountNamePrefix) if not found, return accountName, accountKey
|
||||
func (az *Cloud) EnsureStorageAccount(accountOptions *AccountOptions, genAccountNamePrefix string) (string, string, error) {
|
||||
if accountOptions == nil {
|
||||
return "", "", fmt.Errorf("account options is nil")
|
||||
}
|
||||
accountName := accountOptions.Name
|
||||
accountType := accountOptions.Type
|
||||
accountKind := accountOptions.Kind
|
||||
resourceGroup := accountOptions.ResourceGroup
|
||||
location := accountOptions.Location
|
||||
enableHTTPSTrafficOnly := accountOptions.EnableHTTPSTrafficOnly
|
||||
|
||||
if len(accountName) == 0 {
|
||||
// find a storage account that matches accountType
|
||||
accounts, err := az.getStorageAccounts(accountOptions)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("could not list storage accounts for account type %s: %v", accountType, err)
|
||||
}
|
||||
|
||||
if len(accounts) > 0 {
|
||||
accountName = accounts[0].Name
|
||||
klog.V(4).Infof("found a matching account %s type %s location %s", accounts[0].Name, accounts[0].StorageType, accounts[0].Location)
|
||||
}
|
||||
|
||||
if len(accountName) == 0 {
|
||||
// set network rules for storage account
|
||||
var networkRuleSet *storage.NetworkRuleSet
|
||||
virtualNetworkRules := []storage.VirtualNetworkRule{}
|
||||
for _, subnetID := range accountOptions.VirtualNetworkResourceIDs {
|
||||
vnetRule := storage.VirtualNetworkRule{
|
||||
VirtualNetworkResourceID: &subnetID,
|
||||
Action: storage.Allow,
|
||||
}
|
||||
virtualNetworkRules = append(virtualNetworkRules, vnetRule)
|
||||
klog.V(4).Infof("subnetID(%s) has been set", subnetID)
|
||||
}
|
||||
if len(virtualNetworkRules) > 0 {
|
||||
networkRuleSet = &storage.NetworkRuleSet{
|
||||
VirtualNetworkRules: &virtualNetworkRules,
|
||||
DefaultAction: storage.DefaultActionDeny,
|
||||
}
|
||||
}
|
||||
|
||||
// not found a matching account, now create a new account in current resource group
|
||||
accountName = generateStorageAccountName(genAccountNamePrefix)
|
||||
if location == "" {
|
||||
location = az.Location
|
||||
}
|
||||
if accountType == "" {
|
||||
accountType = defaultStorageAccountType
|
||||
}
|
||||
|
||||
// use StorageV2 by default per https://docs.microsoft.com/en-us/azure/storage/common/storage-account-options
|
||||
kind := defaultStorageAccountKind
|
||||
if accountKind != "" {
|
||||
kind = storage.Kind(accountKind)
|
||||
}
|
||||
if len(accountOptions.Tags) == 0 {
|
||||
accountOptions.Tags = make(map[string]string)
|
||||
}
|
||||
accountOptions.Tags["created-by"] = "azure"
|
||||
tags := convertMapToMapPointer(accountOptions.Tags)
|
||||
|
||||
klog.V(2).Infof("azure - no matching account found, begin to create a new account %s in resource group %s, location: %s, accountType: %s, accountKind: %s, tags: %+v",
|
||||
accountName, resourceGroup, location, accountType, kind, accountOptions.Tags)
|
||||
|
||||
cp := storage.AccountCreateParameters{
|
||||
Sku: &storage.Sku{Name: storage.SkuName(accountType)},
|
||||
Kind: kind,
|
||||
AccountPropertiesCreateParameters: &storage.AccountPropertiesCreateParameters{
|
||||
EnableHTTPSTrafficOnly: &enableHTTPSTrafficOnly,
|
||||
NetworkRuleSet: networkRuleSet,
|
||||
},
|
||||
Tags: tags,
|
||||
Location: &location}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
rerr := az.StorageAccountClient.Create(ctx, resourceGroup, accountName, cp)
|
||||
if rerr != nil {
|
||||
return "", "", fmt.Errorf("failed to create storage account %s, error: %v", accountName, rerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find the access key with this account
|
||||
accountKey, err := az.GetStorageAccesskey(accountName, resourceGroup)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("could not get storage key for storage account %s: %v", accountName, err)
|
||||
}
|
||||
|
||||
return accountName, accountKey, nil
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/storageaccountclient/mockstorageaccountclient"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func TestGetStorageAccessKeys(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
value := "foo bar"
|
||||
|
||||
tests := []struct {
|
||||
results storage.AccountListKeysResult
|
||||
expectedKey string
|
||||
expectErr bool
|
||||
err error
|
||||
}{
|
||||
{storage.AccountListKeysResult{}, "", true, nil},
|
||||
{
|
||||
storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{Value: &value},
|
||||
},
|
||||
},
|
||||
"bar",
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
storage.AccountListKeysResult{
|
||||
Keys: &[]storage.AccountKey{
|
||||
{},
|
||||
{Value: &value},
|
||||
},
|
||||
},
|
||||
"bar",
|
||||
false,
|
||||
nil,
|
||||
},
|
||||
{storage.AccountListKeysResult{}, "", true, fmt.Errorf("test error")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
mockStorageAccountsClient.EXPECT().ListKeys(gomock.Any(), "rg", gomock.Any()).Return(test.results, nil).AnyTimes()
|
||||
key, err := cloud.GetStorageAccesskey("acct", "rg")
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("Unexpected non-error")
|
||||
continue
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
continue
|
||||
}
|
||||
if key != test.expectedKey {
|
||||
t.Errorf("expected: %s, saw %s", test.expectedKey, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStorageAccount(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
|
||||
name := "testAccount"
|
||||
location := "testLocation"
|
||||
networkID := "networkID"
|
||||
accountProperties := storage.AccountProperties{
|
||||
NetworkRuleSet: &storage.NetworkRuleSet{
|
||||
VirtualNetworkRules: &[]storage.VirtualNetworkRule{
|
||||
{
|
||||
VirtualNetworkResourceID: &networkID,
|
||||
Action: storage.Allow,
|
||||
State: "state",
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
account := storage.Account{
|
||||
Sku: &storage.Sku{
|
||||
Name: "testSku",
|
||||
Tier: "testSkuTier",
|
||||
},
|
||||
Kind: "testKind",
|
||||
Location: &location,
|
||||
Name: &name,
|
||||
AccountProperties: &accountProperties,
|
||||
}
|
||||
|
||||
testResourceGroups := []storage.Account{account}
|
||||
|
||||
accountOptions := &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
VirtualNetworkResourceIDs: []string{networkID},
|
||||
}
|
||||
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
|
||||
mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(testResourceGroups, nil).Times(1)
|
||||
|
||||
accountsWithLocations, err := cloud.getStorageAccounts(accountOptions)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if accountsWithLocations == nil {
|
||||
t.Error("unexpected error as returned accounts are nil")
|
||||
}
|
||||
|
||||
if len(accountsWithLocations) == 0 {
|
||||
t.Error("unexpected error as returned accounts slice is empty")
|
||||
}
|
||||
|
||||
expectedAccountWithLocation := accountWithLocation{
|
||||
Name: "testAccount",
|
||||
StorageType: "testSku",
|
||||
Location: "testLocation",
|
||||
}
|
||||
|
||||
accountWithLocation := accountsWithLocations[0]
|
||||
if accountWithLocation.Name != expectedAccountWithLocation.Name {
|
||||
t.Errorf("expected %s, but was %s", accountWithLocation.Name, expectedAccountWithLocation.Name)
|
||||
}
|
||||
|
||||
if accountWithLocation.StorageType != expectedAccountWithLocation.StorageType {
|
||||
t.Errorf("expected %s, but was %s", accountWithLocation.StorageType, expectedAccountWithLocation.StorageType)
|
||||
}
|
||||
|
||||
if accountWithLocation.Location != expectedAccountWithLocation.Location {
|
||||
t.Errorf("expected %s, but was %s", accountWithLocation.Location, expectedAccountWithLocation.Location)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStorageAccountEdgeCases(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cloud := &Cloud{}
|
||||
|
||||
// default account with name, location, sku, kind
|
||||
name := "testAccount"
|
||||
location := "testLocation"
|
||||
sku := &storage.Sku{
|
||||
Name: "testSku",
|
||||
Tier: "testSkuTier",
|
||||
}
|
||||
account := storage.Account{
|
||||
Sku: sku,
|
||||
Kind: "testKind",
|
||||
Location: &location,
|
||||
Name: &name,
|
||||
}
|
||||
|
||||
accountPropertiesWithoutNetworkRuleSet := storage.AccountProperties{NetworkRuleSet: nil}
|
||||
accountPropertiesWithoutVirtualNetworkRules := storage.AccountProperties{
|
||||
NetworkRuleSet: &storage.NetworkRuleSet{
|
||||
VirtualNetworkRules: nil,
|
||||
}}
|
||||
|
||||
tests := []struct {
|
||||
testCase string
|
||||
testAccountOptions *AccountOptions
|
||||
testResourceGroups []storage.Account
|
||||
expectedResult []accountWithLocation
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testCase: "account name is nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
},
|
||||
testResourceGroups: []storage.Account{},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account location is nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
},
|
||||
testResourceGroups: []storage.Account{{Name: &name}},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account sku is nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
},
|
||||
testResourceGroups: []storage.Account{{Name: &name, Location: &location}},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options type is not empty and not equal account storage type",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
Type: "testAccountOptionsType",
|
||||
},
|
||||
testResourceGroups: []storage.Account{account},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options kind is not empty and not equal account type",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
Kind: "testAccountOptionsKind",
|
||||
},
|
||||
testResourceGroups: []storage.Account{account},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options location is not empty and not equal account location",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
Location: "testAccountOptionsLocation",
|
||||
},
|
||||
testResourceGroups: []storage.Account{account},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options account properties are nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
VirtualNetworkResourceIDs: []string{"id"},
|
||||
},
|
||||
testResourceGroups: []storage.Account{},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options account properties network rule set is nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
VirtualNetworkResourceIDs: []string{"id"},
|
||||
},
|
||||
testResourceGroups: []storage.Account{{Name: &name, Kind: "kind", Location: &location, Sku: sku, AccountProperties: &accountPropertiesWithoutNetworkRuleSet}},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testCase: "account options account properties virtual network rule is nil",
|
||||
testAccountOptions: &AccountOptions{
|
||||
ResourceGroup: "rg",
|
||||
VirtualNetworkResourceIDs: []string{"id"},
|
||||
},
|
||||
testResourceGroups: []storage.Account{{Name: &name, Kind: "kind", Location: &location, Sku: sku, AccountProperties: &accountPropertiesWithoutVirtualNetworkRules}},
|
||||
expectedResult: []accountWithLocation{},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Logf("running test case: %s", test.testCase)
|
||||
mockStorageAccountsClient := mockstorageaccountclient.NewMockInterface(ctrl)
|
||||
cloud.StorageAccountClient = mockStorageAccountsClient
|
||||
|
||||
mockStorageAccountsClient.EXPECT().ListByResourceGroup(gomock.Any(), "rg").Return(test.testResourceGroups, nil).AnyTimes()
|
||||
|
||||
accountsWithLocations, err := cloud.getStorageAccounts(test.testAccountOptions)
|
||||
if err != test.expectedError {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(accountsWithLocations) != len(test.expectedResult) {
|
||||
t.Error("unexpected error as returned accounts slice is not empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const (
|
||||
tagsDelimiter = ","
|
||||
tagKeyValueDelimiter = "="
|
||||
)
|
||||
|
||||
// lockMap used to lock on entries
|
||||
type lockMap struct {
|
||||
sync.Mutex
|
||||
mutexMap map[string]*sync.Mutex
|
||||
}
|
||||
|
||||
// NewLockMap returns a new lock map
|
||||
func newLockMap() *lockMap {
|
||||
return &lockMap{
|
||||
mutexMap: make(map[string]*sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// LockEntry acquires a lock associated with the specific entry
|
||||
func (lm *lockMap) LockEntry(entry string) {
|
||||
lm.Lock()
|
||||
// check if entry does not exists, then add entry
|
||||
if _, exists := lm.mutexMap[entry]; !exists {
|
||||
lm.addEntry(entry)
|
||||
}
|
||||
|
||||
lm.Unlock()
|
||||
lm.lockEntry(entry)
|
||||
}
|
||||
|
||||
// UnlockEntry release the lock associated with the specific entry
|
||||
func (lm *lockMap) UnlockEntry(entry string) {
|
||||
lm.Lock()
|
||||
defer lm.Unlock()
|
||||
|
||||
if _, exists := lm.mutexMap[entry]; !exists {
|
||||
return
|
||||
}
|
||||
lm.unlockEntry(entry)
|
||||
}
|
||||
|
||||
func (lm *lockMap) addEntry(entry string) {
|
||||
lm.mutexMap[entry] = &sync.Mutex{}
|
||||
}
|
||||
|
||||
func (lm *lockMap) lockEntry(entry string) {
|
||||
lm.mutexMap[entry].Lock()
|
||||
}
|
||||
|
||||
func (lm *lockMap) unlockEntry(entry string) {
|
||||
lm.mutexMap[entry].Unlock()
|
||||
}
|
||||
|
||||
func getContextWithCancel() (context.Context, context.CancelFunc) {
|
||||
return context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
// ConvertTagsToMap convert the tags from string to map
|
||||
// the valid tags format is "key1=value1,key2=value2", which could be converted to
|
||||
// {"key1": "value1", "key2": "value2"}
|
||||
func ConvertTagsToMap(tags string) (map[string]string, error) {
|
||||
m := make(map[string]string)
|
||||
if tags == "" {
|
||||
return m, nil
|
||||
}
|
||||
s := strings.Split(tags, tagsDelimiter)
|
||||
for _, tag := range s {
|
||||
kv := strings.Split(tag, tagKeyValueDelimiter)
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("Tags '%s' are invalid, the format should like: 'key1=value1,key2=value2'", tags)
|
||||
}
|
||||
key := strings.TrimSpace(kv[0])
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("Tags '%s' are invalid, the format should like: 'key1=value1,key2=value2'", tags)
|
||||
}
|
||||
value := strings.TrimSpace(kv[1])
|
||||
m[key] = value
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func convertMapToMapPointer(origin map[string]string) map[string]*string {
|
||||
newly := make(map[string]*string)
|
||||
for k, v := range origin {
|
||||
value := v
|
||||
newly[k] = &value
|
||||
}
|
||||
return newly
|
||||
}
|
||||
|
||||
func parseTags(tags string) map[string]*string {
|
||||
kvs := strings.Split(tags, ",")
|
||||
formatted := make(map[string]*string)
|
||||
for _, kv := range kvs {
|
||||
res := strings.Split(kv, "=")
|
||||
if len(res) != 2 {
|
||||
klog.Warningf("parseTags: error when parsing key-value pair %s, would ignore this one", kv)
|
||||
continue
|
||||
}
|
||||
k, v := strings.TrimSpace(res[0]), strings.TrimSpace(res[1])
|
||||
if k == "" || v == "" {
|
||||
klog.Warningf("parseTags: error when parsing key-value pair %s-%s, would ignore this one", k, v)
|
||||
continue
|
||||
}
|
||||
formatted[strings.ToLower(k)] = pointer.String(v)
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
func findKeyInMapCaseInsensitive(targetMap map[string]*string, key string) (bool, string) {
|
||||
for k := range targetMap {
|
||||
if strings.EqualFold(k, key) {
|
||||
return true, k
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func reconcileTags(currentTagsOnResource, newTags map[string]*string) (reconciledTags map[string]*string, changed bool) {
|
||||
for k, v := range newTags {
|
||||
found, key := findKeyInMapCaseInsensitive(currentTagsOnResource, k)
|
||||
if !found {
|
||||
currentTagsOnResource[k] = v
|
||||
changed = true
|
||||
} else if !strings.EqualFold(pointer.StringDeref(v, ""), pointer.StringDeref(currentTagsOnResource[key], "")) {
|
||||
currentTagsOnResource[key] = v
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
return currentTagsOnResource, changed
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestSimpleLockEntry(t *testing.T) {
|
||||
testLockMap := newLockMap()
|
||||
|
||||
callbackChan1 := make(chan interface{})
|
||||
go testLockMap.lockAndCallback(t, "entry1", callbackChan1)
|
||||
ensureCallbackHappens(t, callbackChan1)
|
||||
}
|
||||
|
||||
func TestSimpleLockUnlockEntry(t *testing.T) {
|
||||
testLockMap := newLockMap()
|
||||
|
||||
callbackChan1 := make(chan interface{})
|
||||
go testLockMap.lockAndCallback(t, "entry1", callbackChan1)
|
||||
ensureCallbackHappens(t, callbackChan1)
|
||||
testLockMap.UnlockEntry("entry1")
|
||||
}
|
||||
|
||||
func TestConcurrentLockEntry(t *testing.T) {
|
||||
testLockMap := newLockMap()
|
||||
|
||||
callbackChan1 := make(chan interface{})
|
||||
callbackChan2 := make(chan interface{})
|
||||
|
||||
go testLockMap.lockAndCallback(t, "entry1", callbackChan1)
|
||||
ensureCallbackHappens(t, callbackChan1)
|
||||
|
||||
go testLockMap.lockAndCallback(t, "entry1", callbackChan2)
|
||||
ensureNoCallback(t, callbackChan2)
|
||||
|
||||
testLockMap.UnlockEntry("entry1")
|
||||
ensureCallbackHappens(t, callbackChan2)
|
||||
testLockMap.UnlockEntry("entry1")
|
||||
}
|
||||
|
||||
func (lm *lockMap) lockAndCallback(t *testing.T, entry string, callbackChan chan<- interface{}) {
|
||||
lm.LockEntry(entry)
|
||||
callbackChan <- true
|
||||
}
|
||||
|
||||
var callbackTimeout = 2 * time.Second
|
||||
|
||||
func ensureCallbackHappens(t *testing.T, callbackChan <-chan interface{}) bool {
|
||||
select {
|
||||
case <-callbackChan:
|
||||
return true
|
||||
case <-time.After(callbackTimeout):
|
||||
t.Fatalf("timed out waiting for callback")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func ensureNoCallback(t *testing.T, callbackChan <-chan interface{}) bool {
|
||||
select {
|
||||
case <-callbackChan:
|
||||
t.Fatalf("unexpected callback")
|
||||
return false
|
||||
case <-time.After(callbackTimeout):
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTagsToMap(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
tags string
|
||||
expectedOutput map[string]string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "should return empty map when tag is empty",
|
||||
tags: "",
|
||||
expectedOutput: map[string]string{},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "sing valid tag should be converted",
|
||||
tags: "key=value",
|
||||
expectedOutput: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "multiple valid tags should be converted",
|
||||
tags: "key1=value1,key2=value2",
|
||||
expectedOutput: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "whitespaces should be trimmed",
|
||||
tags: "key1=value1, key2=value2",
|
||||
expectedOutput: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
desc: "should return error for invalid format",
|
||||
tags: "foo,bar",
|
||||
expectedOutput: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "should return error for when key is missed",
|
||||
tags: "key1=value1,=bar",
|
||||
expectedOutput: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range testCases {
|
||||
m, err := ConvertTagsToMap(c.tags)
|
||||
if c.expectedError {
|
||||
assert.NotNil(t, err, "TestCase[%d]: %s", i, c.desc)
|
||||
} else {
|
||||
assert.Nil(t, err, "TestCase[%d]: %s", i, c.desc)
|
||||
if !reflect.DeepEqual(m, c.expectedOutput) {
|
||||
t.Errorf("got: %v, expected: %v, desc: %v", m, c.expectedOutput, c.desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileTags(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
description string
|
||||
currentTagsOnResource, newTags, expectedTags map[string]*string
|
||||
expectedChanged bool
|
||||
}{
|
||||
{
|
||||
description: "reconcileTags should add missing tags and update existing tags",
|
||||
currentTagsOnResource: map[string]*string{
|
||||
"a": pointer.String("b"),
|
||||
},
|
||||
newTags: map[string]*string{
|
||||
"a": pointer.String("c"),
|
||||
"b": pointer.String("d"),
|
||||
},
|
||||
expectedTags: map[string]*string{
|
||||
"a": pointer.String("c"),
|
||||
"b": pointer.String("d"),
|
||||
},
|
||||
expectedChanged: true,
|
||||
},
|
||||
{
|
||||
description: "reconcileTags should ignore the case of keys when comparing",
|
||||
currentTagsOnResource: map[string]*string{
|
||||
"A": pointer.String("b"),
|
||||
"c": pointer.String("d"),
|
||||
},
|
||||
newTags: map[string]*string{
|
||||
"a": pointer.String("b"),
|
||||
"C": pointer.String("d"),
|
||||
},
|
||||
expectedTags: map[string]*string{
|
||||
"A": pointer.String("b"),
|
||||
"c": pointer.String("d"),
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "reconcileTags should ignore the case of values when comparing",
|
||||
currentTagsOnResource: map[string]*string{
|
||||
"A": pointer.String("b"),
|
||||
"c": pointer.String("d"),
|
||||
},
|
||||
newTags: map[string]*string{
|
||||
"a": pointer.String("B"),
|
||||
"C": pointer.String("D"),
|
||||
},
|
||||
expectedTags: map[string]*string{
|
||||
"A": pointer.String("b"),
|
||||
"c": pointer.String("d"),
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
tags, changed := reconcileTags(testCase.currentTagsOnResource, testCase.newTags)
|
||||
assert.Equal(t, testCase.expectedChanged, changed)
|
||||
assert.Equal(t, testCase.expectedTags, tags)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=azure_vmsets.go -destination=mockvmsets/azure_mock_vmsets.go -package=mockvmsets VMSet
|
||||
package azure
|
||||
|
||||
import (
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
)
|
||||
|
||||
// VMSet defines functions all vmsets (including scale set and availability
|
||||
// set) should be implemented.
|
||||
|
||||
type VMSet interface {
|
||||
// GetInstanceIDByNodeName gets the cloud provider ID by node name.
|
||||
// It must return ("", cloudprovider.InstanceNotFound) if the instance does
|
||||
// not exist or is no longer running.
|
||||
GetInstanceIDByNodeName(name string) (string, error)
|
||||
// GetInstanceTypeByNodeName gets the instance type by node name.
|
||||
GetInstanceTypeByNodeName(name string) (string, error)
|
||||
// GetIPByNodeName gets machine private IP and public IP by node name.
|
||||
GetIPByNodeName(name string) (string, string, error)
|
||||
// GetPrimaryInterface gets machine primary network interface by node name.
|
||||
GetPrimaryInterface(nodeName string) (network.Interface, error)
|
||||
// GetNodeNameByProviderID gets the node name by provider ID.
|
||||
GetNodeNameByProviderID(providerID string) (types.NodeName, error)
|
||||
|
||||
// GetZoneByNodeName gets cloudprovider.Zone by node name.
|
||||
GetZoneByNodeName(name string) (cloudprovider.Zone, error)
|
||||
|
||||
// GetPrimaryVMSetName returns the VM set name depending on the configured vmType.
|
||||
// It returns config.PrimaryScaleSetName for vmss and config.PrimaryAvailabilitySetName for standard vmType.
|
||||
GetPrimaryVMSetName() string
|
||||
// GetVMSetNames selects all possible availability sets or scale sets
|
||||
// (depending vmType configured) for service load balancer, if the service has
|
||||
// no loadbalancer mode annotation returns the primary VMSet. If service annotation
|
||||
// for loadbalancer exists then return the eligible VMSet.
|
||||
GetVMSetNames(service *v1.Service, nodes []*v1.Node) (availabilitySetNames *[]string, err error)
|
||||
// EnsureHostsInPool ensures the given Node's primary IP configurations are
|
||||
// participating in the specified LoadBalancer Backend Pool.
|
||||
EnsureHostsInPool(service *v1.Service, nodes []*v1.Node, backendPoolID string, vmSetName string, isInternal bool) error
|
||||
// EnsureHostInPool ensures the given VM's Primary NIC's Primary IP Configuration is
|
||||
// participating in the specified LoadBalancer Backend Pool.
|
||||
EnsureHostInPool(service *v1.Service, nodeName types.NodeName, backendPoolID string, vmSetName string, isInternal bool) (string, string, string, *compute.VirtualMachineScaleSetVM, error)
|
||||
// EnsureBackendPoolDeleted ensures the loadBalancer backendAddressPools deleted from the specified nodes.
|
||||
EnsureBackendPoolDeleted(service *v1.Service, backendPoolID, vmSetName string, backendAddressPools *[]network.BackendAddressPool, deleteFromVMSet bool) error
|
||||
|
||||
// AttachDisk attaches a vhd to vm. The vhd must exist, can be identified by diskName, diskURI, and lun.
|
||||
AttachDisk(isManagedDisk bool, diskName, diskURI string, nodeName types.NodeName, lun int32, cachingMode compute.CachingTypes, diskEncryptionSetID string, writeAcceleratorEnabled bool) error
|
||||
// DetachDisk detaches a vhd from host. The vhd can be identified by diskName or diskURI.
|
||||
DetachDisk(diskName, diskURI string, nodeName types.NodeName) error
|
||||
// GetDataDisks gets a list of data disks attached to the node.
|
||||
GetDataDisks(nodeName types.NodeName, crt azcache.AzureCacheReadType) ([]compute.DataDisk, error)
|
||||
|
||||
// GetPowerStatusByNodeName returns the power state of the specified node.
|
||||
GetPowerStatusByNodeName(name string) (string, error)
|
||||
|
||||
// GetProvisioningStateByNodeName returns the provisioningState for the specified node.
|
||||
GetProvisioningStateByNodeName(name string) (string, error)
|
||||
|
||||
// GetPrivateIPsByNodeName returns a slice of all private ips assigned to node (ipv6 and ipv4)
|
||||
GetPrivateIPsByNodeName(name string) ([]string, error)
|
||||
|
||||
// GetNodeNameByIPConfigurationID gets the nodeName and vmSetName by IP configuration ID.
|
||||
GetNodeNameByIPConfigurationID(ipConfigurationID string) (string, string, error)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,350 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
vmssNameSeparator = "_"
|
||||
|
||||
vmssKey = "k8svmssKey"
|
||||
availabilitySetNodesKey = "k8sAvailabilitySetNodesKey"
|
||||
|
||||
availabilitySetNodesCacheTTLDefaultInSeconds = 900
|
||||
vmssCacheTTLDefaultInSeconds = 600
|
||||
vmssVirtualMachinesCacheTTLDefaultInSeconds = 600
|
||||
)
|
||||
|
||||
type vmssVirtualMachinesEntry struct {
|
||||
resourceGroup string
|
||||
vmssName string
|
||||
instanceID string
|
||||
virtualMachine *compute.VirtualMachineScaleSetVM
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
type vmssEntry struct {
|
||||
vmss *compute.VirtualMachineScaleSet
|
||||
resourceGroup string
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
type availabilitySetEntry struct {
|
||||
vmNames sets.String
|
||||
nodeNames sets.String
|
||||
}
|
||||
|
||||
func (ss *scaleSet) newVMSSCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
localCache := &sync.Map{} // [vmssName]*vmssEntry
|
||||
|
||||
allResourceGroups, err := ss.GetResourceGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, resourceGroup := range allResourceGroups.List() {
|
||||
allScaleSets, rerr := ss.VirtualMachineScaleSetsClient.List(context.Background(), resourceGroup)
|
||||
if rerr != nil {
|
||||
klog.Errorf("VirtualMachineScaleSetsClient.List failed: %v", rerr)
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
for i := range allScaleSets {
|
||||
scaleSet := allScaleSets[i]
|
||||
if scaleSet.Name == nil || *scaleSet.Name == "" {
|
||||
klog.Warning("failed to get the name of VMSS")
|
||||
continue
|
||||
}
|
||||
localCache.Store(*scaleSet.Name, &vmssEntry{
|
||||
vmss: &scaleSet,
|
||||
resourceGroup: resourceGroup,
|
||||
lastUpdate: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return localCache, nil
|
||||
}
|
||||
|
||||
if ss.Config.VmssCacheTTLInSeconds == 0 {
|
||||
ss.Config.VmssCacheTTLInSeconds = vmssCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(ss.Config.VmssCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func extractVmssVMName(name string) (string, string, error) {
|
||||
split := strings.SplitAfter(name, vmssNameSeparator)
|
||||
if len(split) < 2 {
|
||||
klog.V(3).Infof("Failed to extract vmssVMName %q", name)
|
||||
return "", "", ErrorNotVmssInstance
|
||||
}
|
||||
|
||||
ssName := strings.Join(split[0:len(split)-1], "")
|
||||
// removing the trailing `vmssNameSeparator` since we used SplitAfter
|
||||
ssName = ssName[:len(ssName)-1]
|
||||
instanceID := split[len(split)-1]
|
||||
return ssName, instanceID, nil
|
||||
}
|
||||
|
||||
// getVMSSVMCache returns an *azcache.TimedCache and cache key for a VMSS (creating that cache if new).
|
||||
func (ss *scaleSet) getVMSSVMCache(resourceGroup, vmssName string) (string, *azcache.TimedCache, error) {
|
||||
cacheKey := strings.ToLower(fmt.Sprintf("%s/%s", resourceGroup, vmssName))
|
||||
if entry, ok := ss.vmssVMCache.Load(cacheKey); ok {
|
||||
cache := entry.(*azcache.TimedCache)
|
||||
return cacheKey, cache, nil
|
||||
}
|
||||
|
||||
cache, err := ss.newVMSSVirtualMachinesCache(resourceGroup, vmssName, cacheKey)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
ss.vmssVMCache.Store(cacheKey, cache)
|
||||
return cacheKey, cache, nil
|
||||
}
|
||||
|
||||
// gcVMSSVMCache delete stale VMSS VMs caches from deleted VMSSes.
|
||||
func (ss *scaleSet) gcVMSSVMCache() error {
|
||||
cached, err := ss.vmssCache.Get(vmssKey, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vmsses := cached.(*sync.Map)
|
||||
removed := map[string]bool{}
|
||||
ss.vmssVMCache.Range(func(key, value interface{}) bool {
|
||||
cacheKey := key.(string)
|
||||
vlistIdx := cacheKey[strings.LastIndex(cacheKey, "/")+1:]
|
||||
if _, ok := vmsses.Load(vlistIdx); !ok {
|
||||
removed[cacheKey] = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for key := range removed {
|
||||
ss.vmssVMCache.Delete(key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newVMSSVirtualMachinesCache instantiates a new VMs cache for VMs belonging to the provided VMSS.
|
||||
func (ss *scaleSet) newVMSSVirtualMachinesCache(resourceGroupName, vmssName, cacheKey string) (*azcache.TimedCache, error) {
|
||||
vmssVirtualMachinesCacheTTL := time.Duration(ss.Config.VmssVirtualMachinesCacheTTLInSeconds) * time.Second
|
||||
|
||||
getter := func(key string) (interface{}, error) {
|
||||
localCache := &sync.Map{} // [nodeName]*vmssVirtualMachinesEntry
|
||||
|
||||
oldCache := make(map[string]vmssVirtualMachinesEntry)
|
||||
|
||||
if vmssCache, ok := ss.vmssVMCache.Load(cacheKey); ok {
|
||||
// get old cache before refreshing the cache
|
||||
cache := vmssCache.(*azcache.TimedCache)
|
||||
entry, exists, err := cache.Store.GetByKey(cacheKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
cached := entry.(*azcache.AzureCacheEntry).Data
|
||||
if cached != nil {
|
||||
virtualMachines := cached.(*sync.Map)
|
||||
virtualMachines.Range(func(key, value interface{}) bool {
|
||||
oldCache[key.(string)] = *value.(*vmssVirtualMachinesEntry)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vms, err := ss.listScaleSetVMs(vmssName, resourceGroupName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range vms {
|
||||
vm := vms[i]
|
||||
if vm.OsProfile == nil || vm.OsProfile.ComputerName == nil {
|
||||
klog.Warningf("failed to get computerName for vmssVM (%q)", vmssName)
|
||||
continue
|
||||
}
|
||||
|
||||
computerName := strings.ToLower(*vm.OsProfile.ComputerName)
|
||||
if vm.NetworkProfile == nil || vm.NetworkProfile.NetworkInterfaces == nil {
|
||||
klog.Warningf("skip caching vmssVM %s since its network profile hasn't initialized yet (probably still under creating)", computerName)
|
||||
continue
|
||||
}
|
||||
|
||||
vmssVMCacheEntry := &vmssVirtualMachinesEntry{
|
||||
resourceGroup: resourceGroupName,
|
||||
vmssName: vmssName,
|
||||
instanceID: pointer.StringDeref(vm.InstanceID, ""),
|
||||
virtualMachine: &vm,
|
||||
lastUpdate: time.Now().UTC(),
|
||||
}
|
||||
// set cache entry to nil when the VM is under deleting.
|
||||
if vm.VirtualMachineScaleSetVMProperties != nil &&
|
||||
strings.EqualFold(pointer.StringDeref(vm.VirtualMachineScaleSetVMProperties.ProvisioningState, ""), string(compute.ProvisioningStateDeleting)) {
|
||||
klog.V(4).Infof("VMSS virtualMachine %q is under deleting, setting its cache to nil", computerName)
|
||||
vmssVMCacheEntry.virtualMachine = nil
|
||||
}
|
||||
localCache.Store(computerName, vmssVMCacheEntry)
|
||||
|
||||
delete(oldCache, computerName)
|
||||
}
|
||||
|
||||
// add old missing cache data with nil entries to prevent aggressive
|
||||
// ARM calls during cache invalidation
|
||||
for name, vmEntry := range oldCache {
|
||||
// if the nil cache entry has existed for vmssVirtualMachinesCacheTTL in the cache
|
||||
// then it should not be added back to the cache
|
||||
if vmEntry.virtualMachine == nil && time.Since(vmEntry.lastUpdate) > vmssVirtualMachinesCacheTTL {
|
||||
klog.V(5).Infof("ignoring expired entries from old cache for %s", name)
|
||||
continue
|
||||
}
|
||||
lastUpdate := time.Now().UTC()
|
||||
if vmEntry.virtualMachine == nil {
|
||||
// if this is already a nil entry then keep the time the nil
|
||||
// entry was first created, so we can cleanup unwanted entries
|
||||
lastUpdate = vmEntry.lastUpdate
|
||||
}
|
||||
|
||||
klog.V(5).Infof("adding old entries to new cache for %s", name)
|
||||
localCache.Store(name, &vmssVirtualMachinesEntry{
|
||||
resourceGroup: vmEntry.resourceGroup,
|
||||
vmssName: vmEntry.vmssName,
|
||||
instanceID: vmEntry.instanceID,
|
||||
virtualMachine: nil,
|
||||
lastUpdate: lastUpdate,
|
||||
})
|
||||
}
|
||||
|
||||
return localCache, nil
|
||||
}
|
||||
|
||||
return azcache.NewTimedcache(vmssVirtualMachinesCacheTTL, getter)
|
||||
}
|
||||
|
||||
func (ss *scaleSet) deleteCacheForNode(nodeName string) error {
|
||||
node, err := ss.getNodeIdentityByNodeName(nodeName, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
cacheKey, timedcache, err := ss.getVMSSVMCache(node.resourceGroup, node.vmssName)
|
||||
if err != nil {
|
||||
klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
vmcache, err := timedcache.Get(cacheKey, azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
klog.Errorf("deleteCacheForNode(%s) failed with error: %v", nodeName, err)
|
||||
return err
|
||||
}
|
||||
virtualMachines := vmcache.(*sync.Map)
|
||||
virtualMachines.Delete(nodeName)
|
||||
|
||||
if err := ss.gcVMSSVMCache(); err != nil {
|
||||
klog.Errorf("deleteCacheForNode(%s) failed to gc stale vmss caches: %v", nodeName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *scaleSet) newAvailabilitySetNodesCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
vmNames := sets.NewString()
|
||||
resourceGroups, err := ss.GetResourceGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, resourceGroup := range resourceGroups.List() {
|
||||
vmList, err := ss.Cloud.ListVirtualMachines(resourceGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, vm := range vmList {
|
||||
if vm.Name != nil {
|
||||
vmNames.Insert(*vm.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// store all the node names in the cluster when the cache data was created.
|
||||
nodeNames, err := ss.GetNodeNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
localCache := availabilitySetEntry{
|
||||
vmNames: vmNames,
|
||||
nodeNames: nodeNames,
|
||||
}
|
||||
|
||||
return localCache, nil
|
||||
}
|
||||
|
||||
if ss.Config.AvailabilitySetNodesCacheTTLInSeconds == 0 {
|
||||
ss.Config.AvailabilitySetNodesCacheTTLInSeconds = availabilitySetNodesCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(ss.Config.AvailabilitySetNodesCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func (ss *scaleSet) isNodeManagedByAvailabilitySet(nodeName string, crt azcache.AzureCacheReadType) (bool, error) {
|
||||
// Assume all nodes are managed by VMSS when DisableAvailabilitySetNodes is enabled.
|
||||
if ss.DisableAvailabilitySetNodes {
|
||||
klog.V(2).Infof("Assuming node %q is managed by VMSS since DisableAvailabilitySetNodes is set to true", nodeName)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cached, err := ss.availabilitySetNodesCache.Get(availabilitySetNodesKey, crt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
cachedNodes := cached.(availabilitySetEntry).nodeNames
|
||||
// if the node is not in the cache, assume the node has joined after the last cache refresh and attempt to refresh the cache.
|
||||
if !cachedNodes.Has(nodeName) {
|
||||
klog.V(2).Infof("Node %s has joined the cluster since the last VM cache refresh, refreshing the cache", nodeName)
|
||||
cached, err = ss.availabilitySetNodesCache.Get(availabilitySetNodesKey, azcache.CacheReadTypeForceRefresh)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
cachedVMs := cached.(availabilitySetEntry).vmNames
|
||||
return cachedVMs.Has(nodeName), nil
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssclient/mockvmssclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmssvmclient/mockvmssvmclient"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestExtractVmssVMName(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
vmName string
|
||||
expectError bool
|
||||
expectedScaleSet string
|
||||
expectedInstanceID string
|
||||
}{
|
||||
{
|
||||
description: "wrong vmss VM name should report error",
|
||||
vmName: "vm1234",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "wrong VM name separator should report error",
|
||||
vmName: "vm-1234",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
description: "correct vmss VM name should return correct scaleSet and instanceID",
|
||||
vmName: "vm_1234",
|
||||
expectedScaleSet: "vm",
|
||||
expectedInstanceID: "1234",
|
||||
},
|
||||
{
|
||||
description: "correct vmss VM name with Extra Separator should return correct scaleSet and instanceID",
|
||||
vmName: "vm_test_1234",
|
||||
expectedScaleSet: "vm_test",
|
||||
expectedInstanceID: "1234",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ssName, instanceID, err := extractVmssVMName(c.vmName)
|
||||
if c.expectError {
|
||||
assert.Error(t, err, c.description)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, c.expectedScaleSet, ssName, c.description)
|
||||
assert.Equal(t, c.expectedInstanceID, instanceID, c.description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMSSVMCache(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
vmssName := "vmss"
|
||||
vmList := []string{"vmssee6c2000000", "vmssee6c2000001", "vmssee6c2000002"}
|
||||
ss, err := newTestScaleSet(ctrl)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockVMSSClient := mockvmssclient.NewMockInterface(ctrl)
|
||||
mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl)
|
||||
ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient
|
||||
ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient
|
||||
|
||||
expectedScaleSet := buildTestVMSS(vmssName, "vmssee6c2")
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, nil).AnyTimes()
|
||||
|
||||
expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, vmssName, "", 0, vmList, "", false)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, nil).AnyTimes()
|
||||
|
||||
// validate getting VMSS VM via cache.
|
||||
for i := range expectedVMs {
|
||||
vm := expectedVMs[i]
|
||||
vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "")
|
||||
ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vmss", ssName)
|
||||
assert.Equal(t, pointer.StringDeref(vm.InstanceID, ""), instanceID)
|
||||
assert.Equal(t, &vm, realVM)
|
||||
}
|
||||
|
||||
// validate deleteCacheForNode().
|
||||
vm := expectedVMs[0]
|
||||
vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "")
|
||||
err = ss.deleteCacheForNode(vmName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// the VM should be removed from cache after deleteCacheForNode().
|
||||
cacheKey, cache, err := ss.getVMSSVMCache("rg", vmssName)
|
||||
assert.NoError(t, err)
|
||||
cached, err := cache.Get(cacheKey, azcache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
cachedVirtualMachines := cached.(*sync.Map)
|
||||
_, ok := cachedVirtualMachines.Load(vmName)
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// the VM should be back after another cache refresh.
|
||||
ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vmss", ssName)
|
||||
assert.Equal(t, pointer.StringDeref(vm.InstanceID, ""), instanceID)
|
||||
assert.Equal(t, &vm, realVM)
|
||||
}
|
||||
|
||||
func TestVMSSVMCacheWithDeletingNodes(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
vmssName := "vmss"
|
||||
vmList := []string{"vmssee6c2000000", "vmssee6c2000001", "vmssee6c2000002"}
|
||||
ss, err := newTestScaleSetWithState(ctrl)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockVMSSClient := mockvmssclient.NewMockInterface(ctrl)
|
||||
mockVMSSVMClient := mockvmssvmclient.NewMockInterface(ctrl)
|
||||
ss.cloud.VirtualMachineScaleSetsClient = mockVMSSClient
|
||||
ss.cloud.VirtualMachineScaleSetVMsClient = mockVMSSVMClient
|
||||
|
||||
expectedScaleSet := compute.VirtualMachineScaleSet{
|
||||
Name: &vmssName,
|
||||
VirtualMachineScaleSetProperties: &compute.VirtualMachineScaleSetProperties{},
|
||||
}
|
||||
mockVMSSClient.EXPECT().List(gomock.Any(), gomock.Any()).Return([]compute.VirtualMachineScaleSet{expectedScaleSet}, nil).AnyTimes()
|
||||
|
||||
expectedVMs, _, _ := buildTestVirtualMachineEnv(ss.cloud, vmssName, "", 0, vmList, string(compute.ProvisioningStateDeleting), false)
|
||||
mockVMSSVMClient.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedVMs, nil).AnyTimes()
|
||||
|
||||
for i := range expectedVMs {
|
||||
vm := expectedVMs[i]
|
||||
vmName := pointer.StringDeref(vm.OsProfile.ComputerName, "")
|
||||
assert.Equal(t, vm.ProvisioningState, pointer.String(string(compute.ProvisioningStateDeleting)))
|
||||
|
||||
ssName, instanceID, realVM, err := ss.getVmssVM(vmName, azcache.CacheReadTypeDefault)
|
||||
assert.Nil(t, realVM)
|
||||
assert.Equal(t, "", ssName)
|
||||
assert.Equal(t, instanceID, ssName)
|
||||
assert.Equal(t, cloudprovider.InstanceNotFound, err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,356 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var (
|
||||
vmCacheTTLDefaultInSeconds = 60
|
||||
loadBalancerCacheTTLDefaultInSeconds = 120
|
||||
nsgCacheTTLDefaultInSeconds = 120
|
||||
routeTableCacheTTLDefaultInSeconds = 120
|
||||
|
||||
azureNodeProviderIDRE = regexp.MustCompile(`^azure:///subscriptions/(?:.*)/resourceGroups/(?:.*)/providers/Microsoft.Compute/(?:.*)`)
|
||||
azureResourceGroupNameRE = regexp.MustCompile(`.*/subscriptions/(?:.*)/resourceGroups/(.+)/providers/(?:.*)`)
|
||||
)
|
||||
|
||||
// checkExistsFromError inspects an error and returns a true if err is nil,
|
||||
// false if error is an autorest.Error with StatusCode=404 and will return the
|
||||
// error back if error is another status code or another type of error.
|
||||
func checkResourceExistsFromError(err *retry.Error) (bool, *retry.Error) {
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err.HTTPStatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// / getVirtualMachine calls 'VirtualMachinesClient.Get' with a timed cache
|
||||
// / The service side has throttling control that delays responses if there are multiple requests onto certain vm
|
||||
// / resource request in short period.
|
||||
func (az *Cloud) getVirtualMachine(nodeName types.NodeName, crt azcache.AzureCacheReadType) (vm compute.VirtualMachine, err error) {
|
||||
vmName := string(nodeName)
|
||||
cachedVM, err := az.vmCache.Get(vmName, crt)
|
||||
if err != nil {
|
||||
return vm, err
|
||||
}
|
||||
|
||||
if cachedVM == nil {
|
||||
return vm, cloudprovider.InstanceNotFound
|
||||
}
|
||||
|
||||
return *(cachedVM.(*compute.VirtualMachine)), nil
|
||||
}
|
||||
|
||||
func (az *Cloud) getRouteTable(crt azcache.AzureCacheReadType) (routeTable network.RouteTable, exists bool, err error) {
|
||||
cachedRt, err := az.rtCache.Get(az.RouteTableName, crt)
|
||||
if err != nil {
|
||||
return routeTable, false, err
|
||||
}
|
||||
|
||||
if cachedRt == nil {
|
||||
return routeTable, false, nil
|
||||
}
|
||||
|
||||
return *(cachedRt.(*network.RouteTable)), true, nil
|
||||
}
|
||||
|
||||
func (az *Cloud) getPublicIPAddress(pipResourceGroup string, pipName string) (network.PublicIPAddress, bool, error) {
|
||||
resourceGroup := az.ResourceGroup
|
||||
if pipResourceGroup != "" {
|
||||
resourceGroup = pipResourceGroup
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
pip, err := az.PublicIPAddressesClient.Get(ctx, resourceGroup, pipName, "")
|
||||
exists, rerr := checkResourceExistsFromError(err)
|
||||
if rerr != nil {
|
||||
return pip, false, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Public IP %q not found", pipName)
|
||||
return pip, false, nil
|
||||
}
|
||||
|
||||
return pip, exists, nil
|
||||
}
|
||||
|
||||
func (az *Cloud) getSubnet(virtualNetworkName string, subnetName string) (network.Subnet, bool, error) {
|
||||
var rg string
|
||||
if len(az.VnetResourceGroup) > 0 {
|
||||
rg = az.VnetResourceGroup
|
||||
} else {
|
||||
rg = az.ResourceGroup
|
||||
}
|
||||
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
subnet, err := az.SubnetsClient.Get(ctx, rg, virtualNetworkName, subnetName, "")
|
||||
exists, rerr := checkResourceExistsFromError(err)
|
||||
if rerr != nil {
|
||||
return subnet, false, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Subnet %q not found", subnetName)
|
||||
return subnet, false, nil
|
||||
}
|
||||
|
||||
return subnet, exists, nil
|
||||
}
|
||||
|
||||
func (az *Cloud) getAzureLoadBalancer(name string, crt azcache.AzureCacheReadType) (lb network.LoadBalancer, exists bool, err error) {
|
||||
cachedLB, err := az.lbCache.Get(name, crt)
|
||||
if err != nil {
|
||||
return lb, false, err
|
||||
}
|
||||
|
||||
if cachedLB == nil {
|
||||
return lb, false, nil
|
||||
}
|
||||
|
||||
return *(cachedLB.(*network.LoadBalancer)), true, nil
|
||||
}
|
||||
|
||||
func (az *Cloud) getSecurityGroup(crt azcache.AzureCacheReadType) (network.SecurityGroup, error) {
|
||||
nsg := network.SecurityGroup{}
|
||||
if az.SecurityGroupName == "" {
|
||||
return nsg, fmt.Errorf("securityGroupName is not configured")
|
||||
}
|
||||
|
||||
securityGroup, err := az.nsgCache.Get(az.SecurityGroupName, crt)
|
||||
if err != nil {
|
||||
return nsg, err
|
||||
}
|
||||
|
||||
if securityGroup == nil {
|
||||
return nsg, fmt.Errorf("nsg %q not found", az.SecurityGroupName)
|
||||
}
|
||||
|
||||
return *(securityGroup.(*network.SecurityGroup)), nil
|
||||
}
|
||||
|
||||
func (az *Cloud) newVMCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
// Currently InstanceView request are used by azure_zones, while the calls come after non-InstanceView
|
||||
// request. If we first send an InstanceView request and then a non InstanceView request, the second
|
||||
// request will still hit throttling. This is what happens now for cloud controller manager: In this
|
||||
// case we do get instance view every time to fulfill the azure_zones requirement without hitting
|
||||
// throttling.
|
||||
// Consider adding separate parameter for controlling 'InstanceView' once node update issue #56276 is fixed
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
resourceGroup, err := az.GetNodeResourceGroup(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vm, verr := az.VirtualMachinesClient.Get(ctx, resourceGroup, key, compute.InstanceView)
|
||||
exists, rerr := checkResourceExistsFromError(verr)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Virtual machine %q not found", key)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if vm.VirtualMachineProperties != nil &&
|
||||
strings.EqualFold(pointer.StringDeref(vm.VirtualMachineProperties.ProvisioningState, ""), string(compute.ProvisioningStateDeleting)) {
|
||||
klog.V(2).Infof("Virtual machine %q is under deleting", key)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &vm, nil
|
||||
}
|
||||
|
||||
if az.VMCacheTTLInSeconds == 0 {
|
||||
az.VMCacheTTLInSeconds = vmCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(az.VMCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func (az *Cloud) newLBCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
|
||||
lb, err := az.LoadBalancerClient.Get(ctx, az.getLoadBalancerResourceGroup(), key, "")
|
||||
exists, rerr := checkResourceExistsFromError(err)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Load balancer %q not found", key)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &lb, nil
|
||||
}
|
||||
|
||||
if az.LoadBalancerCacheTTLInSeconds == 0 {
|
||||
az.LoadBalancerCacheTTLInSeconds = loadBalancerCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(az.LoadBalancerCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func (az *Cloud) newNSGCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
nsg, err := az.SecurityGroupsClient.Get(ctx, az.SecurityGroupResourceGroup, key, "")
|
||||
exists, rerr := checkResourceExistsFromError(err)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Security group %q not found", key)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &nsg, nil
|
||||
}
|
||||
|
||||
if az.NsgCacheTTLInSeconds == 0 {
|
||||
az.NsgCacheTTLInSeconds = nsgCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(az.NsgCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func (az *Cloud) newRouteTableCache() (*azcache.TimedCache, error) {
|
||||
getter := func(key string) (interface{}, error) {
|
||||
ctx, cancel := getContextWithCancel()
|
||||
defer cancel()
|
||||
rt, err := az.RouteTablesClient.Get(ctx, az.RouteTableResourceGroup, key, "")
|
||||
exists, rerr := checkResourceExistsFromError(err)
|
||||
if rerr != nil {
|
||||
return nil, rerr.Error()
|
||||
}
|
||||
|
||||
if !exists {
|
||||
klog.V(2).Infof("Route table %q not found", key)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &rt, nil
|
||||
}
|
||||
|
||||
if az.RouteTableCacheTTLInSeconds == 0 {
|
||||
az.RouteTableCacheTTLInSeconds = routeTableCacheTTLDefaultInSeconds
|
||||
}
|
||||
return azcache.NewTimedcache(time.Duration(az.RouteTableCacheTTLInSeconds)*time.Second, getter)
|
||||
}
|
||||
|
||||
func (az *Cloud) useStandardLoadBalancer() bool {
|
||||
return strings.EqualFold(az.LoadBalancerSku, loadBalancerSkuStandard)
|
||||
}
|
||||
|
||||
func (az *Cloud) excludeMasterNodesFromStandardLB() bool {
|
||||
return az.ExcludeMasterFromStandardLB != nil && *az.ExcludeMasterFromStandardLB
|
||||
}
|
||||
|
||||
func (az *Cloud) disableLoadBalancerOutboundSNAT() bool {
|
||||
if !az.useStandardLoadBalancer() || az.DisableOutboundSNAT == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *az.DisableOutboundSNAT
|
||||
}
|
||||
|
||||
// IsNodeUnmanaged returns true if the node is not managed by Azure cloud provider.
|
||||
// Those nodes includes on-prem or VMs from other clouds. They will not be added to load balancer
|
||||
// backends. Azure routes and managed disks are also not supported for them.
|
||||
func (az *Cloud) IsNodeUnmanaged(nodeName string) (bool, error) {
|
||||
unmanagedNodes, err := az.GetUnmanagedNodes()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return unmanagedNodes.Has(nodeName), nil
|
||||
}
|
||||
|
||||
// IsNodeUnmanagedByProviderID returns true if the node is not managed by Azure cloud provider.
|
||||
// All managed node's providerIDs are in format 'azure:///subscriptions/<id>/resourceGroups/<rg>/providers/Microsoft.Compute/.*'
|
||||
func (az *Cloud) IsNodeUnmanagedByProviderID(providerID string) bool {
|
||||
return !azureNodeProviderIDRE.Match([]byte(providerID))
|
||||
}
|
||||
|
||||
// convertResourceGroupNameToLower converts the resource group name in the resource ID to be lowered.
|
||||
func convertResourceGroupNameToLower(resourceID string) (string, error) {
|
||||
matches := azureResourceGroupNameRE.FindStringSubmatch(resourceID)
|
||||
if len(matches) != 2 {
|
||||
return "", fmt.Errorf("%q isn't in Azure resource ID format %q", resourceID, azureResourceGroupNameRE.String())
|
||||
}
|
||||
|
||||
resourceGroup := matches[1]
|
||||
return strings.Replace(resourceID, resourceGroup, strings.ToLower(resourceGroup), 1), nil
|
||||
}
|
||||
|
||||
// isBackendPoolOnSameLB checks whether newBackendPoolID is on the same load balancer as existingBackendPools.
|
||||
// Since both public and internal LBs are supported, lbName and lbName-internal are treated as same.
|
||||
// If not same, the lbName for existingBackendPools would also be returned.
|
||||
func isBackendPoolOnSameLB(newBackendPoolID string, existingBackendPools []string) (bool, string, error) {
|
||||
matches := backendPoolIDRE.FindStringSubmatch(newBackendPoolID)
|
||||
if len(matches) != 2 {
|
||||
return false, "", fmt.Errorf("new backendPoolID %q is in wrong format", newBackendPoolID)
|
||||
}
|
||||
|
||||
newLBName := matches[1]
|
||||
newLBNameTrimmed := strings.TrimSuffix(newLBName, InternalLoadBalancerNameSuffix)
|
||||
for _, backendPool := range existingBackendPools {
|
||||
matches := backendPoolIDRE.FindStringSubmatch(backendPool)
|
||||
if len(matches) != 2 {
|
||||
return false, "", fmt.Errorf("existing backendPoolID %q is in wrong format", backendPool)
|
||||
}
|
||||
|
||||
lbName := matches[1]
|
||||
if !strings.EqualFold(strings.TrimSuffix(lbName, InternalLoadBalancerNameSuffix), newLBNameTrimmed) {
|
||||
return false, lbName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
func TestExtractNotFound(t *testing.T) {
|
||||
notFound := &retry.Error{HTTPStatusCode: http.StatusNotFound}
|
||||
otherHTTP := &retry.Error{HTTPStatusCode: http.StatusForbidden}
|
||||
otherErr := &retry.Error{HTTPStatusCode: http.StatusTooManyRequests}
|
||||
|
||||
tests := []struct {
|
||||
err *retry.Error
|
||||
expectedErr *retry.Error
|
||||
exists bool
|
||||
}{
|
||||
{nil, nil, true},
|
||||
{otherErr, otherErr, false},
|
||||
{notFound, nil, false},
|
||||
{otherHTTP, otherHTTP, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
exists, err := checkResourceExistsFromError(test.err)
|
||||
if test.exists != exists {
|
||||
t.Errorf("expected: %v, saw: %v", test.exists, exists)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedErr, err) {
|
||||
t.Errorf("expected err: %v, saw: %v", test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNodeUnmanaged(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
unmanagedNodes sets.String
|
||||
node string
|
||||
expected bool
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "unmanaged node should return true",
|
||||
unmanagedNodes: sets.NewString("node1", "node2"),
|
||||
node: "node1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "managed node should return false",
|
||||
unmanagedNodes: sets.NewString("node1", "node2"),
|
||||
node: "node3",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty unmanagedNodes should return true",
|
||||
unmanagedNodes: sets.NewString(),
|
||||
node: "node3",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no synced informer should report error",
|
||||
unmanagedNodes: sets.NewString(),
|
||||
node: "node1",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
for _, test := range tests {
|
||||
az.unmanagedNodes = test.unmanagedNodes
|
||||
if test.expectErr {
|
||||
az.nodeInformerSynced = func() bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
real, err := az.IsNodeUnmanaged(test.node)
|
||||
if test.expectErr {
|
||||
assert.Error(t, err, test.name)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.NoError(t, err, test.name)
|
||||
assert.Equal(t, test.expected, real, test.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNodeUnmanagedByProviderID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
providerID string
|
||||
expected bool
|
||||
name string
|
||||
}{
|
||||
{
|
||||
providerID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
providerID: CloudProviderName + "://",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
providerID: ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
providerID: "aws:///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
providerID: "k8s-agent-AAAAAAAA-0",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
for _, test := range tests {
|
||||
isUnmanagedNode := az.IsNodeUnmanagedByProviderID(test.providerID)
|
||||
assert.Equal(t, test.expected, isUnmanagedNode, test.providerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertResourceGroupNameToLower(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
resourceID string
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
desc: "empty string should report error",
|
||||
resourceID: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "resourceID not in Azure format should report error",
|
||||
resourceID: "invalid-id",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "providerID not in Azure format should report error",
|
||||
resourceID: "azure://invalid-id",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "resource group name in VM providerID should be converted",
|
||||
resourceID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
expected: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
},
|
||||
{
|
||||
desc: "resource group name in VM resourceID should be converted",
|
||||
resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
expected: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachines/k8s-agent-AAAAAAAA-0",
|
||||
},
|
||||
{
|
||||
desc: "resource group name in VMSS providerID should be converted",
|
||||
resourceID: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156",
|
||||
expected: CloudProviderName + ":///subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156",
|
||||
},
|
||||
{
|
||||
desc: "resource group name in VMSS resourceID should be converted",
|
||||
resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroupName/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156",
|
||||
expected: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroupname/providers/Microsoft.Compute/virtualMachineScaleSets/myScaleSetName/virtualMachines/156",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
real, err := convertResourceGroupNameToLower(test.resourceID)
|
||||
if test.expectError {
|
||||
assert.NotNil(t, err, test.desc)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Nil(t, err, test.desc)
|
||||
assert.Equal(t, test.expected, real, test.desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBackendPoolOnSameLB(t *testing.T) {
|
||||
tests := []struct {
|
||||
backendPoolID string
|
||||
existingBackendPools []string
|
||||
expected bool
|
||||
expectedLBName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2",
|
||||
},
|
||||
expected: true,
|
||||
expectedLBName: "",
|
||||
},
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1-internal/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2",
|
||||
},
|
||||
expected: true,
|
||||
expectedLBName: "",
|
||||
},
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1-internal/backendAddressPools/pool2",
|
||||
},
|
||||
expected: true,
|
||||
expectedLBName: "",
|
||||
},
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb2/backendAddressPools/pool2",
|
||||
},
|
||||
expected: false,
|
||||
expectedLBName: "lb2",
|
||||
},
|
||||
{
|
||||
backendPoolID: "wrong-backendpool-id",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool2",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/lb1/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"wrong-existing-backendpool-id",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
backendPoolID: "wrong-backendpool-id",
|
||||
existingBackendPools: []string{
|
||||
"wrong-existing-backendpool-id",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
backendPoolID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/malformed-lb1-internal/backendAddressPools/pool1",
|
||||
existingBackendPools: []string{
|
||||
"/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/malformed-lb1-lanretni/backendAddressPools/pool2",
|
||||
},
|
||||
expected: false,
|
||||
expectedLBName: "malformed-lb1-lanretni",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
isSameLB, lbName, err := isBackendPoolOnSameLB(test.backendPoolID, test.existingBackendPools)
|
||||
if test.expectError {
|
||||
assert.Error(t, err)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, isSameLB)
|
||||
assert.Equal(t, test.expectedLBName, lbName)
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/klog/v2"
|
||||
azcache "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
)
|
||||
|
||||
// makeZone returns the zone value in format of <region>-<zone-id>.
|
||||
func (az *Cloud) makeZone(location string, zoneID int) string {
|
||||
return fmt.Sprintf("%s-%d", strings.ToLower(location), zoneID)
|
||||
}
|
||||
|
||||
// isAvailabilityZone returns true if the zone is in format of <region>-<zone-id>.
|
||||
func (az *Cloud) isAvailabilityZone(zone string) bool {
|
||||
return strings.HasPrefix(zone, fmt.Sprintf("%s-", az.Location))
|
||||
}
|
||||
|
||||
// GetZoneID returns the ID of zone from node's zone label.
|
||||
func (az *Cloud) GetZoneID(zoneLabel string) string {
|
||||
if !az.isAvailabilityZone(zoneLabel) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(zoneLabel, fmt.Sprintf("%s-", az.Location))
|
||||
}
|
||||
|
||||
// GetZone returns the Zone containing the current availability zone and locality region that the program is running in.
|
||||
// If the node is not running with availability zones, then it will fall back to fault domain.
|
||||
func (az *Cloud) GetZone(ctx context.Context) (cloudprovider.Zone, error) {
|
||||
if az.UseInstanceMetadata {
|
||||
metadata, err := az.metadata.GetMetadata(azcache.CacheReadTypeUnsafe)
|
||||
if err != nil {
|
||||
return cloudprovider.Zone{}, err
|
||||
}
|
||||
|
||||
if metadata.Compute == nil {
|
||||
az.metadata.imsCache.Delete(metadataCacheKey)
|
||||
return cloudprovider.Zone{}, fmt.Errorf("failure of getting compute information from instance metadata")
|
||||
}
|
||||
|
||||
zone := ""
|
||||
location := metadata.Compute.Location
|
||||
if metadata.Compute.Zone != "" {
|
||||
zoneID, err := strconv.Atoi(metadata.Compute.Zone)
|
||||
if err != nil {
|
||||
return cloudprovider.Zone{}, fmt.Errorf("failed to parse zone ID %q: %v", metadata.Compute.Zone, err)
|
||||
}
|
||||
zone = az.makeZone(location, zoneID)
|
||||
} else {
|
||||
klog.V(3).Infof("Availability zone is not enabled for the node, falling back to fault domain")
|
||||
zone = metadata.Compute.FaultDomain
|
||||
}
|
||||
|
||||
return cloudprovider.Zone{
|
||||
FailureDomain: strings.ToLower(zone),
|
||||
Region: strings.ToLower(location),
|
||||
}, nil
|
||||
}
|
||||
// if UseInstanceMetadata is false, get Zone name by calling ARM
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return cloudprovider.Zone{}, fmt.Errorf("failure getting hostname from kernel")
|
||||
}
|
||||
return az.VMSet.GetZoneByNodeName(strings.ToLower(hostname))
|
||||
}
|
||||
|
||||
// GetZoneByProviderID implements Zones.GetZoneByProviderID
|
||||
// This is particularly useful in external cloud providers where the kubelet
|
||||
// does not initialize node data.
|
||||
func (az *Cloud) GetZoneByProviderID(ctx context.Context, providerID string) (cloudprovider.Zone, error) {
|
||||
if providerID == "" {
|
||||
return cloudprovider.Zone{}, errNodeNotInitialized
|
||||
}
|
||||
|
||||
// Returns nil for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
if az.IsNodeUnmanagedByProviderID(providerID) {
|
||||
klog.V(2).Infof("GetZoneByProviderID: omitting unmanaged node %q", providerID)
|
||||
return cloudprovider.Zone{}, nil
|
||||
}
|
||||
|
||||
nodeName, err := az.VMSet.GetNodeNameByProviderID(providerID)
|
||||
if err != nil {
|
||||
return cloudprovider.Zone{}, err
|
||||
}
|
||||
|
||||
return az.GetZoneByNodeName(ctx, nodeName)
|
||||
}
|
||||
|
||||
// GetZoneByNodeName implements Zones.GetZoneByNodeName
|
||||
// This is particularly useful in external cloud providers where the kubelet
|
||||
// does not initialize node data.
|
||||
func (az *Cloud) GetZoneByNodeName(ctx context.Context, nodeName types.NodeName) (cloudprovider.Zone, error) {
|
||||
// Returns "" for unmanaged nodes because azure cloud provider couldn't fetch information for them.
|
||||
unmanaged, err := az.IsNodeUnmanaged(string(nodeName))
|
||||
if err != nil {
|
||||
return cloudprovider.Zone{}, err
|
||||
}
|
||||
if unmanaged {
|
||||
klog.V(2).Infof("GetZoneByNodeName: omitting unmanaged node %q", nodeName)
|
||||
return cloudprovider.Zone{}, nil
|
||||
}
|
||||
|
||||
return az.VMSet.GetZoneByNodeName(string(nodeName))
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
cloudprovider "k8s.io/cloud-provider"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/vmclient/mockvmclient"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
testAvailabilitySetNodeProviderID = "azure:///subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm-0"
|
||||
)
|
||||
|
||||
func TestIsAvailabilityZone(t *testing.T) {
|
||||
location := "eastus"
|
||||
az := &Cloud{
|
||||
Config: Config{
|
||||
Location: location,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
desc string
|
||||
zone string
|
||||
expected bool
|
||||
}{
|
||||
{"empty string should return false", "", false},
|
||||
{"wrong format should return false", "123", false},
|
||||
{"wrong location should return false", "chinanorth-1", false},
|
||||
{"correct zone should return true", "eastus-1", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := az.isAvailabilityZone(test.zone)
|
||||
if actual != test.expected {
|
||||
t.Errorf("test [%q] get unexpected result: %v != %v", test.desc, actual, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetZoneID(t *testing.T) {
|
||||
location := "eastus"
|
||||
az := &Cloud{
|
||||
Config: Config{
|
||||
Location: location,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
desc string
|
||||
zone string
|
||||
expected string
|
||||
}{
|
||||
{"empty string should return empty string", "", ""},
|
||||
{"wrong format should return empty string", "123", ""},
|
||||
{"wrong location should return empty string", "chinanorth-1", ""},
|
||||
{"correct zone should return zone ID", "eastus-1", "1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := az.GetZoneID(test.zone)
|
||||
if actual != test.expected {
|
||||
t.Errorf("test [%q] get unexpected result: %q != %q", test.desc, actual, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetZone(t *testing.T) {
|
||||
cloud := &Cloud{
|
||||
Config: Config{
|
||||
Location: "eastus",
|
||||
UseInstanceMetadata: true,
|
||||
},
|
||||
}
|
||||
testcases := []struct {
|
||||
name string
|
||||
zone string
|
||||
location string
|
||||
faultDomain string
|
||||
expected string
|
||||
isNilResp bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "GetZone should get real zone if only node's zone is set",
|
||||
zone: "1",
|
||||
location: "eastus",
|
||||
expected: "eastus-1",
|
||||
},
|
||||
{
|
||||
name: "GetZone should get real zone if both node's zone and FD are set",
|
||||
zone: "1",
|
||||
location: "eastus",
|
||||
faultDomain: "99",
|
||||
expected: "eastus-1",
|
||||
},
|
||||
{
|
||||
name: "GetZone should get faultDomain if node's zone isn't set",
|
||||
location: "eastus",
|
||||
faultDomain: "99",
|
||||
expected: "99",
|
||||
},
|
||||
{
|
||||
name: "GetZone should get availability zone in lower cases",
|
||||
location: "EastUS",
|
||||
zone: "1",
|
||||
expected: "eastus-1",
|
||||
},
|
||||
{
|
||||
name: "GetZone should report an error if there is no `Compute` in the response",
|
||||
isNilResp: true,
|
||||
expectedErr: fmt.Errorf("failure of getting compute information from instance metadata"),
|
||||
},
|
||||
{
|
||||
name: "GetZone should report an error if the zone is invalid",
|
||||
zone: "a",
|
||||
location: "eastus",
|
||||
faultDomain: "99",
|
||||
expected: "",
|
||||
expectedErr: fmt.Errorf("failed to parse zone ID \"a\": strconv.Atoi: parsing \"a\": invalid syntax"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
respString := fmt.Sprintf(`{"compute":{"zone":"%s", "platformFaultDomain":"%s", "location":"%s"}}`, test.zone, test.faultDomain, test.location)
|
||||
if test.isNilResp {
|
||||
respString = "{}"
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, respString)
|
||||
}))
|
||||
go func() {
|
||||
http.Serve(listener, mux)
|
||||
}()
|
||||
defer listener.Close()
|
||||
|
||||
cloud.metadata, err = NewInstanceMetadataService("http://" + listener.Addr().String() + "/")
|
||||
if err != nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
zone, err := cloud.GetZone(context.Background())
|
||||
if err != nil {
|
||||
if test.expectedErr == nil {
|
||||
t.Errorf("Test [%s] unexpected error: %v", test.name, err)
|
||||
} else {
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
if zone.FailureDomain != test.expected {
|
||||
t.Errorf("Test [%s] unexpected zone: %s, expected %q", test.name, zone.FailureDomain, test.expected)
|
||||
}
|
||||
if err == nil && zone.Region != cloud.Location {
|
||||
t.Errorf("Test [%s] unexpected region: %s, expected: %s", test.name, zone.Region, cloud.Location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeZone(t *testing.T) {
|
||||
az := &Cloud{}
|
||||
zone := az.makeZone("EASTUS", 2)
|
||||
assert.Equal(t, "eastus-2", zone)
|
||||
}
|
||||
|
||||
func TestGetZoneByProviderID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
az := GetTestCloud(ctrl)
|
||||
|
||||
zone, err := az.GetZoneByProviderID(context.Background(), "")
|
||||
assert.Equal(t, errNodeNotInitialized, err)
|
||||
assert.Equal(t, cloudprovider.Zone{}, zone)
|
||||
|
||||
zone, err = az.GetZoneByProviderID(context.Background(), "invalid/id")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cloudprovider.Zone{}, zone)
|
||||
|
||||
mockVMClient := az.VirtualMachinesClient.(*mockvmclient.MockInterface)
|
||||
mockVMClient.EXPECT().Get(gomock.Any(), az.ResourceGroup, "vm-0", gomock.Any()).Return(compute.VirtualMachine{
|
||||
Zones: &[]string{"1"},
|
||||
Location: pointer.String("eastus"),
|
||||
}, nil)
|
||||
zone, err = az.GetZoneByProviderID(context.Background(), testAvailabilitySetNodeProviderID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cloudprovider.Zone{
|
||||
FailureDomain: "eastus-1",
|
||||
Region: "eastus",
|
||||
}, zone)
|
||||
}
|
||||
|
||||
func TestAvailabilitySetGetZoneByNodeName(t *testing.T) {
|
||||
az := &Cloud{
|
||||
unmanagedNodes: sets.String{"vm-0": sets.Empty{}},
|
||||
nodeInformerSynced: func() bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
zone, err := az.GetZoneByNodeName(context.Background(), "vm-0")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cloudprovider.Zone{}, zone)
|
||||
|
||||
az = &Cloud{
|
||||
unmanagedNodes: sets.String{"vm-0": sets.Empty{}},
|
||||
nodeInformerSynced: func() bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
zone, err = az.GetZoneByNodeName(context.Background(), "vm-0")
|
||||
assert.Equal(t, fmt.Errorf("node informer is not synced when trying to GetUnmanagedNodes"), err)
|
||||
assert.Equal(t, cloudprovider.Zone{}, zone)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// AzureCacheReadType defines the read type for cache data
|
||||
type AzureCacheReadType int
|
||||
|
||||
const (
|
||||
// CacheReadTypeDefault returns data from cache if cache entry not expired
|
||||
// if cache entry expired, then it will refetch the data using getter
|
||||
// save the entry in cache and then return
|
||||
CacheReadTypeDefault AzureCacheReadType = iota
|
||||
// CacheReadTypeUnsafe returns data from cache even if the cache entry is
|
||||
// active/expired. If entry doesn't exist in cache, then data is fetched
|
||||
// using getter, saved in cache and returned
|
||||
CacheReadTypeUnsafe
|
||||
// CacheReadTypeForceRefresh force refreshes the cache even if the cache entry
|
||||
// is not expired
|
||||
CacheReadTypeForceRefresh
|
||||
)
|
||||
|
||||
// GetFunc defines a getter function for timedCache.
|
||||
type GetFunc func(key string) (interface{}, error)
|
||||
|
||||
// AzureCacheEntry is the internal structure stores inside TTLStore.
|
||||
type AzureCacheEntry struct {
|
||||
Key string
|
||||
Data interface{}
|
||||
|
||||
// The lock to ensure not updating same entry simultaneously.
|
||||
Lock sync.Mutex
|
||||
// time when entry was fetched and created
|
||||
CreatedOn time.Time
|
||||
}
|
||||
|
||||
// cacheKeyFunc defines the key function required in TTLStore.
|
||||
func cacheKeyFunc(obj interface{}) (string, error) {
|
||||
return obj.(*AzureCacheEntry).Key, nil
|
||||
}
|
||||
|
||||
// TimedCache is a cache with TTL.
|
||||
type TimedCache struct {
|
||||
Store cache.Store
|
||||
Lock sync.Mutex
|
||||
Getter GetFunc
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// NewTimedcache creates a new TimedCache.
|
||||
func NewTimedcache(ttl time.Duration, getter GetFunc) (*TimedCache, error) {
|
||||
if getter == nil {
|
||||
return nil, fmt.Errorf("getter is not provided")
|
||||
}
|
||||
|
||||
return &TimedCache{
|
||||
Getter: getter,
|
||||
// switch to using NewStore instead of NewTTLStore so that we can
|
||||
// reuse entries for calls that are fine with reading expired/stalled data.
|
||||
// with NewTTLStore, entries are not returned if they have already expired.
|
||||
Store: cache.NewStore(cacheKeyFunc),
|
||||
TTL: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getInternal returns AzureCacheEntry by key. If the key is not cached yet,
|
||||
// it returns a AzureCacheEntry with nil data.
|
||||
func (t *TimedCache) getInternal(key string) (*AzureCacheEntry, error) {
|
||||
entry, exists, err := t.Store.GetByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if entry exists, return the entry
|
||||
if exists {
|
||||
return entry.(*AzureCacheEntry), nil
|
||||
}
|
||||
|
||||
// lock here to ensure if entry doesn't exist, we add a new entry
|
||||
// avoiding overwrites
|
||||
t.Lock.Lock()
|
||||
defer t.Lock.Unlock()
|
||||
|
||||
// Another goroutine might have written the same key.
|
||||
entry, exists, err = t.Store.GetByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return entry.(*AzureCacheEntry), nil
|
||||
}
|
||||
|
||||
// Still not found, add new entry with nil data.
|
||||
// Note the data will be filled later by getter.
|
||||
newEntry := &AzureCacheEntry{
|
||||
Key: key,
|
||||
Data: nil,
|
||||
}
|
||||
t.Store.Add(newEntry)
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
// Get returns the requested item by key.
|
||||
func (t *TimedCache) Get(key string, crt AzureCacheReadType) (interface{}, error) {
|
||||
entry, err := t.getInternal(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry.Lock.Lock()
|
||||
defer entry.Lock.Unlock()
|
||||
|
||||
// entry exists and if cache is not force refreshed
|
||||
if entry.Data != nil && crt != CacheReadTypeForceRefresh {
|
||||
// allow unsafe read, so return data even if expired
|
||||
if crt == CacheReadTypeUnsafe {
|
||||
return entry.Data, nil
|
||||
}
|
||||
// if cached data is not expired, return cached data
|
||||
if crt == CacheReadTypeDefault && time.Since(entry.CreatedOn) < t.TTL {
|
||||
return entry.Data, nil
|
||||
}
|
||||
}
|
||||
// Data is not cached yet, cache data is expired or requested force refresh
|
||||
// cache it by getter. entry is locked before getting to ensure concurrent
|
||||
// gets don't result in multiple ARM calls.
|
||||
data, err := t.Getter(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the data in cache and also set the last update time
|
||||
// to now as the data was recently fetched
|
||||
entry.Data = data
|
||||
entry.CreatedOn = time.Now().UTC()
|
||||
|
||||
return entry.Data, nil
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache.
|
||||
func (t *TimedCache) Delete(key string) error {
|
||||
return t.Store.Delete(&AzureCacheEntry{
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
|
||||
// Set sets the data cache for the key.
|
||||
// It is only used for testing.
|
||||
func (t *TimedCache) Set(key string, data interface{}) {
|
||||
t.Store.Add(&AzureCacheEntry{
|
||||
Key: key,
|
||||
Data: data,
|
||||
CreatedOn: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeCacheTTL = 2 * time.Second
|
||||
)
|
||||
|
||||
type fakeDataObj struct{}
|
||||
|
||||
type fakeDataSource struct {
|
||||
called int
|
||||
data map[string]*fakeDataObj
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (fake *fakeDataSource) get(key string) (interface{}, error) {
|
||||
fake.lock.Lock()
|
||||
defer fake.lock.Unlock()
|
||||
|
||||
fake.called = fake.called + 1
|
||||
if v, ok := fake.data[key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fake *fakeDataSource) set(data map[string]*fakeDataObj) {
|
||||
fake.lock.Lock()
|
||||
defer fake.lock.Unlock()
|
||||
|
||||
fake.data = data
|
||||
fake.called = 0
|
||||
}
|
||||
|
||||
func newFakeCache(t *testing.T) (*fakeDataSource, *TimedCache) {
|
||||
dataSource := &fakeDataSource{
|
||||
data: make(map[string]*fakeDataObj),
|
||||
}
|
||||
getter := dataSource.get
|
||||
cache, err := NewTimedcache(fakeCacheTTL, getter)
|
||||
assert.NoError(t, err)
|
||||
return dataSource, cache
|
||||
}
|
||||
|
||||
func TestCacheGet(t *testing.T) {
|
||||
val := &fakeDataObj{}
|
||||
cases := []struct {
|
||||
name string
|
||||
data map[string]*fakeDataObj
|
||||
key string
|
||||
expected interface{}
|
||||
}{
|
||||
{
|
||||
name: "cache should return nil for empty data source",
|
||||
key: "key1",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "cache should return nil for non exist key",
|
||||
data: map[string]*fakeDataObj{"key2": val},
|
||||
key: "key1",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "cache should return data for existing key",
|
||||
data: map[string]*fakeDataObj{"key1": val},
|
||||
key: "key1",
|
||||
expected: val,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(c.data)
|
||||
val, err := cache.Get(c.key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err, c.name)
|
||||
assert.Equal(t, c.expected, val, c.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheGetError(t *testing.T) {
|
||||
getError := fmt.Errorf("getError")
|
||||
getter := func(key string) (interface{}, error) {
|
||||
return nil, getError
|
||||
}
|
||||
cache, err := NewTimedcache(fakeCacheTTL, getter)
|
||||
assert.NoError(t, err)
|
||||
|
||||
val, err := cache.Get("key", CacheReadTypeDefault)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, getError, err)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestCacheDelete(t *testing.T) {
|
||||
key := "key1"
|
||||
val := &fakeDataObj{}
|
||||
data := map[string]*fakeDataObj{
|
||||
key: val,
|
||||
}
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(data)
|
||||
|
||||
v, err := cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, val, v, "cache should get correct data")
|
||||
|
||||
dataSource.set(nil)
|
||||
cache.Delete(key)
|
||||
v, err = cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, nil, v, "cache should get nil after data is removed")
|
||||
}
|
||||
|
||||
func TestCacheExpired(t *testing.T) {
|
||||
key := "key1"
|
||||
val := &fakeDataObj{}
|
||||
data := map[string]*fakeDataObj{
|
||||
key: val,
|
||||
}
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(data)
|
||||
|
||||
v, err := cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should get correct data")
|
||||
|
||||
time.Sleep(fakeCacheTTL)
|
||||
v, err = cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should get correct data even after expired")
|
||||
}
|
||||
|
||||
func TestCacheAllowUnsafeRead(t *testing.T) {
|
||||
key := "key1"
|
||||
val := &fakeDataObj{}
|
||||
data := map[string]*fakeDataObj{
|
||||
key: val,
|
||||
}
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(data)
|
||||
|
||||
v, err := cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should get correct data")
|
||||
|
||||
time.Sleep(fakeCacheTTL)
|
||||
v, err = cache.Get(key, CacheReadTypeUnsafe)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should return expired as allow unsafe read is allowed")
|
||||
}
|
||||
|
||||
func TestCacheNoConcurrentGet(t *testing.T) {
|
||||
key := "key1"
|
||||
val := &fakeDataObj{}
|
||||
data := map[string]*fakeDataObj{
|
||||
key: val,
|
||||
}
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(data)
|
||||
|
||||
time.Sleep(fakeCacheTTL)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = cache.Get(key, CacheReadTypeDefault)
|
||||
}()
|
||||
}
|
||||
v, err := cache.Get(key, CacheReadTypeDefault)
|
||||
wg.Wait()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should get correct data")
|
||||
}
|
||||
|
||||
func TestCacheForceRefresh(t *testing.T) {
|
||||
key := "key1"
|
||||
val := &fakeDataObj{}
|
||||
data := map[string]*fakeDataObj{
|
||||
key: val,
|
||||
}
|
||||
dataSource, cache := newFakeCache(t)
|
||||
dataSource.set(data)
|
||||
|
||||
v, err := cache.Get(key, CacheReadTypeDefault)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, dataSource.called)
|
||||
assert.Equal(t, val, v, "cache should get correct data")
|
||||
|
||||
v, err = cache.Get(key, CacheReadTypeForceRefresh)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, dataSource.called)
|
||||
assert.Equal(t, val, v, "should refetch unexpired data as forced refresh")
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package cache is an implementation of Azure caches.
|
||||
package cache // import "k8s.io/legacy-cloud-providers/azure/cache"
|
||||
@@ -1,701 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package armclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
|
||||
"k8s.io/client-go/pkg/version"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
var _ Interface = &Client{}
|
||||
|
||||
// Client implements ARM client Interface.
|
||||
type Client struct {
|
||||
client autorest.Client
|
||||
backoff *retry.Backoff
|
||||
|
||||
baseURI string
|
||||
apiVersion string
|
||||
clientRegion string
|
||||
}
|
||||
|
||||
// New creates a ARM client
|
||||
func New(authorizer autorest.Authorizer, baseURI, userAgent, apiVersion, clientRegion string, clientBackoff *retry.Backoff) *Client {
|
||||
restClient := autorest.NewClientWithUserAgent(userAgent)
|
||||
restClient.PollingDelay = 5 * time.Second
|
||||
restClient.RetryAttempts = 3
|
||||
restClient.RetryDuration = time.Second * 1
|
||||
restClient.Authorizer = authorizer
|
||||
|
||||
if userAgent == "" {
|
||||
restClient.UserAgent = GetUserAgent(restClient)
|
||||
}
|
||||
|
||||
backoff := clientBackoff
|
||||
if backoff == nil {
|
||||
backoff = &retry.Backoff{}
|
||||
}
|
||||
if backoff.Steps == 0 {
|
||||
// 1 steps means no retry.
|
||||
backoff.Steps = 1
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: restClient,
|
||||
baseURI: baseURI,
|
||||
backoff: backoff,
|
||||
apiVersion: apiVersion,
|
||||
clientRegion: NormalizeAzureRegion(clientRegion),
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAgent gets the autorest client with a user agent that
|
||||
// includes "kubernetes" and the full kubernetes git version string
|
||||
// example:
|
||||
// Azure-SDK-for-Go/7.0.1 arm-network/2016-09-01; kubernetes-cloudprovider/v1.17.0;
|
||||
func GetUserAgent(client autorest.Client) string {
|
||||
k8sVersion := version.Get().GitVersion
|
||||
return fmt.Sprintf("%s; kubernetes-cloudprovider/%s", client.UserAgent, k8sVersion)
|
||||
}
|
||||
|
||||
// NormalizeAzureRegion returns a normalized Azure region with white spaces removed and converted to lower case
|
||||
func NormalizeAzureRegion(name string) string {
|
||||
region := ""
|
||||
for _, runeValue := range name {
|
||||
if !unicode.IsSpace(runeValue) {
|
||||
region += string(runeValue)
|
||||
}
|
||||
}
|
||||
return strings.ToLower(region)
|
||||
}
|
||||
|
||||
// sendRequest sends a http request to ARM service.
|
||||
// Although Azure SDK supports retries per https://github.com/azure/azure-sdk-for-go#request-retry-policy, we
|
||||
// disable it since we want to fully control the retry policies.
|
||||
func (c *Client) sendRequest(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) {
|
||||
sendBackoff := *c.backoff
|
||||
response, err := autorest.SendWithSender(
|
||||
c.client,
|
||||
request,
|
||||
retry.DoExponentialBackoffRetry(&sendBackoff),
|
||||
)
|
||||
|
||||
if response == nil && err == nil {
|
||||
return response, retry.NewError(false, fmt.Errorf("Empty response and no HTTP code"))
|
||||
}
|
||||
|
||||
return response, retry.GetError(response, err)
|
||||
}
|
||||
|
||||
// Send sends a http request to ARM service with possible retry to regional ARM endpoint.
|
||||
func (c *Client) Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) {
|
||||
response, rerr := c.sendRequest(ctx, request)
|
||||
if rerr != nil {
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusNotFound || c.clientRegion == "" {
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
||||
defer func() {
|
||||
response.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}()
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
klog.V(5).Infof("Send.sendRequest original error message: %s", bodyString)
|
||||
|
||||
// Hack: retry the regional ARM endpoint in case of ARM traffic split and arm resource group replication is too slow
|
||||
var body map[string]interface{}
|
||||
if e := json.Unmarshal(bodyBytes, &body); e != nil {
|
||||
klog.V(5).Infof("Send.sendRequest: error in parsing response body string: %s, Skip retrying regional host", e)
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
if err, ok := body["error"].(map[string]interface{}); !ok ||
|
||||
err["code"] == nil ||
|
||||
!strings.EqualFold(err["code"].(string), "ResourceGroupNotFound") {
|
||||
klog.V(5).Infof("Send.sendRequest: response body does not contain ResourceGroupNotFound error code. Skip retrying regional host")
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
currentHost := request.URL.Host
|
||||
if request.Host != "" {
|
||||
currentHost = request.Host
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(currentHost), c.clientRegion) {
|
||||
klog.V(5).Infof("Send.sendRequest: current host %s is regional host. Skip retrying regional host.", currentHost)
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
request.Host = fmt.Sprintf("%s.%s", c.clientRegion, strings.ToLower(currentHost))
|
||||
klog.V(5).Infof("Send.sendRegionalRequest on ResourceGroupNotFound error. Retrying regional host: %s", request.Host)
|
||||
regionalResponse, regionalError := c.sendRequest(ctx, request)
|
||||
|
||||
// only use the result if the regional request actually goes through and returns 2xx status code, for two reasons:
|
||||
// 1. the retry on regional ARM host approach is a hack.
|
||||
// 2. the concatenated regional uri could be wrong as the rule is not officially declared by ARM.
|
||||
if regionalResponse == nil || regionalResponse.StatusCode > 299 {
|
||||
regionalErrStr := ""
|
||||
if regionalError != nil {
|
||||
regionalErrStr = regionalError.Error().Error()
|
||||
}
|
||||
|
||||
klog.V(5).Infof("Send.sendRegionalRequest failed to get response from regional host, error: '%s'. Ignoring the result.", regionalErrStr)
|
||||
return response, rerr
|
||||
}
|
||||
|
||||
return regionalResponse, regionalError
|
||||
}
|
||||
|
||||
// PreparePutRequest prepares put request
|
||||
func (c *Client) PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsContentType("application/json; charset=utf-8"),
|
||||
autorest.AsPut(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// PreparePatchRequest prepares patch request
|
||||
func (c *Client) PreparePatchRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsContentType("application/json; charset=utf-8"),
|
||||
autorest.AsPatch(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// PreparePostRequest prepares post request
|
||||
func (c *Client) PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsContentType("application/json; charset=utf-8"),
|
||||
autorest.AsPost(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// PrepareGetRequest prepares get request
|
||||
func (c *Client) PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsGet(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// PrepareDeleteRequest preparse delete request
|
||||
func (c *Client) PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsDelete(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// PrepareHeadRequest prepares head request
|
||||
func (c *Client) PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
[]autorest.PrepareDecorator{
|
||||
autorest.AsHead(),
|
||||
autorest.WithBaseURL(c.baseURI)},
|
||||
decorators...)
|
||||
return c.prepareRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationCompletion waits for an operation completion
|
||||
func (c *Client) WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error {
|
||||
err := future.WaitForCompletionRef(ctx, c.client)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in WaitForCompletionRef: '%v'", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var done bool
|
||||
done, err = future.DoneWithContext(ctx, c.client)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in DoneWithContext: '%v'", err)
|
||||
return autorest.NewErrorWithError(err, asyncOperationName, "Result", future.Response(), "Polling failure")
|
||||
}
|
||||
if !done {
|
||||
return azure.NewAsyncOpIncompleteError(asyncOperationName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationResult waits for an operation result.
|
||||
func (c *Client) WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error) {
|
||||
err := c.WaitForAsyncOperationCompletion(ctx, future, asyncOperationName)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationCompletion: '%v'", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sendBackoff := *c.backoff
|
||||
sender := autorest.DecorateSender(
|
||||
c.client,
|
||||
retry.DoExponentialBackoffRetry(&sendBackoff),
|
||||
)
|
||||
return future.GetResult(sender)
|
||||
}
|
||||
|
||||
// SendAsync send a request and return a future object representing the async result as well as the origin http response
|
||||
func (c *Client) SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error) {
|
||||
asyncResponse, rerr := c.Send(ctx, request)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.send", request.URL.String(), rerr.Error())
|
||||
return nil, nil, rerr
|
||||
}
|
||||
|
||||
future, err := azure.NewFutureFromResponse(asyncResponse)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "sendAsync.respond", request.URL.String(), err)
|
||||
return nil, asyncResponse, retry.GetError(asyncResponse, err)
|
||||
}
|
||||
|
||||
return &future, asyncResponse, nil
|
||||
}
|
||||
|
||||
// GetResource get a resource by resource ID
|
||||
func (c *Client) GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error) {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
}
|
||||
if expand != "" {
|
||||
queryParameters := map[string]interface{}{
|
||||
"$expand": autorest.Encode("query", expand),
|
||||
}
|
||||
decorators = append(decorators, autorest.WithQueryParameters(queryParameters))
|
||||
}
|
||||
request, err := c.PrepareGetRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "get.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
return c.Send(ctx, request)
|
||||
}
|
||||
|
||||
// GetResourceWithDecorators get a resource with decorators by resource ID
|
||||
func (c *Client) GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
|
||||
getDecorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
}
|
||||
getDecorators = append(getDecorators, decorators...)
|
||||
request, err := c.PrepareGetRequest(ctx, getDecorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "get.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
return c.Send(ctx, request)
|
||||
}
|
||||
|
||||
// PutResource puts a resource by resource ID
|
||||
func (c *Client) PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
putDecorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
return c.PutResourceWithDecorators(ctx, resourceID, parameters, putDecorators)
|
||||
}
|
||||
|
||||
// PutResources puts a list of resources from resources map[resourceID]parameters.
|
||||
// Those resources sync requests are sequential while async requests are concurrent. It's especially
|
||||
// useful when the ARM API doesn't support concurrent requests.
|
||||
func (c *Client) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*PutResourcesResponse {
|
||||
if len(resources) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sequential sync requests.
|
||||
futures := make(map[string]*azure.Future)
|
||||
responses := make(map[string]*PutResourcesResponse)
|
||||
for resourceID, parameters := range resources {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
request, err := c.PreparePutRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err)
|
||||
responses[resourceID] = &PutResourcesResponse{
|
||||
Error: retry.NewError(false, err),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
future, resp, clientErr := c.SendAsync(ctx, request)
|
||||
defer c.CloseResponse(ctx, resp)
|
||||
if clientErr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, clientErr.Error())
|
||||
responses[resourceID] = &PutResourcesResponse{
|
||||
Error: clientErr,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
futures[resourceID] = future
|
||||
}
|
||||
|
||||
// Concurrent async requests.
|
||||
wg := sync.WaitGroup{}
|
||||
var responseLock sync.Mutex
|
||||
for resourceID, future := range futures {
|
||||
wg.Add(1)
|
||||
go func(resourceID string, future *azure.Future) {
|
||||
defer wg.Done()
|
||||
response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PutResource")
|
||||
if err != nil {
|
||||
if response != nil {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode)
|
||||
} else {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error())
|
||||
}
|
||||
|
||||
retriableErr := retry.GetError(response, err)
|
||||
if !retriableErr.Retriable &&
|
||||
strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) {
|
||||
klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error())
|
||||
retriableErr.Retriable = true
|
||||
}
|
||||
|
||||
responseLock.Lock()
|
||||
responses[resourceID] = &PutResourcesResponse{
|
||||
Error: retriableErr,
|
||||
}
|
||||
responseLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
responseLock.Lock()
|
||||
responses[resourceID] = &PutResourcesResponse{
|
||||
Response: response,
|
||||
}
|
||||
responseLock.Unlock()
|
||||
}(resourceID, future)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return responses
|
||||
}
|
||||
|
||||
// PutResourceWithDecorators puts a resource by resource ID
|
||||
func (c *Client) PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
|
||||
request, err := c.PreparePutRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
future, resp, clientErr := c.SendAsync(ctx, request)
|
||||
defer c.CloseResponse(ctx, resp)
|
||||
if clientErr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, clientErr.Error())
|
||||
return nil, clientErr
|
||||
}
|
||||
|
||||
response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PutResource")
|
||||
if err != nil {
|
||||
if response != nil {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode)
|
||||
} else {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error())
|
||||
}
|
||||
|
||||
retriableErr := retry.GetError(response, err)
|
||||
if !retriableErr.Retriable &&
|
||||
strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) {
|
||||
klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error())
|
||||
retriableErr.Retriable = true
|
||||
}
|
||||
return nil, retriableErr
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// PatchResource patches a resource by resource ID
|
||||
func (c *Client) PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
|
||||
request, err := c.PreparePatchRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "patch.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
future, resp, clientErr := c.SendAsync(ctx, request)
|
||||
defer c.CloseResponse(ctx, resp)
|
||||
if clientErr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "patch.send", resourceID, clientErr.Error())
|
||||
return nil, clientErr
|
||||
}
|
||||
|
||||
response, err := c.WaitForAsyncOperationResult(ctx, future, "armclient.PatchResource")
|
||||
if err != nil {
|
||||
if response != nil {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', response code %d", err.Error(), response.StatusCode)
|
||||
} else {
|
||||
klog.V(5).Infof("Received error in WaitForAsyncOperationResult: '%s', no response", err.Error())
|
||||
}
|
||||
|
||||
retriableErr := retry.GetError(response, err)
|
||||
if !retriableErr.Retriable &&
|
||||
strings.Contains(strings.ToUpper(err.Error()), strings.ToUpper("InternalServerError")) {
|
||||
klog.V(5).Infof("Received InternalServerError in WaitForAsyncOperationResult: '%s', setting error retriable", err.Error())
|
||||
retriableErr.Retriable = true
|
||||
}
|
||||
return nil, retriableErr
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// PutResourceAsync puts a resource by resource ID in async mode
|
||||
func (c *Client) PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error) {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
|
||||
request, err := c.PreparePutRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
future, resp, rErr := c.SendAsync(ctx, request)
|
||||
defer c.CloseResponse(ctx, resp)
|
||||
if rErr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "put.send", resourceID, err)
|
||||
return nil, rErr
|
||||
}
|
||||
|
||||
return future, nil
|
||||
}
|
||||
|
||||
// PostResource posts a resource by resource ID
|
||||
func (c *Client) PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceID": resourceID,
|
||||
"action": action,
|
||||
}
|
||||
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}/{action}", pathParameters),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
request, err := c.PreparePostRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "post.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
return c.sendRequest(ctx, request)
|
||||
}
|
||||
|
||||
// DeleteResource deletes a resource by resource ID
|
||||
func (c *Client) DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error {
|
||||
future, clientErr := c.DeleteResourceAsync(ctx, resourceID, ifMatch)
|
||||
if clientErr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "delete.request", resourceID, clientErr.Error())
|
||||
return clientErr
|
||||
}
|
||||
|
||||
if future == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.WaitForAsyncOperationCompletion(ctx, future, "armclient.DeleteResource"); err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "delete.wait", resourceID, clientErr.Error())
|
||||
return retry.NewError(true, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HeadResource heads a resource by resource ID
|
||||
func (c *Client) HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error) {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
}
|
||||
request, err := c.PrepareHeadRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "head.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
return c.sendRequest(ctx, request)
|
||||
}
|
||||
|
||||
// DeleteResourceAsync delete a resource by resource ID and returns a future representing the async result
|
||||
func (c *Client) DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error) {
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
}
|
||||
if len(ifMatch) > 0 {
|
||||
decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(ifMatch)))
|
||||
}
|
||||
|
||||
deleteRequest, err := c.PrepareDeleteRequest(ctx, decorators...)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.prepare", resourceID, err)
|
||||
return nil, retry.NewError(false, err)
|
||||
}
|
||||
|
||||
resp, rerr := c.sendRequest(ctx, deleteRequest)
|
||||
defer c.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.send", resourceID, rerr.Error())
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
err = autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent, http.StatusNotFound))
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.respond", resourceID, err)
|
||||
return nil, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
future, err := azure.NewFutureFromResponse(resp)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deleteAsync.future", resourceID, err)
|
||||
return nil, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
return &future, nil
|
||||
}
|
||||
|
||||
// CloseResponse closes a response
|
||||
func (c *Client) CloseResponse(ctx context.Context, response *http.Response) {
|
||||
if response != nil && response.Body != nil {
|
||||
if err := response.Body.Close(); err != nil {
|
||||
klog.Errorf("Error closing the response body: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) prepareRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
decorators = append(
|
||||
decorators,
|
||||
withAPIVersion(c.apiVersion))
|
||||
preparer := autorest.CreatePreparer(decorators...)
|
||||
return preparer.Prepare((&http.Request{}).WithContext(ctx))
|
||||
}
|
||||
|
||||
func withAPIVersion(apiVersion string) autorest.PrepareDecorator {
|
||||
const apiVersionKey = "api-version"
|
||||
return func(p autorest.Preparer) autorest.Preparer {
|
||||
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||
r, err := p.Prepare(r)
|
||||
if err == nil {
|
||||
if r.URL == nil {
|
||||
return r, fmt.Errorf("Error in withAPIVersion: Invoked with a nil URL")
|
||||
}
|
||||
|
||||
v := r.URL.Query()
|
||||
if len(v.Get(apiVersionKey)) > 0 {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
v.Add(apiVersionKey, apiVersion)
|
||||
r.URL.RawQuery = v.Encode()
|
||||
}
|
||||
return r, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetResourceID gets Azure resource ID
|
||||
func GetResourceID(subscriptionID, resourceGroupName, resourceType, resourceName string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s",
|
||||
autorest.Encode("path", subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName),
|
||||
resourceType,
|
||||
autorest.Encode("path", resourceName))
|
||||
}
|
||||
|
||||
// GetChildResourceID gets Azure child resource ID
|
||||
func GetChildResourceID(subscriptionID, resourceGroupName, resourceType, resourceName, childResourceType, childResourceName string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s/%s",
|
||||
autorest.Encode("path", subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName),
|
||||
resourceType,
|
||||
autorest.Encode("path", resourceName),
|
||||
childResourceType,
|
||||
autorest.Encode("path", childResourceName))
|
||||
}
|
||||
|
||||
// GetChildResourcesListID gets Azure child resources list ID
|
||||
func GetChildResourcesListID(subscriptionID, resourceGroupName, resourceType, resourceName, childResourceType string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s",
|
||||
autorest.Encode("path", subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName),
|
||||
resourceType,
|
||||
autorest.Encode("path", resourceName),
|
||||
childResourceType)
|
||||
}
|
||||
|
||||
// GetProviderResourceID gets Azure RP resource ID
|
||||
func GetProviderResourceID(subscriptionID, providerNamespace string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/providers/%s",
|
||||
autorest.Encode("path", subscriptionID),
|
||||
providerNamespace)
|
||||
}
|
||||
|
||||
// GetProviderResourcesListID gets Azure RP resources list ID
|
||||
func GetProviderResourcesListID(subscriptionID string) string {
|
||||
return fmt.Sprintf("/subscriptions/%s/providers", autorest.Encode("path", subscriptionID))
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package armclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
const (
|
||||
testResourceID = "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, "", "test", "2019-01-01", "eastus", backoff)
|
||||
assert.NotNil(t, armClient.backoff)
|
||||
assert.Equal(t, 3, armClient.backoff.Steps, "Backoff steps should be same as the value passed in")
|
||||
|
||||
backoff = &retry.Backoff{Steps: 0}
|
||||
armClient = New(nil, "", "test", "2019-01-01", "eastus", backoff)
|
||||
assert.NotNil(t, armClient.backoff)
|
||||
assert.Equal(t, 1, armClient.backoff.Steps, "Backoff steps should be default to 1 if it is 0")
|
||||
|
||||
armClient = New(nil, "", "test", "2019-01-01", "eastus", nil)
|
||||
assert.NotNil(t, armClient.backoff)
|
||||
assert.Equal(t, 1, armClient.backoff.Steps, "Backoff steps should be default to 1 if it is not set")
|
||||
}
|
||||
|
||||
func TestSend(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if count <= 1 {
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
count++
|
||||
}
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceGroupName": autorest.Encode("path", "testgroup"),
|
||||
"subscriptionId": autorest.Encode("path", "testid"),
|
||||
"resourceName": autorest.Encode("path", "testname"),
|
||||
}
|
||||
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters(
|
||||
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request, err := armClient.PrepareGetRequest(ctx, decorators...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, rerr := armClient.Send(ctx, request)
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, http.StatusOK, response.StatusCode)
|
||||
}
|
||||
|
||||
func TestSendFailure(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
count++
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceGroupName": autorest.Encode("path", "testgroup"),
|
||||
"subscriptionId": autorest.Encode("path", "testid"),
|
||||
"resourceName": autorest.Encode("path", "testname"),
|
||||
}
|
||||
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters(
|
||||
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request, err := armClient.PreparePatchRequest(ctx, decorators...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, rerr := armClient.Send(ctx, request)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Equal(t, http.StatusInternalServerError, response.StatusCode)
|
||||
}
|
||||
|
||||
func TestSendThrottled(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(retry.RetryAfterHeaderKey, "30")
|
||||
http.Error(w, "failed", http.StatusTooManyRequests)
|
||||
count++
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceGroupName": autorest.Encode("path", "testgroup"),
|
||||
"subscriptionId": autorest.Encode("path", "testid"),
|
||||
"resourceName": autorest.Encode("path", "testname"),
|
||||
}
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters(
|
||||
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request, err := armClient.PrepareGetRequest(ctx, decorators...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, rerr := armClient.Send(ctx, request)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Equal(t, http.StatusTooManyRequests, response.StatusCode)
|
||||
}
|
||||
|
||||
func TestSendAsync(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusForbidden)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceGroupName": autorest.Encode("path", "testgroup"),
|
||||
"subscriptionId": autorest.Encode("path", "testid"),
|
||||
"resourceName": autorest.Encode("path", "testname"),
|
||||
}
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters(
|
||||
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request, err := armClient.PreparePutRequest(ctx, decorators...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
future, response, rerr := armClient.SendAsync(ctx, request)
|
||||
assert.Nil(t, future)
|
||||
assert.Nil(t, response)
|
||||
assert.Equal(t, 1, count)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, false, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestSendAsyncSuccess(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
pathParameters := map[string]interface{}{
|
||||
"resourceGroupName": autorest.Encode("path", "testgroup"),
|
||||
"subscriptionId": autorest.Encode("path", "testid"),
|
||||
"resourceName": autorest.Encode("path", "testname"),
|
||||
}
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters(
|
||||
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/vNets/{resourceName}", pathParameters),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
request, err := armClient.PreparePostRequest(ctx, decorators...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
future, response, rerr := armClient.SendAsync(ctx, request)
|
||||
assert.Nil(t, rerr)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotNil(t, future)
|
||||
}
|
||||
|
||||
func TestNormalizeAzureRegion(t *testing.T) {
|
||||
tests := []struct {
|
||||
region string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
region: "eastus",
|
||||
expected: "eastus",
|
||||
},
|
||||
{
|
||||
region: " eastus ",
|
||||
expected: "eastus",
|
||||
},
|
||||
{
|
||||
region: " eastus\t",
|
||||
expected: "eastus",
|
||||
},
|
||||
{
|
||||
region: " eastus\v",
|
||||
expected: "eastus",
|
||||
},
|
||||
{
|
||||
region: " eastus\v\r\f\n",
|
||||
expected: "eastus",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
real := NormalizeAzureRegion(test.region)
|
||||
assert.Equal(t, test.expected, real, "test[%d]: NormalizeAzureRegion(%q) != %q", i, test.region, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResource(t *testing.T) {
|
||||
expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?%24expand=data&api-version=2019-01-01"
|
||||
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, expectedURI, r.URL.String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{data: testPIP}"))
|
||||
count++
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
response, rerr := armClient.GetResource(ctx, testResourceID, "data")
|
||||
byteResponseBody, _ := ioutil.ReadAll(response.Body)
|
||||
stringResponseBody := string(byteResponseBody)
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, "{data: testPIP}", stringResponseBody)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestGetResourceWithDecorators(t *testing.T) {
|
||||
expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01¶m1=value1¶m2=value2"
|
||||
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, expectedURI, r.URL.String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{data: testPIP}"))
|
||||
count++
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
params := map[string]interface{}{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
}
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithQueryParameters(params),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
response, rerr := armClient.GetResourceWithDecorators(ctx, testResourceID, decorators)
|
||||
byteResponseBody, _ := ioutil.ReadAll(response.Body)
|
||||
stringResponseBody := string(byteResponseBody)
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, "{data: testPIP}", stringResponseBody)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestPutResource(t *testing.T) {
|
||||
expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01"
|
||||
operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01"
|
||||
handlers := []func(http.ResponseWriter, *http.Request){
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
assert.Equal(t, expectedURI, req.URL.String())
|
||||
rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"),
|
||||
fmt.Sprintf("http://%s%s", req.Host, operationURI))
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
},
|
||||
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
assert.Equal(t, operationURI, req.URL.String())
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`))
|
||||
},
|
||||
}
|
||||
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers[count](w, r)
|
||||
count++
|
||||
if count > 1 {
|
||||
count = 1
|
||||
}
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
response, rerr := armClient.PutResource(ctx, testResourceID, nil)
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Nil(t, response)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestPutResources(t *testing.T) {
|
||||
serverFuncs := []func(rw http.ResponseWriter, req *http.Request){
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
|
||||
rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"),
|
||||
fmt.Sprintf("http://%s%s", req.Host, "/id/1?api-version=2019-01-01"))
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
},
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
|
||||
rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"),
|
||||
fmt.Sprintf("http://%s%s", req.Host, "/id/2?api-version=2019-01-01"))
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
},
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`))
|
||||
},
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`))
|
||||
},
|
||||
}
|
||||
|
||||
i, total := 0, 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
serverFuncs[i](w, r)
|
||||
i++
|
||||
if i > 3 {
|
||||
i = 3
|
||||
}
|
||||
total++
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resources := map[string]interface{}{
|
||||
"/id/1": nil,
|
||||
"/id/2": nil,
|
||||
}
|
||||
responses := armClient.PutResources(ctx, nil)
|
||||
assert.Nil(t, responses)
|
||||
responses = armClient.PutResources(ctx, resources)
|
||||
assert.NotNil(t, responses)
|
||||
assert.Equal(t, 3, total)
|
||||
}
|
||||
|
||||
func TestPutResourceAsync(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resourceID := testResourceID
|
||||
future, rerr := armClient.PutResourceAsync(ctx, resourceID, "")
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Nil(t, future)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestDeleteResourceAsync(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resourceID := testResourceID
|
||||
future, rerr := armClient.DeleteResourceAsync(ctx, resourceID, "")
|
||||
assert.Equal(t, 3, count)
|
||||
assert.Nil(t, future)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestPatchResource(t *testing.T) {
|
||||
expectedURI := "/subscriptions/subscription/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/testPIP?api-version=2019-01-01"
|
||||
operationURI := "/subscriptions/subscription/providers/Microsoft.Network/locations/eastus/operations/op?api-version=2019-01-01"
|
||||
handlers := []func(http.ResponseWriter, *http.Request){
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "PATCH", req.Method)
|
||||
assert.Equal(t, expectedURI, req.URL.String())
|
||||
rw.Header().Set(http.CanonicalHeaderKey("Azure-AsyncOperation"),
|
||||
fmt.Sprintf("http://%s%s", req.Host, operationURI))
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
},
|
||||
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
assert.Equal(t, operationURI, req.URL.String())
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"error":{"code":"InternalServerError"},"status":"Failed"}`))
|
||||
},
|
||||
}
|
||||
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers[count](w, r)
|
||||
count++
|
||||
if count > 1 {
|
||||
count = 1
|
||||
}
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 1}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
response, rerr := armClient.PatchResource(ctx, testResourceID, nil)
|
||||
assert.Equal(t, 1, count)
|
||||
assert.Nil(t, response)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestPostResource(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resourceID := testResourceID
|
||||
future, rerr := armClient.PostResource(ctx, resourceID, "post", "")
|
||||
assert.Equal(t, 3, count)
|
||||
assert.NotNil(t, future)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestDeleteResource(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resourceID := testResourceID
|
||||
rerr := armClient.DeleteResource(ctx, resourceID, "")
|
||||
assert.Equal(t, 3, count)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestHeadResource(t *testing.T) {
|
||||
count := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count++
|
||||
http.Error(w, "failed", http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
backoff := &retry.Backoff{Steps: 3}
|
||||
armClient := New(nil, server.URL, "test", "2019-01-01", "eastus", backoff)
|
||||
armClient.client.RetryDuration = time.Millisecond * 1
|
||||
|
||||
ctx := context.Background()
|
||||
resourceID := testResourceID
|
||||
response, rerr := armClient.HeadResource(ctx, resourceID)
|
||||
assert.Equal(t, 3, count)
|
||||
assert.NotNil(t, response)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, true, rerr.Retriable)
|
||||
}
|
||||
|
||||
func TestGetResourceID(t *testing.T) {
|
||||
expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name"
|
||||
|
||||
resourceID := GetResourceID("sub", "rg", "type", "name")
|
||||
assert.Equal(t, expectedResourceID, resourceID)
|
||||
}
|
||||
|
||||
func TestGetChildResourceID(t *testing.T) {
|
||||
expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2/name-3"
|
||||
|
||||
resourceID := GetChildResourceID("sub", "rg", "type", "name-1", "name-2", "name-3")
|
||||
assert.Equal(t, expectedResourceID, resourceID)
|
||||
}
|
||||
|
||||
func TestGetChildResourcesListID(t *testing.T) {
|
||||
expectedResourceID := "/subscriptions/sub/resourceGroups/rg/providers/type/name-1/name-2"
|
||||
|
||||
resourceID := GetChildResourcesListID("sub", "rg", "type", "name-1", "name-2")
|
||||
assert.Equal(t, expectedResourceID, resourceID)
|
||||
}
|
||||
|
||||
func TestGetProviderResourceID(t *testing.T) {
|
||||
expectedResourceID := "/subscriptions/sub/providers/namespace"
|
||||
|
||||
resourceID := GetProviderResourceID("sub", "namespace")
|
||||
assert.Equal(t, expectedResourceID, resourceID)
|
||||
}
|
||||
|
||||
func TestGetProviderResourcesListID(t *testing.T) {
|
||||
expectedResourceID := "/subscriptions/sub/providers"
|
||||
|
||||
resourceID := GetProviderResourcesListID("sub")
|
||||
assert.Equal(t, expectedResourceID, resourceID)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package armclient implements the client for ARM.
|
||||
package armclient // import "k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
@@ -1,104 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go -destination=mockarmclient/interface.go -package=mockarmclient Interface
|
||||
package armclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
// PutResourcesResponse defines the response for PutResources.
|
||||
type PutResourcesResponse struct {
|
||||
Response *http.Response
|
||||
Error *retry.Error
|
||||
}
|
||||
|
||||
// Interface is the client interface for ARM.
|
||||
type Interface interface {
|
||||
// Send sends a http request to ARM service with possible retry to regional ARM endpoint.
|
||||
Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error)
|
||||
|
||||
// PreparePutRequest prepares put request
|
||||
PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error)
|
||||
|
||||
// PreparePostRequest prepares post request
|
||||
PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error)
|
||||
|
||||
// PrepareGetRequest prepares get request
|
||||
PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error)
|
||||
|
||||
// PrepareDeleteRequest preparse delete request
|
||||
PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error)
|
||||
|
||||
// PrepareHeadRequest prepares head request
|
||||
PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error)
|
||||
|
||||
// WaitForAsyncOperationCompletion waits for an operation completion
|
||||
WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error
|
||||
|
||||
// WaitForAsyncOperationResult waits for an operation result.
|
||||
WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error)
|
||||
|
||||
// SendAsync send a request and return a future object representing the async result as well as the origin http response
|
||||
SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error)
|
||||
|
||||
// PutResource puts a resource by resource ID
|
||||
PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error)
|
||||
|
||||
// PutResources puts a list of resources from resources map[resourceID]parameters.
|
||||
// Those resources sync requests are sequential while async requests are concurrent. It 's especially
|
||||
// useful when the ARM API doesn't support concurrent requests.
|
||||
PutResources(ctx context.Context, resources map[string]interface{}) map[string]*PutResourcesResponse
|
||||
|
||||
// PutResourceWithDecorators puts a resource with decorators by resource ID
|
||||
PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error)
|
||||
|
||||
// PatchResource patches a resource by resource ID
|
||||
PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error)
|
||||
|
||||
// PutResourceAsync puts a resource by resource ID in async mode
|
||||
PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error)
|
||||
|
||||
// HeadResource heads a resource by resource ID
|
||||
HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error)
|
||||
|
||||
// GetResource get a resource by resource ID
|
||||
GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error)
|
||||
|
||||
//GetResourceWithDecorators get a resource with decorators by resource ID
|
||||
GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error)
|
||||
|
||||
// PostResource posts a resource by resource ID
|
||||
PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error)
|
||||
|
||||
// DeleteResource deletes a resource by resource ID
|
||||
DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error
|
||||
|
||||
// DeleteResourceAsync delete a resource by resource ID and returns a future representing the async result
|
||||
DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error)
|
||||
|
||||
// CloseResponse closes a response
|
||||
CloseResponse(ctx context.Context, response *http.Response)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package mockarmclient implements the mock client for ARM.
|
||||
package mockarmclient // import "k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient"
|
||||
@@ -1,394 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interface.go
|
||||
|
||||
// Package mockarmclient is a generated GoMock package.
|
||||
package mockarmclient
|
||||
|
||||
import (
|
||||
context "context"
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
|
||||
autorest "github.com/Azure/go-autorest/autorest"
|
||||
azure "github.com/Azure/go-autorest/autorest/azure"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
armclient "k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
retry "k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CloseResponse mocks base method.
|
||||
func (m *MockInterface) CloseResponse(ctx context.Context, response *http.Response) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "CloseResponse", ctx, response)
|
||||
}
|
||||
|
||||
// CloseResponse indicates an expected call of CloseResponse.
|
||||
func (mr *MockInterfaceMockRecorder) CloseResponse(ctx, response interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseResponse", reflect.TypeOf((*MockInterface)(nil).CloseResponse), ctx, response)
|
||||
}
|
||||
|
||||
// DeleteResource mocks base method.
|
||||
func (m *MockInterface) DeleteResource(ctx context.Context, resourceID, ifMatch string) *retry.Error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteResource", ctx, resourceID, ifMatch)
|
||||
ret0, _ := ret[0].(*retry.Error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteResource indicates an expected call of DeleteResource.
|
||||
func (mr *MockInterfaceMockRecorder) DeleteResource(ctx, resourceID, ifMatch interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResource", reflect.TypeOf((*MockInterface)(nil).DeleteResource), ctx, resourceID, ifMatch)
|
||||
}
|
||||
|
||||
// DeleteResourceAsync mocks base method.
|
||||
func (m *MockInterface) DeleteResourceAsync(ctx context.Context, resourceID, ifMatch string) (*azure.Future, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteResourceAsync", ctx, resourceID, ifMatch)
|
||||
ret0, _ := ret[0].(*azure.Future)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DeleteResourceAsync indicates an expected call of DeleteResourceAsync.
|
||||
func (mr *MockInterfaceMockRecorder) DeleteResourceAsync(ctx, resourceID, ifMatch interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResourceAsync", reflect.TypeOf((*MockInterface)(nil).DeleteResourceAsync), ctx, resourceID, ifMatch)
|
||||
}
|
||||
|
||||
// GetResource mocks base method.
|
||||
func (m *MockInterface) GetResource(ctx context.Context, resourceID, expand string) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetResource", ctx, resourceID, expand)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetResource indicates an expected call of GetResource.
|
||||
func (mr *MockInterfaceMockRecorder) GetResource(ctx, resourceID, expand interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResource", reflect.TypeOf((*MockInterface)(nil).GetResource), ctx, resourceID, expand)
|
||||
}
|
||||
|
||||
// GetResourceWithDecorators mocks base method.
|
||||
func (m *MockInterface) GetResourceWithDecorators(ctx context.Context, resourceID string, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetResourceWithDecorators", ctx, resourceID, decorators)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetResourceWithDecorators indicates an expected call of GetResourceWithDecorators.
|
||||
func (mr *MockInterfaceMockRecorder) GetResourceWithDecorators(ctx, resourceID, decorators interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceWithDecorators", reflect.TypeOf((*MockInterface)(nil).GetResourceWithDecorators), ctx, resourceID, decorators)
|
||||
}
|
||||
|
||||
// HeadResource mocks base method.
|
||||
func (m *MockInterface) HeadResource(ctx context.Context, resourceID string) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HeadResource", ctx, resourceID)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// HeadResource indicates an expected call of HeadResource.
|
||||
func (mr *MockInterfaceMockRecorder) HeadResource(ctx, resourceID interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadResource", reflect.TypeOf((*MockInterface)(nil).HeadResource), ctx, resourceID)
|
||||
}
|
||||
|
||||
// PatchResource mocks base method.
|
||||
func (m *MockInterface) PatchResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PatchResource", ctx, resourceID, parameters)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PatchResource indicates an expected call of PatchResource.
|
||||
func (mr *MockInterfaceMockRecorder) PatchResource(ctx, resourceID, parameters interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchResource", reflect.TypeOf((*MockInterface)(nil).PatchResource), ctx, resourceID, parameters)
|
||||
}
|
||||
|
||||
// PostResource mocks base method.
|
||||
func (m *MockInterface) PostResource(ctx context.Context, resourceID, action string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostResource", ctx, resourceID, action, parameters)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PostResource indicates an expected call of PostResource.
|
||||
func (mr *MockInterfaceMockRecorder) PostResource(ctx, resourceID, action, parameters interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostResource", reflect.TypeOf((*MockInterface)(nil).PostResource), ctx, resourceID, action, parameters)
|
||||
}
|
||||
|
||||
// PrepareDeleteRequest mocks base method.
|
||||
func (m *MockInterface) PrepareDeleteRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx}
|
||||
for _, a := range decorators {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "PrepareDeleteRequest", varargs...)
|
||||
ret0, _ := ret[0].(*http.Request)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PrepareDeleteRequest indicates an expected call of PrepareDeleteRequest.
|
||||
func (mr *MockInterfaceMockRecorder) PrepareDeleteRequest(ctx interface{}, decorators ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx}, decorators...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareDeleteRequest", reflect.TypeOf((*MockInterface)(nil).PrepareDeleteRequest), varargs...)
|
||||
}
|
||||
|
||||
// PrepareGetRequest mocks base method.
|
||||
func (m *MockInterface) PrepareGetRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx}
|
||||
for _, a := range decorators {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "PrepareGetRequest", varargs...)
|
||||
ret0, _ := ret[0].(*http.Request)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PrepareGetRequest indicates an expected call of PrepareGetRequest.
|
||||
func (mr *MockInterfaceMockRecorder) PrepareGetRequest(ctx interface{}, decorators ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx}, decorators...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareGetRequest", reflect.TypeOf((*MockInterface)(nil).PrepareGetRequest), varargs...)
|
||||
}
|
||||
|
||||
// PrepareHeadRequest mocks base method.
|
||||
func (m *MockInterface) PrepareHeadRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx}
|
||||
for _, a := range decorators {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "PrepareHeadRequest", varargs...)
|
||||
ret0, _ := ret[0].(*http.Request)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PrepareHeadRequest indicates an expected call of PrepareHeadRequest.
|
||||
func (mr *MockInterfaceMockRecorder) PrepareHeadRequest(ctx interface{}, decorators ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx}, decorators...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareHeadRequest", reflect.TypeOf((*MockInterface)(nil).PrepareHeadRequest), varargs...)
|
||||
}
|
||||
|
||||
// PreparePostRequest mocks base method.
|
||||
func (m *MockInterface) PreparePostRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx}
|
||||
for _, a := range decorators {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "PreparePostRequest", varargs...)
|
||||
ret0, _ := ret[0].(*http.Request)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PreparePostRequest indicates an expected call of PreparePostRequest.
|
||||
func (mr *MockInterfaceMockRecorder) PreparePostRequest(ctx interface{}, decorators ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx}, decorators...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePostRequest", reflect.TypeOf((*MockInterface)(nil).PreparePostRequest), varargs...)
|
||||
}
|
||||
|
||||
// PreparePutRequest mocks base method.
|
||||
func (m *MockInterface) PreparePutRequest(ctx context.Context, decorators ...autorest.PrepareDecorator) (*http.Request, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{ctx}
|
||||
for _, a := range decorators {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "PreparePutRequest", varargs...)
|
||||
ret0, _ := ret[0].(*http.Request)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PreparePutRequest indicates an expected call of PreparePutRequest.
|
||||
func (mr *MockInterfaceMockRecorder) PreparePutRequest(ctx interface{}, decorators ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{ctx}, decorators...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePutRequest", reflect.TypeOf((*MockInterface)(nil).PreparePutRequest), varargs...)
|
||||
}
|
||||
|
||||
// PutResource mocks base method.
|
||||
func (m *MockInterface) PutResource(ctx context.Context, resourceID string, parameters interface{}) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PutResource", ctx, resourceID, parameters)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PutResource indicates an expected call of PutResource.
|
||||
func (mr *MockInterfaceMockRecorder) PutResource(ctx, resourceID, parameters interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResource", reflect.TypeOf((*MockInterface)(nil).PutResource), ctx, resourceID, parameters)
|
||||
}
|
||||
|
||||
// PutResourceAsync mocks base method.
|
||||
func (m *MockInterface) PutResourceAsync(ctx context.Context, resourceID string, parameters interface{}) (*azure.Future, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PutResourceAsync", ctx, resourceID, parameters)
|
||||
ret0, _ := ret[0].(*azure.Future)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PutResourceAsync indicates an expected call of PutResourceAsync.
|
||||
func (mr *MockInterfaceMockRecorder) PutResourceAsync(ctx, resourceID, parameters interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceAsync", reflect.TypeOf((*MockInterface)(nil).PutResourceAsync), ctx, resourceID, parameters)
|
||||
}
|
||||
|
||||
// PutResourceWithDecorators mocks base method.
|
||||
func (m *MockInterface) PutResourceWithDecorators(ctx context.Context, resourceID string, parameters interface{}, decorators []autorest.PrepareDecorator) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PutResourceWithDecorators", ctx, resourceID, parameters, decorators)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PutResourceWithDecorators indicates an expected call of PutResourceWithDecorators.
|
||||
func (mr *MockInterfaceMockRecorder) PutResourceWithDecorators(ctx, resourceID, parameters, decorators interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResourceWithDecorators", reflect.TypeOf((*MockInterface)(nil).PutResourceWithDecorators), ctx, resourceID, parameters, decorators)
|
||||
}
|
||||
|
||||
// PutResources mocks base method.
|
||||
func (m *MockInterface) PutResources(ctx context.Context, resources map[string]interface{}) map[string]*armclient.PutResourcesResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PutResources", ctx, resources)
|
||||
ret0, _ := ret[0].(map[string]*armclient.PutResourcesResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PutResources indicates an expected call of PutResources.
|
||||
func (mr *MockInterfaceMockRecorder) PutResources(ctx, resources interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutResources", reflect.TypeOf((*MockInterface)(nil).PutResources), ctx, resources)
|
||||
}
|
||||
|
||||
// Send mocks base method.
|
||||
func (m *MockInterface) Send(ctx context.Context, request *http.Request) (*http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Send", ctx, request)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Send indicates an expected call of Send.
|
||||
func (mr *MockInterfaceMockRecorder) Send(ctx, request interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockInterface)(nil).Send), ctx, request)
|
||||
}
|
||||
|
||||
// SendAsync mocks base method.
|
||||
func (m *MockInterface) SendAsync(ctx context.Context, request *http.Request) (*azure.Future, *http.Response, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendAsync", ctx, request)
|
||||
ret0, _ := ret[0].(*azure.Future)
|
||||
ret1, _ := ret[1].(*http.Response)
|
||||
ret2, _ := ret[2].(*retry.Error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// SendAsync indicates an expected call of SendAsync.
|
||||
func (mr *MockInterfaceMockRecorder) SendAsync(ctx, request interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendAsync", reflect.TypeOf((*MockInterface)(nil).SendAsync), ctx, request)
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationCompletion mocks base method.
|
||||
func (m *MockInterface) WaitForAsyncOperationCompletion(ctx context.Context, future *azure.Future, asyncOperationName string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WaitForAsyncOperationCompletion", ctx, future, asyncOperationName)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationCompletion indicates an expected call of WaitForAsyncOperationCompletion.
|
||||
func (mr *MockInterfaceMockRecorder) WaitForAsyncOperationCompletion(ctx, future, asyncOperationName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForAsyncOperationCompletion", reflect.TypeOf((*MockInterface)(nil).WaitForAsyncOperationCompletion), ctx, future, asyncOperationName)
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationResult mocks base method.
|
||||
func (m *MockInterface) WaitForAsyncOperationResult(ctx context.Context, future *azure.Future, asyncOperationName string) (*http.Response, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WaitForAsyncOperationResult", ctx, future, asyncOperationName)
|
||||
ret0, _ := ret[0].(*http.Response)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// WaitForAsyncOperationResult indicates an expected call of WaitForAsyncOperationResult.
|
||||
func (mr *MockInterfaceMockRecorder) WaitForAsyncOperationResult(ctx, future, asyncOperationName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForAsyncOperationResult", reflect.TypeOf((*MockInterface)(nil).WaitForAsyncOperationResult), ctx, future, asyncOperationName)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package clients
|
||||
|
||||
import (
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
// ClientConfig contains all essential information to create an Azure client.
|
||||
type ClientConfig struct {
|
||||
CloudName string
|
||||
Location string
|
||||
SubscriptionID string
|
||||
ResourceManagerEndpoint string
|
||||
Authorizer autorest.Authorizer
|
||||
RateLimitConfig *RateLimitConfig
|
||||
Backoff *retry.Backoff
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// WithRateLimiter returns a new ClientConfig with rateLimitConfig set.
|
||||
func (cfg *ClientConfig) WithRateLimiter(rl *RateLimitConfig) *ClientConfig {
|
||||
newClientConfig := *cfg
|
||||
newClientConfig.RateLimitConfig = rl
|
||||
return &newClientConfig
|
||||
}
|
||||
|
||||
// RateLimitConfig indicates the rate limit config options.
|
||||
type RateLimitConfig struct {
|
||||
// Enable rate limiting
|
||||
CloudProviderRateLimit bool `json:"cloudProviderRateLimit,omitempty" yaml:"cloudProviderRateLimit,omitempty"`
|
||||
// Rate limit QPS (Read)
|
||||
CloudProviderRateLimitQPS float32 `json:"cloudProviderRateLimitQPS,omitempty" yaml:"cloudProviderRateLimitQPS,omitempty"`
|
||||
// Rate limit Bucket Size
|
||||
CloudProviderRateLimitBucket int `json:"cloudProviderRateLimitBucket,omitempty" yaml:"cloudProviderRateLimitBucket,omitempty"`
|
||||
// Rate limit QPS (Write)
|
||||
CloudProviderRateLimitQPSWrite float32 `json:"cloudProviderRateLimitQPSWrite,omitempty" yaml:"cloudProviderRateLimitQPSWrite,omitempty"`
|
||||
// Rate limit Bucket Size
|
||||
CloudProviderRateLimitBucketWrite int `json:"cloudProviderRateLimitBucketWrite,omitempty" yaml:"cloudProviderRateLimitBucketWrite,omitempty"`
|
||||
}
|
||||
|
||||
// RateLimitEnabled returns true if CloudProviderRateLimit is set to true.
|
||||
func RateLimitEnabled(config *RateLimitConfig) bool {
|
||||
return config != nil && config.CloudProviderRateLimit
|
||||
}
|
||||
|
||||
// NewRateLimiter creates new read and write flowcontrol.RateLimiter from RateLimitConfig.
|
||||
func NewRateLimiter(config *RateLimitConfig) (flowcontrol.RateLimiter, flowcontrol.RateLimiter) {
|
||||
readLimiter := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
writeLimiter := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
|
||||
if config != nil && config.CloudProviderRateLimit {
|
||||
readLimiter = flowcontrol.NewTokenBucketRateLimiter(
|
||||
config.CloudProviderRateLimitQPS,
|
||||
config.CloudProviderRateLimitBucket)
|
||||
|
||||
writeLimiter = flowcontrol.NewTokenBucketRateLimiter(
|
||||
config.CloudProviderRateLimitQPSWrite,
|
||||
config.CloudProviderRateLimitBucketWrite)
|
||||
}
|
||||
|
||||
return readLimiter, writeLimiter
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package clients
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
)
|
||||
|
||||
func TestWithRateLimiter(t *testing.T) {
|
||||
config := &ClientConfig{}
|
||||
assert.Nil(t, config.RateLimitConfig)
|
||||
c := config.WithRateLimiter(&RateLimitConfig{CloudProviderRateLimit: true})
|
||||
assert.Equal(t, &RateLimitConfig{CloudProviderRateLimit: true}, c.RateLimitConfig)
|
||||
config.WithRateLimiter(nil)
|
||||
assert.Nil(t, config.RateLimitConfig)
|
||||
}
|
||||
|
||||
func TestRateLimitEnabled(t *testing.T) {
|
||||
assert.Equal(t, false, RateLimitEnabled(nil))
|
||||
config := &RateLimitConfig{}
|
||||
assert.Equal(t, false, RateLimitEnabled(config))
|
||||
config.CloudProviderRateLimit = true
|
||||
assert.Equal(t, true, RateLimitEnabled(config))
|
||||
}
|
||||
|
||||
func TestNewRateLimiter(t *testing.T) {
|
||||
fakeRateLimiter := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
readLimiter, writeLimiter := NewRateLimiter(nil)
|
||||
assert.Equal(t, readLimiter, fakeRateLimiter)
|
||||
assert.Equal(t, writeLimiter, fakeRateLimiter)
|
||||
|
||||
rateLimitConfig := &RateLimitConfig{
|
||||
CloudProviderRateLimit: false,
|
||||
}
|
||||
readLimiter, writeLimiter = NewRateLimiter(rateLimitConfig)
|
||||
assert.Equal(t, readLimiter, fakeRateLimiter)
|
||||
assert.Equal(t, writeLimiter, fakeRateLimiter)
|
||||
|
||||
rateLimitConfig = &RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitQPS: 3,
|
||||
CloudProviderRateLimitBucket: 10,
|
||||
CloudProviderRateLimitQPSWrite: 1,
|
||||
CloudProviderRateLimitBucketWrite: 3,
|
||||
}
|
||||
readLimiter, writeLimiter = NewRateLimiter(rateLimitConfig)
|
||||
assert.Equal(t, flowcontrol.NewTokenBucketRateLimiter(3, 10), readLimiter)
|
||||
assert.Equal(t, flowcontrol.NewTokenBucketRateLimiter(1, 3), writeLimiter)
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package containerserviceclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/klog/v2"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/metrics"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var _ Interface = &Client{}
|
||||
|
||||
// Client implements ContainerService client Interface.
|
||||
type Client struct {
|
||||
armClient armclient.Interface
|
||||
subscriptionID string
|
||||
|
||||
// Rate limiting configures.
|
||||
rateLimiterReader flowcontrol.RateLimiter
|
||||
rateLimiterWriter flowcontrol.RateLimiter
|
||||
|
||||
// ARM throttling configures.
|
||||
RetryAfterReader time.Time
|
||||
RetryAfterWriter time.Time
|
||||
}
|
||||
|
||||
// New creates a new ContainerServiceClient client with ratelimiting.
|
||||
func New(config *azclients.ClientConfig) *Client {
|
||||
baseURI := config.ResourceManagerEndpoint
|
||||
authorizer := config.Authorizer
|
||||
armClient := armclient.New(authorizer, baseURI, config.UserAgent, APIVersion, config.Location, config.Backoff)
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
|
||||
|
||||
klog.V(2).Infof("Azure ContainerServiceClient (read ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPS,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucket)
|
||||
klog.V(2).Infof("Azure ContainerServiceClient (write ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
|
||||
|
||||
client := &Client{
|
||||
armClient: armClient,
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
subscriptionID: config.SubscriptionID,
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// Get gets a ManagedCluster.
|
||||
func (c *Client) Get(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) {
|
||||
mc := metrics.NewMetricContext("managed_clusters", "get", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterReader.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return containerservice.ManagedCluster{}, retry.GetRateLimitError(false, "GetManagedCluster")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterReader.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("GetManagedCluster", "client throttled", c.RetryAfterReader)
|
||||
return containerservice.ManagedCluster{}, rerr
|
||||
}
|
||||
|
||||
result, rerr := c.getManagedCluster(ctx, resourceGroupName, managedClusterName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterReader = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getManagedCluster gets a ManagedCluster.
|
||||
func (c *Client) getManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.ContainerService/managedClusters",
|
||||
managedClusterName,
|
||||
)
|
||||
result := containerservice.ManagedCluster{}
|
||||
|
||||
response, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.get.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
err := autorest.Respond(
|
||||
response,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.get.respond", resourceID, err)
|
||||
return result, retry.GetError(response, err)
|
||||
}
|
||||
|
||||
result.Response = autorest.Response{Response: response}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List gets a list of ManagedClusters in the resource group.
|
||||
func (c *Client) List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) {
|
||||
mc := metrics.NewMetricContext("managed_clusters", "list", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterReader.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return nil, retry.GetRateLimitError(false, "ListManagedCluster")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterReader.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("ListManagedCluster", "client throttled", c.RetryAfterReader)
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
result, rerr := c.listManagedCluster(ctx, resourceGroupName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterReader = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// listManagedCluster gets a list of ManagedClusters in the resource group.
|
||||
func (c *Client) listManagedCluster(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) {
|
||||
resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters",
|
||||
autorest.Encode("path", c.subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName))
|
||||
result := make([]containerservice.ManagedCluster, 0)
|
||||
page := &ManagedClusterResultPage{}
|
||||
page.fn = c.listNextResults
|
||||
|
||||
resp, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
var err error
|
||||
page.mclr, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.respond", resourceID, err)
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
for {
|
||||
result = append(result, page.Values()...)
|
||||
|
||||
// Abort the loop when there's no nextLink in the response.
|
||||
if pointer.StringDeref(page.Response().NextLink, "") == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if err = page.NextWithContext(ctx); err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedcluster.list.next", resourceID, err)
|
||||
return result, retry.GetError(page.Response().Response.Response, err)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) listResponder(resp *http.Response) (result containerservice.ManagedClusterListResult, err error) {
|
||||
err = autorest.Respond(
|
||||
resp,
|
||||
autorest.ByIgnoring(),
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return
|
||||
}
|
||||
|
||||
// managedClusterListResultPreparer prepares a request to retrieve the next set of results.
|
||||
// It returns nil if no more results exist.
|
||||
func (c *Client) managedClusterListResultPreparer(ctx context.Context, mclr containerservice.ManagedClusterListResult) (*http.Request, error) {
|
||||
if mclr.NextLink == nil || len(pointer.StringDeref(mclr.NextLink, "")) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithBaseURL(pointer.StringDeref(mclr.NextLink, "")),
|
||||
}
|
||||
return c.armClient.PrepareGetRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// listNextResults retrieves the next set of results, if any.
|
||||
func (c *Client) listNextResults(ctx context.Context, lastResults containerservice.ManagedClusterListResult) (result containerservice.ManagedClusterListResult, err error) {
|
||||
req, err := c.managedClusterListResultPreparer(ctx, lastResults)
|
||||
if err != nil {
|
||||
return result, autorest.NewErrorWithError(err, "managedclusterclient", "listNextResults", nil, "Failure preparing next results request")
|
||||
}
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, rerr := c.armClient.Send(ctx, req)
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, autorest.NewErrorWithError(rerr.Error(), "managedclusterclient", "listNextResults", resp, "Failure sending next results request")
|
||||
}
|
||||
|
||||
result, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
err = autorest.NewErrorWithError(err, "managedclusterclient", "listNextResults", resp, "Failure responding to next results request")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ManagedClusterResultPage contains a page of ManagedCluster values.
|
||||
type ManagedClusterResultPage struct {
|
||||
fn func(context.Context, containerservice.ManagedClusterListResult) (containerservice.ManagedClusterListResult, error)
|
||||
mclr containerservice.ManagedClusterListResult
|
||||
}
|
||||
|
||||
// NextWithContext advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
func (page *ManagedClusterResultPage) NextWithContext(ctx context.Context) (err error) {
|
||||
next, err := page.fn(ctx, page.mclr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page.mclr = next
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
// Deprecated: Use NextWithContext() instead.
|
||||
func (page *ManagedClusterResultPage) Next() error {
|
||||
return page.NextWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NotDone returns true if the page enumeration should be started or is not yet complete.
|
||||
func (page ManagedClusterResultPage) NotDone() bool {
|
||||
return !page.mclr.IsEmpty()
|
||||
}
|
||||
|
||||
// Response returns the raw server response from the last page request.
|
||||
func (page ManagedClusterResultPage) Response() containerservice.ManagedClusterListResult {
|
||||
return page.mclr
|
||||
}
|
||||
|
||||
// Values returns the slice of values for the current page or nil if there are no values.
|
||||
func (page ManagedClusterResultPage) Values() []containerservice.ManagedCluster {
|
||||
if page.mclr.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
return *page.mclr.Value
|
||||
}
|
||||
|
||||
// CreateOrUpdate creates or updates a ManagedCluster.
|
||||
func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error {
|
||||
mc := metrics.NewMetricContext("managed_clusters", "create_or_update", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "CreateOrUpdateManagedCluster")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("CreateOrUpdateManagedCluster", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.createOrUpdateManagedCluster(ctx, resourceGroupName, managedClusterName, parameters, etag)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateManagedCluster creates or updates a ManagedCluster.
|
||||
func (c *Client) createOrUpdateManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.ContainerService/managedClusters",
|
||||
managedClusterName,
|
||||
)
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
if etag != "" {
|
||||
decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag)))
|
||||
}
|
||||
|
||||
response, rerr := c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators)
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedCluster.put.request", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode != http.StatusNoContent {
|
||||
_, rerr = c.createOrUpdateResponder(response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "managedCluster.put.respond", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createOrUpdateResponder(resp *http.Response) (*containerservice.ManagedCluster, *retry.Error) {
|
||||
result := &containerservice.ManagedCluster{}
|
||||
err := autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
// Delete deletes a ManagedCluster by name.
|
||||
func (c *Client) Delete(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error {
|
||||
mc := metrics.NewMetricContext("managed_clusters", "delete", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "DeleteManagedCluster")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("DeleteManagedCluster", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.deleteManagedCluster(ctx, resourceGroupName, managedClusterName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteManagedCluster deletes a ManagedCluster by name.
|
||||
func (c *Client) deleteManagedCluster(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.ContainerService/managedClusters",
|
||||
managedClusterName,
|
||||
)
|
||||
|
||||
return c.armClient.DeleteResource(ctx, resourceID, "")
|
||||
}
|
||||
@@ -1,634 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package containerserviceclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// 2065-01-24 05:20:00 +0000 UTC
|
||||
func getFutureTime() time.Time {
|
||||
return time.Unix(3000000000, 0)
|
||||
}
|
||||
|
||||
func getTestManagedClusterClient(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestManagedClusterClientWithNeverRateLimiter(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter()
|
||||
rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter()
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestManagedClusterClientWithRetryAfterReader(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
RetryAfterReader: getFutureTime(),
|
||||
RetryAfterWriter: getFutureTime(),
|
||||
}
|
||||
}
|
||||
|
||||
func getTestManagedCluster(name string) containerservice.ManagedCluster {
|
||||
return containerservice.ManagedCluster{
|
||||
ID: pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/%s", name)),
|
||||
Name: pointer.String(name),
|
||||
Location: pointer.String("eastus"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
config := &azclients.ClientConfig{
|
||||
SubscriptionID: "sub",
|
||||
ResourceManagerEndpoint: "endpoint",
|
||||
Location: "eastus",
|
||||
RateLimitConfig: &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitQPS: 0.5,
|
||||
CloudProviderRateLimitBucket: 1,
|
||||
CloudProviderRateLimitQPSWrite: 0.5,
|
||||
CloudProviderRateLimitBucketWrite: 1,
|
||||
},
|
||||
Backoff: &retry.Backoff{Steps: 1},
|
||||
}
|
||||
|
||||
mcClient := New(config)
|
||||
assert.Equal(t, "sub", mcClient.subscriptionID)
|
||||
assert.NotEmpty(t, mcClient.rateLimiterReader)
|
||||
assert.NotEmpty(t, mcClient.rateLimiterWriter)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
expected := containerservice.ManagedCluster{}
|
||||
expected.Response = autorest.Response{Response: response}
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestGetNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcGetErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetManagedCluster"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient)
|
||||
expected := containerservice.ManagedCluster{}
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Equal(t, mcGetErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcGetErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetManagedCluster", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient)
|
||||
expected := containerservice.ManagedCluster{}
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Equal(t, mcGetErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Empty(t, result)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
expected := containerservice.ManagedCluster{Response: autorest.Response{}}
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestGetInternalError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters/cluster"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
expected := containerservice.ManagedCluster{Response: autorest.Response{}}
|
||||
result, rerr := mcClient.Get(context.TODO(), "rg", "cluster")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")}
|
||||
responseBody, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(responseBody)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, 3, len(result))
|
||||
}
|
||||
|
||||
func TestListNextResultsMultiPages(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
prepareErr error
|
||||
sendErr *retry.Error
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
prepareErr: nil,
|
||||
sendErr: nil,
|
||||
},
|
||||
{
|
||||
prepareErr: fmt.Errorf("error"),
|
||||
},
|
||||
{
|
||||
sendErr: &retry.Error{RawError: fmt.Errorf("error")},
|
||||
},
|
||||
}
|
||||
|
||||
lastResult := containerservice.ManagedClusterListResult{
|
||||
NextLink: pointer.String("next"),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
}
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr)
|
||||
if test.prepareErr == nil {
|
||||
armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))),
|
||||
}, test.sendErr)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any())
|
||||
}
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, err := mcClient.listNextResults(context.TODO(), lastResult)
|
||||
if test.prepareErr != nil || test.sendErr != nil {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if test.prepareErr != nil {
|
||||
assert.Empty(t, result)
|
||||
} else {
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNextResultsMultiPagesWithListResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
test := struct {
|
||||
prepareErr error
|
||||
sendErr *retry.Error
|
||||
}{
|
||||
prepareErr: nil,
|
||||
sendErr: nil,
|
||||
}
|
||||
|
||||
lastResult := containerservice.ManagedClusterListResult{
|
||||
NextLink: pointer.String("next"),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
}
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr)
|
||||
if test.prepareErr == nil {
|
||||
armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))),
|
||||
}, test.sendErr)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any())
|
||||
}
|
||||
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(`{"foo":"bar"}`))),
|
||||
}
|
||||
expected := containerservice.ManagedClusterListResult{}
|
||||
expected.Response = autorest.Response{Response: response}
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, err := mcClient.listNextResults(context.TODO(), lastResult)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestListWithListResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")}
|
||||
responseBody, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(responseBody)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, 0, len(result))
|
||||
}
|
||||
|
||||
func TestListWithNextPage(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
mcList := []containerservice.ManagedCluster{getTestManagedCluster("cluster"), getTestManagedCluster("cluster1"), getTestManagedCluster("cluster2")}
|
||||
// ManagedClusterListResult.MarshalJson() doesn't include "nextLink" in its result, hence responseBody is composed manually below.
|
||||
responseBody, err := json.Marshal(map[string]interface{}{"value": mcList, "nextLink": "nextLink"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
pagedResponse, err := json.Marshal(containerservice.ManagedClusterListResult{Value: &mcList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(&http.Request{}, nil)
|
||||
armClient.EXPECT().Send(gomock.Any(), gomock.Any()).Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(pagedResponse)),
|
||||
}, nil)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(responseBody)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(2)
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, 6, len(result))
|
||||
}
|
||||
|
||||
func TestListNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcListErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "ListManagedCluster"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.Equal(t, 0, len(result))
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcListErr, rerr)
|
||||
}
|
||||
|
||||
func TestListRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcListErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "ListManagedCluster", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.Equal(t, 0, len(result))
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcListErr, rerr)
|
||||
}
|
||||
|
||||
func TestListThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.ContainerService/managedClusters"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
result, rerr := mcClient.List(context.TODO(), "rg")
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "*")
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "")
|
||||
assert.NotNil(t, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcCreateOrUpdateErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "CreateOrUpdateManagedCluster"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient)
|
||||
mc := getTestManagedCluster("cluster")
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcCreateOrUpdateErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcCreateOrUpdateErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "CreateOrUpdateManagedCluster", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient)
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcCreateOrUpdateErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(mc.ID, ""), mc, gomock.Any()).Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "cluster", mc, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(mc.ID, ""), "").Return(nil).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
rerr := mcClient.Delete(context.TODO(), "rg", "cluster")
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcDeleteErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "DeleteManagedCluster"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithNeverRateLimiter(armClient)
|
||||
rerr := mcClient.Delete(context.TODO(), "rg", "cluster")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcDeleteErr, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mcDeleteErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "DeleteManagedCluster", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestManagedClusterClientWithRetryAfterReader(armClient)
|
||||
rerr := mcClient.Delete(context.TODO(), "rg", "cluster")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, mcDeleteErr, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
mc := getTestManagedCluster("cluster")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(mc.ID, ""), "").Return(throttleErr).Times(1)
|
||||
|
||||
mcClient := getTestManagedClusterClient(armClient)
|
||||
rerr := mcClient.Delete(context.TODO(), "rg", "cluster")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package containerserviceclient implements the client for azure container service.
|
||||
package containerserviceclient // import "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient"
|
||||
@@ -1,42 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go -destination=mockcontainerserviceclient/interface.go -package=mockcontainerserviceclient Interface
|
||||
package containerserviceclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice"
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the API version for containerservice.
|
||||
APIVersion = "2020-04-01"
|
||||
)
|
||||
|
||||
// Interface is the client interface for ContainerService.
|
||||
type Interface interface {
|
||||
CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error
|
||||
Delete(ctx context.Context, resourceGroupName string, managedClusterName string) *retry.Error
|
||||
Get(ctx context.Context, resourceGroupName string, managedClusterName string) (containerservice.ManagedCluster, *retry.Error)
|
||||
List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package mockcontainerserviceclient implements the mock client for azure container service.
|
||||
package mockcontainerserviceclient // import "k8s.io/legacy-cloud-providers/azure/clients/containerserviceclient/mockcontainerserviceclient"
|
||||
@@ -1,114 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interface.go
|
||||
|
||||
// Package mockcontainerserviceclient is a generated GoMock package.
|
||||
package mockcontainerserviceclient
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
containerservice "github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2020-04-01/containerservice"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
retry "k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CreateOrUpdate mocks base method.
|
||||
func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, managedClusterName string, parameters containerservice.ManagedCluster, etag string) *retry.Error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, managedClusterName, parameters, etag)
|
||||
ret0, _ := ret[0].(*retry.Error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateOrUpdate indicates an expected call of CreateOrUpdate.
|
||||
func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, managedClusterName, parameters, etag interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, managedClusterName, parameters, etag)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockInterface) Delete(ctx context.Context, resourceGroupName, managedClusterName string) *retry.Error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, resourceGroupName, managedClusterName)
|
||||
ret0, _ := ret[0].(*retry.Error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockInterfaceMockRecorder) Delete(ctx, resourceGroupName, managedClusterName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), ctx, resourceGroupName, managedClusterName)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockInterface) Get(ctx context.Context, resourceGroupName, managedClusterName string) (containerservice.ManagedCluster, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, managedClusterName)
|
||||
ret0, _ := ret[0].(containerservice.ManagedCluster)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, managedClusterName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, managedClusterName)
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockInterface) List(ctx context.Context, resourceGroupName string) ([]containerservice.ManagedCluster, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", ctx, resourceGroupName)
|
||||
ret0, _ := ret[0].([]containerservice.ManagedCluster)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockInterfaceMockRecorder) List(ctx, resourceGroupName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), ctx, resourceGroupName)
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package deploymentclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/klog/v2"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/metrics"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var _ Interface = &Client{}
|
||||
|
||||
// Client implements ContainerService client Interface.
|
||||
type Client struct {
|
||||
armClient armclient.Interface
|
||||
subscriptionID string
|
||||
|
||||
// Rate limiting configures.
|
||||
rateLimiterReader flowcontrol.RateLimiter
|
||||
rateLimiterWriter flowcontrol.RateLimiter
|
||||
|
||||
// ARM throttling configures.
|
||||
RetryAfterReader time.Time
|
||||
RetryAfterWriter time.Time
|
||||
}
|
||||
|
||||
// New creates a new ContainerServiceClient client with ratelimiting.
|
||||
func New(config *azclients.ClientConfig) *Client {
|
||||
baseURI := config.ResourceManagerEndpoint
|
||||
authorizer := config.Authorizer
|
||||
armClient := armclient.New(authorizer, baseURI, config.UserAgent, APIVersion, config.Location, config.Backoff)
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
|
||||
|
||||
klog.V(2).Infof("Azure DeploymentClient (read ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPS,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucket)
|
||||
klog.V(2).Infof("Azure DeploymentClient (write ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
|
||||
|
||||
client := &Client{
|
||||
armClient: armClient,
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
subscriptionID: config.SubscriptionID,
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// Get gets a deployment
|
||||
func (c *Client) Get(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error) {
|
||||
mc := metrics.NewMetricContext("deployments", "get", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterReader.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return resources.DeploymentExtended{}, retry.GetRateLimitError(false, "GetDeployment")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterReader.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("GetDeployment", "client throttled", c.RetryAfterReader)
|
||||
return resources.DeploymentExtended{}, rerr
|
||||
}
|
||||
|
||||
result, rerr := c.getDeployment(ctx, resourceGroupName, deploymentName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterReader = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getDeployment gets a deployment.
|
||||
func (c *Client) getDeployment(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error) {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Resources/deployments",
|
||||
deploymentName,
|
||||
)
|
||||
result := resources.DeploymentExtended{}
|
||||
|
||||
response, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.get.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
err := autorest.Respond(
|
||||
response,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.get.respond", resourceID, err)
|
||||
return result, retry.GetError(response, err)
|
||||
}
|
||||
|
||||
result.Response = autorest.Response{Response: response}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List gets a list of deployments in the resource group.
|
||||
func (c *Client) List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) {
|
||||
mc := metrics.NewMetricContext("deployments", "list", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterReader.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return nil, retry.GetRateLimitError(false, "ListDeployment")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterReader.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("ListDeployment", "client throttled", c.RetryAfterReader)
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
result, rerr := c.listDeployment(ctx, resourceGroupName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterReader = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// listDeployment gets a list of deployments in the resource group.
|
||||
func (c *Client) listDeployment(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) {
|
||||
resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments",
|
||||
autorest.Encode("path", c.subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName))
|
||||
result := make([]resources.DeploymentExtended, 0)
|
||||
page := &DeploymentResultPage{}
|
||||
page.fn = c.listNextResults
|
||||
|
||||
resp, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
var err error
|
||||
page.dplr, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.respond", resourceID, err)
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
for {
|
||||
result = append(result, page.Values()...)
|
||||
|
||||
// Abort the loop when there's no nextLink in the response.
|
||||
if pointer.StringDeref(page.Response().NextLink, "") == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if err = page.NextWithContext(ctx); err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.list.next", resourceID, err)
|
||||
return result, retry.GetError(page.Response().Response.Response, err)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) listResponder(resp *http.Response) (result resources.DeploymentListResult, err error) {
|
||||
err = autorest.Respond(
|
||||
resp,
|
||||
autorest.ByIgnoring(),
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return
|
||||
}
|
||||
|
||||
// deploymentListResultPreparer prepares a request to retrieve the next set of results.
|
||||
// It returns nil if no more results exist.
|
||||
func (c *Client) deploymentListResultPreparer(ctx context.Context, dplr resources.DeploymentListResult) (*http.Request, error) {
|
||||
if dplr.NextLink == nil || len(pointer.StringDeref(dplr.NextLink, "")) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithBaseURL(pointer.StringDeref(dplr.NextLink, "")),
|
||||
}
|
||||
return c.armClient.PrepareGetRequest(ctx, decorators...)
|
||||
}
|
||||
|
||||
// listNextResults retrieves the next set of results, if any.
|
||||
func (c *Client) listNextResults(ctx context.Context, lastResults resources.DeploymentListResult) (result resources.DeploymentListResult, err error) {
|
||||
req, err := c.deploymentListResultPreparer(ctx, lastResults)
|
||||
if err != nil {
|
||||
return result, autorest.NewErrorWithError(err, "deploymentclient", "listNextResults", nil, "Failure preparing next results request")
|
||||
}
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, rerr := c.armClient.Send(ctx, req)
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, autorest.NewErrorWithError(rerr.Error(), "deploymentclient", "listNextResults", resp, "Failure sending next results request")
|
||||
}
|
||||
|
||||
result, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
err = autorest.NewErrorWithError(err, "deploymentclient", "listNextResults", resp, "Failure responding to next results request")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeploymentResultPage contains a page of deployments values.
|
||||
type DeploymentResultPage struct {
|
||||
fn func(context.Context, resources.DeploymentListResult) (resources.DeploymentListResult, error)
|
||||
dplr resources.DeploymentListResult
|
||||
}
|
||||
|
||||
// NextWithContext advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
func (page *DeploymentResultPage) NextWithContext(ctx context.Context) (err error) {
|
||||
next, err := page.fn(ctx, page.dplr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page.dplr = next
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
// Deprecated: Use NextWithContext() instead.
|
||||
func (page *DeploymentResultPage) Next() error {
|
||||
return page.NextWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NotDone returns true if the page enumeration should be started or is not yet complete.
|
||||
func (page DeploymentResultPage) NotDone() bool {
|
||||
return !page.dplr.IsEmpty()
|
||||
}
|
||||
|
||||
// Response returns the raw server response from the last page request.
|
||||
func (page DeploymentResultPage) Response() resources.DeploymentListResult {
|
||||
return page.dplr
|
||||
}
|
||||
|
||||
// Values returns the slice of values for the current page or nil if there are no values.
|
||||
func (page DeploymentResultPage) Values() []resources.DeploymentExtended {
|
||||
if page.dplr.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
return *page.dplr.Value
|
||||
}
|
||||
|
||||
// CreateOrUpdate creates or updates a deployment.
|
||||
func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, deploymentName string, parameters resources.Deployment, etag string) *retry.Error {
|
||||
mc := metrics.NewMetricContext("deployments", "create_or_update", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "CreateOrUpdateDeployment")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("CreateOrUpdateDeployment", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.createOrUpdateDeployment(ctx, resourceGroupName, deploymentName, parameters, etag)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createOrUpdateDeployment(ctx context.Context, resourceGroupName string, deploymentName string, parameters resources.Deployment, etag string) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Resources/deployments",
|
||||
deploymentName,
|
||||
)
|
||||
decorators := []autorest.PrepareDecorator{
|
||||
autorest.WithPathParameters("{resourceID}", map[string]interface{}{"resourceID": resourceID}),
|
||||
autorest.WithJSON(parameters),
|
||||
}
|
||||
if etag != "" {
|
||||
decorators = append(decorators, autorest.WithHeader("If-Match", autorest.String(etag)))
|
||||
}
|
||||
|
||||
response, rerr := c.armClient.PutResourceWithDecorators(ctx, resourceID, parameters, decorators)
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.put.request", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode != http.StatusNoContent {
|
||||
_, rerr = c.createOrUpdateResponder(response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.put.respond", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createOrUpdateResponder(resp *http.Response) (*resources.DeploymentExtended, *retry.Error) {
|
||||
result := &resources.DeploymentExtended{}
|
||||
err := autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
// Delete deletes a deployment by name.
|
||||
func (c *Client) Delete(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error {
|
||||
mc := metrics.NewMetricContext("deployments", "delete", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "DeleteDeployment")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("DeleteDeployment", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.deleteDeployment(ctx, resourceGroupName, deploymentName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteDeployment deletes a deployment by name.
|
||||
func (c *Client) deleteDeployment(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Resources/deployments",
|
||||
deploymentName,
|
||||
)
|
||||
|
||||
return c.armClient.DeleteResource(ctx, resourceID, "")
|
||||
}
|
||||
|
||||
// ExportTemplate exports the template used for specified deployment
|
||||
func (c *Client) ExportTemplate(ctx context.Context, resourceGroupName string, deploymentName string) (result resources.DeploymentExportResult, rerr *retry.Error) {
|
||||
mc := metrics.NewMetricContext("deployments", "export_template", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return resources.DeploymentExportResult{}, retry.GetRateLimitError(true, "ExportTemplateDeployment")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("CreateOrUpdateDeployment", "client throttled", c.RetryAfterWriter)
|
||||
return resources.DeploymentExportResult{}, rerr
|
||||
}
|
||||
|
||||
resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Resources/deployments/%s/exportTemplate",
|
||||
autorest.Encode("path", c.subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName),
|
||||
autorest.Encode("path", deploymentName))
|
||||
response, rerr := c.armClient.PostResource(ctx, resourceID, "exportTemplate", struct{}{})
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.exportTemplate.request", resourceID, rerr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := autorest.Respond(
|
||||
response,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "deployment.exportTemplate.respond", resourceID, err)
|
||||
return result, retry.GetError(response, err)
|
||||
}
|
||||
|
||||
result.Response = autorest.Response{Response: response}
|
||||
return
|
||||
}
|
||||
@@ -1,635 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package deploymentclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
// 2065-01-24 05:20:00 +0000 UTC
|
||||
func getFutureTime() time.Time {
|
||||
return time.Unix(3000000000, 0)
|
||||
}
|
||||
|
||||
func getTestDeploymentClient(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestDeploymentClientWithNeverRateLimiter(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader := flowcontrol.NewFakeNeverRateLimiter()
|
||||
rateLimiterWriter := flowcontrol.NewFakeNeverRateLimiter()
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func getTestDeploymentClientWithRetryAfterReader(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
rateLimiterWriter := flowcontrol.NewFakeAlwaysRateLimiter()
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
RetryAfterReader: getFutureTime(),
|
||||
RetryAfterWriter: getFutureTime(),
|
||||
}
|
||||
}
|
||||
|
||||
func getTestDeploymentExtended(name string) resources.DeploymentExtended {
|
||||
return resources.DeploymentExtended{
|
||||
ID: pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/%s", name)),
|
||||
Name: pointer.String(name),
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
config := &azclients.ClientConfig{
|
||||
SubscriptionID: "sub",
|
||||
ResourceManagerEndpoint: "endpoint",
|
||||
Location: "eastus",
|
||||
RateLimitConfig: &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitQPS: 0.5,
|
||||
CloudProviderRateLimitBucket: 1,
|
||||
CloudProviderRateLimitQPSWrite: 0.5,
|
||||
CloudProviderRateLimitBucketWrite: 1,
|
||||
},
|
||||
Backoff: &retry.Backoff{Steps: 1},
|
||||
}
|
||||
dpClient := New(config)
|
||||
assert.Equal(t, "sub", dpClient.subscriptionID)
|
||||
assert.NotEmpty(t, dpClient.rateLimiterReader)
|
||||
assert.NotEmpty(t, dpClient.rateLimiterWriter)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
expected := resources.DeploymentExtended{}
|
||||
expected.Response = autorest.Response{Response: response}
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestGetNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpGetErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "GetDeployment"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient)
|
||||
expected := resources.DeploymentExtended{}
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Equal(t, dpGetErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpGetErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "GetDeployment", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
dpClient := getTestDeploymentClientWithRetryAfterReader(armClient)
|
||||
expected := resources.DeploymentExtended{}
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.Equal(t, dpGetErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Empty(t, result)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
expected := resources.DeploymentExtended{Response: autorest.Response{}}
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestGetInternalError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments/dep"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
expected := resources.DeploymentExtended{Response: autorest.Response{}}
|
||||
result, rerr := dpClient.Get(context.TODO(), "rg", "dep")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")}
|
||||
responseBody, err := json.Marshal(resources.DeploymentListResult{Value: &dpList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(responseBody)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, 3, len(result))
|
||||
}
|
||||
|
||||
func TestListNextResultsMultiPages(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
tests := []struct {
|
||||
prepareErr error
|
||||
sendErr *retry.Error
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
prepareErr: nil,
|
||||
sendErr: nil,
|
||||
},
|
||||
{
|
||||
prepareErr: fmt.Errorf("error"),
|
||||
},
|
||||
{
|
||||
sendErr: &retry.Error{RawError: fmt.Errorf("error")},
|
||||
},
|
||||
}
|
||||
|
||||
lastResult := resources.DeploymentListResult{
|
||||
NextLink: pointer.String("next"),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
}
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr)
|
||||
if test.prepareErr == nil {
|
||||
armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))),
|
||||
}, test.sendErr)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any())
|
||||
}
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, err := dpClient.listNextResults(context.TODO(), lastResult)
|
||||
if test.prepareErr != nil || test.sendErr != nil {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if test.prepareErr != nil {
|
||||
assert.Empty(t, result)
|
||||
} else {
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNextResultsMultiPagesWithListResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
test := struct {
|
||||
prepareErr error
|
||||
sendErr *retry.Error
|
||||
}{
|
||||
prepareErr: nil,
|
||||
sendErr: nil,
|
||||
}
|
||||
|
||||
lastResult := resources.DeploymentListResult{
|
||||
NextLink: pointer.String("next"),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
}
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(req, test.prepareErr)
|
||||
if test.prepareErr == nil {
|
||||
armClient.EXPECT().Send(gomock.Any(), req).Return(&http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"foo":"bar"}`))),
|
||||
}, test.sendErr)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any())
|
||||
}
|
||||
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewBuffer([]byte(`{"foo":"bar"}`))),
|
||||
}
|
||||
expected := resources.DeploymentListResult{}
|
||||
expected.Response = autorest.Response{Response: response}
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, err := dpClient.listNextResults(context.TODO(), lastResult)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestListWithListResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")}
|
||||
responseBody, err := json.Marshal(resources.DeploymentListResult{Value: &dpList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(responseBody)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, 0, len(result))
|
||||
}
|
||||
|
||||
func TestListWithNextPage(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments"
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
dpList := []resources.DeploymentExtended{getTestDeploymentExtended("dep"), getTestDeploymentExtended("dep1"), getTestDeploymentExtended("dep2")}
|
||||
// DeploymentListResult.MarshalJson() doesn't include "nextLink" in its result, hence partialResponse is composed manually below.
|
||||
partialResponse, err := json.Marshal(map[string]interface{}{"value": dpList, "nextLink": "nextLink"})
|
||||
assert.NoError(t, err)
|
||||
pagedResponse, err := json.Marshal(resources.DeploymentListResult{Value: &dpList})
|
||||
assert.NoError(t, err)
|
||||
armClient.EXPECT().PrepareGetRequest(gomock.Any(), gomock.Any()).Return(&http.Request{}, nil)
|
||||
armClient.EXPECT().Send(gomock.Any(), gomock.Any()).Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(pagedResponse)),
|
||||
}, nil)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(
|
||||
&http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(partialResponse)),
|
||||
}, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(2)
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.Nil(t, rerr)
|
||||
assert.Equal(t, 6, len(result))
|
||||
}
|
||||
|
||||
func TestListNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpListErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "read", "ListDeployment"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.Equal(t, 0, len(result))
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpListErr, rerr)
|
||||
}
|
||||
|
||||
func TestListRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpListErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "ListDeployment", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
dpClient := getTestDeploymentClientWithRetryAfterReader(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.Equal(t, 0, len(result))
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpListErr, rerr)
|
||||
}
|
||||
|
||||
func TestListThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Resources/deployments"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
result, rerr := dpClient.List(context.TODO(), "rg")
|
||||
assert.Empty(t, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dp := resources.Deployment{}
|
||||
dpExtended := getTestDeploymentExtended("dep")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "*")
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateWithCreateOrUpdateResponderError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dp := resources.Deployment{}
|
||||
dpExtended := getTestDeploymentExtended("dep")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "")
|
||||
assert.NotNil(t, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpCreateOrUpdateErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "CreateOrUpdateDeployment"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient)
|
||||
dp := resources.Deployment{}
|
||||
rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpCreateOrUpdateErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpCreateOrUpdateErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "CreateOrUpdateDeployment", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
dp := resources.Deployment{}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
mcClient := getTestDeploymentClientWithRetryAfterReader(armClient)
|
||||
rerr := mcClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpCreateOrUpdateErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
dp := resources.Deployment{}
|
||||
dpExtended := getTestDeploymentExtended("dep")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().PutResourceWithDecorators(gomock.Any(), pointer.StringDeref(dpExtended.ID, ""), dp, gomock.Any()).Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
rerr := dpClient.CreateOrUpdate(context.TODO(), "rg", "dep", dp, "")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dp := getTestDeploymentExtended("dep")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(dp.ID, ""), "").Return(nil).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
rerr := dpClient.Delete(context.TODO(), "rg", "dep")
|
||||
assert.Nil(t, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteNeverRateLimiter(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpDeleteErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider rate limited(%s) for operation %q", "write", "DeleteDeployment"),
|
||||
Retriable: true,
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
dpClient := getTestDeploymentClientWithNeverRateLimiter(armClient)
|
||||
rerr := dpClient.Delete(context.TODO(), "rg", "dep")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpDeleteErr, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteRetryAfterReader(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dpDeleteErr := &retry.Error{
|
||||
RawError: fmt.Errorf("azure cloud provider throttled for operation %s with reason %q", "DeleteDeployment", "client throttled"),
|
||||
Retriable: true,
|
||||
RetryAfter: getFutureTime(),
|
||||
}
|
||||
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
|
||||
dpClient := getTestDeploymentClientWithRetryAfterReader(armClient)
|
||||
rerr := dpClient.Delete(context.TODO(), "rg", "dep")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, dpDeleteErr, rerr)
|
||||
}
|
||||
|
||||
func TestDeleteThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
dp := getTestDeploymentExtended("dep")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(dp.ID, ""), "").Return(throttleErr).Times(1)
|
||||
|
||||
dpClient := getTestDeploymentClient(armClient)
|
||||
rerr := dpClient.Delete(context.TODO(), "rg", "dep")
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package deploymentclient implements the client for azure deployments.
|
||||
package deploymentclient // import "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient"
|
||||
@@ -1,42 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
//go:generate mockgen -copyright_file=$BUILD_TAG_FILE -source=interface.go -destination=mockdeploymentclient/interface.go -package=mockdeploymentclient Interface
|
||||
package deploymentclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the API version for resources.
|
||||
APIVersion = "2017-05-10"
|
||||
)
|
||||
|
||||
// Interface is the client interface for Deployments.
|
||||
type Interface interface {
|
||||
Get(ctx context.Context, resourceGroupName string, deploymentName string) (resources.DeploymentExtended, *retry.Error)
|
||||
List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error)
|
||||
ExportTemplate(ctx context.Context, resourceGroupName string, deploymentName string) (result resources.DeploymentExportResult, rerr *retry.Error)
|
||||
CreateOrUpdate(ctx context.Context, resourceGroupName string, managedClusterName string, parameters resources.Deployment, etag string) *retry.Error
|
||||
Delete(ctx context.Context, resourceGroupName string, deploymentName string) *retry.Error
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package mockdeploymentclient implements the mock client for azure deployments.
|
||||
package mockdeploymentclient // import "k8s.io/legacy-cloud-providers/azure/clients/deploymentclient/mockdeploymentclient"
|
||||
@@ -1,129 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: interface.go
|
||||
|
||||
// Package mockdeploymentclient is a generated GoMock package.
|
||||
package mockdeploymentclient
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
resources "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
retry "k8s.io/legacy-cloud-providers/azure/retry"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface.
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance.
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CreateOrUpdate mocks base method.
|
||||
func (m *MockInterface) CreateOrUpdate(ctx context.Context, resourceGroupName, managedClusterName string, parameters resources.Deployment, etag string) *retry.Error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateOrUpdate", ctx, resourceGroupName, managedClusterName, parameters, etag)
|
||||
ret0, _ := ret[0].(*retry.Error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateOrUpdate indicates an expected call of CreateOrUpdate.
|
||||
func (mr *MockInterfaceMockRecorder) CreateOrUpdate(ctx, resourceGroupName, managedClusterName, parameters, etag interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), ctx, resourceGroupName, managedClusterName, parameters, etag)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockInterface) Delete(ctx context.Context, resourceGroupName, deploymentName string) *retry.Error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, resourceGroupName, deploymentName)
|
||||
ret0, _ := ret[0].(*retry.Error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockInterfaceMockRecorder) Delete(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), ctx, resourceGroupName, deploymentName)
|
||||
}
|
||||
|
||||
// ExportTemplate mocks base method.
|
||||
func (m *MockInterface) ExportTemplate(ctx context.Context, resourceGroupName, deploymentName string) (resources.DeploymentExportResult, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExportTemplate", ctx, resourceGroupName, deploymentName)
|
||||
ret0, _ := ret[0].(resources.DeploymentExportResult)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ExportTemplate indicates an expected call of ExportTemplate.
|
||||
func (mr *MockInterfaceMockRecorder) ExportTemplate(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportTemplate", reflect.TypeOf((*MockInterface)(nil).ExportTemplate), ctx, resourceGroupName, deploymentName)
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockInterface) Get(ctx context.Context, resourceGroupName, deploymentName string) (resources.DeploymentExtended, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, deploymentName)
|
||||
ret0, _ := ret[0].(resources.DeploymentExtended)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockInterfaceMockRecorder) Get(ctx, resourceGroupName, deploymentName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockInterface)(nil).Get), ctx, resourceGroupName, deploymentName)
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockInterface) List(ctx context.Context, resourceGroupName string) ([]resources.DeploymentExtended, *retry.Error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", ctx, resourceGroupName)
|
||||
ret0, _ := ret[0].([]resources.DeploymentExtended)
|
||||
ret1, _ := ret[1].(*retry.Error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockInterfaceMockRecorder) List(ctx, resourceGroupName interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInterface)(nil).List), ctx, resourceGroupName)
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package diskclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
"k8s.io/klog/v2"
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/metrics"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
var _ Interface = &Client{}
|
||||
|
||||
// Client implements Disk client Interface.
|
||||
type Client struct {
|
||||
armClient armclient.Interface
|
||||
subscriptionID string
|
||||
|
||||
// Rate limiting configures.
|
||||
rateLimiterReader flowcontrol.RateLimiter
|
||||
rateLimiterWriter flowcontrol.RateLimiter
|
||||
|
||||
// ARM throttling configures.
|
||||
RetryAfterReader time.Time
|
||||
RetryAfterWriter time.Time
|
||||
}
|
||||
|
||||
// New creates a new Disk client with ratelimiting.
|
||||
func New(config *azclients.ClientConfig) *Client {
|
||||
baseURI := config.ResourceManagerEndpoint
|
||||
authorizer := config.Authorizer
|
||||
apiVersion := APIVersion
|
||||
if strings.EqualFold(config.CloudName, AzureStackCloudName) {
|
||||
apiVersion = AzureStackCloudAPIVersion
|
||||
}
|
||||
armClient := armclient.New(authorizer, baseURI, config.UserAgent, apiVersion, config.Location, config.Backoff)
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(config.RateLimitConfig)
|
||||
|
||||
klog.V(2).Infof("Azure DisksClient (read ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPS,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucket)
|
||||
klog.V(2).Infof("Azure DisksClient (write ops) using rate limit config: QPS=%g, bucket=%d",
|
||||
config.RateLimitConfig.CloudProviderRateLimitQPSWrite,
|
||||
config.RateLimitConfig.CloudProviderRateLimitBucketWrite)
|
||||
|
||||
client := &Client{
|
||||
armClient: armClient,
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
subscriptionID: config.SubscriptionID,
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// Get gets a Disk.
|
||||
func (c *Client) Get(ctx context.Context, resourceGroupName string, diskName string) (compute.Disk, *retry.Error) {
|
||||
mc := metrics.NewMetricContext("disks", "get", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterReader.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return compute.Disk{}, retry.GetRateLimitError(false, "GetDisk")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterReader.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("GetDisk", "client throttled", c.RetryAfterReader)
|
||||
return compute.Disk{}, rerr
|
||||
}
|
||||
|
||||
result, rerr := c.getDisk(ctx, resourceGroupName, diskName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterReader = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getDisk gets a Disk.
|
||||
func (c *Client) getDisk(ctx context.Context, resourceGroupName string, diskName string) (compute.Disk, *retry.Error) {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Compute/disks",
|
||||
diskName,
|
||||
)
|
||||
result := compute.Disk{}
|
||||
|
||||
response, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.get.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
err := autorest.Respond(
|
||||
response,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.get.respond", resourceID, err)
|
||||
return result, retry.GetError(response, err)
|
||||
}
|
||||
|
||||
result.Response = autorest.Response{Response: response}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdate creates or updates a Disk.
|
||||
func (c *Client) CreateOrUpdate(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.Disk) *retry.Error {
|
||||
mc := metrics.NewMetricContext("disks", "create_or_update", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "DiskCreateOrUpdate")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("DiskCreateOrUpdate", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.createOrUpdateDisk(ctx, resourceGroupName, diskName, diskParameter)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateDisk creates or updates a Disk.
|
||||
func (c *Client) createOrUpdateDisk(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.Disk) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Compute/disks",
|
||||
diskName,
|
||||
)
|
||||
|
||||
response, rerr := c.armClient.PutResource(ctx, resourceID, diskParameter)
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.request", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode != http.StatusNoContent {
|
||||
_, rerr = c.createOrUpdateResponder(response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.respond", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) createOrUpdateResponder(resp *http.Response) (*compute.Disk, *retry.Error) {
|
||||
result := &compute.Disk{}
|
||||
err := autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
// Update creates or updates a Disk.
|
||||
func (c *Client) Update(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.DiskUpdate) *retry.Error {
|
||||
mc := metrics.NewMetricContext("disks", "update", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "DiskUpdate")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("DiskUpdate", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.updateDisk(ctx, resourceGroupName, diskName, diskParameter)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDisk updates a Disk.
|
||||
func (c *Client) updateDisk(ctx context.Context, resourceGroupName string, diskName string, diskParameter compute.DiskUpdate) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Compute/disks",
|
||||
diskName,
|
||||
)
|
||||
|
||||
response, rerr := c.armClient.PatchResource(ctx, resourceID, diskParameter)
|
||||
defer c.armClient.CloseResponse(ctx, response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.request", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode != http.StatusNoContent {
|
||||
_, rerr = c.updateResponder(response)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.put.respond", resourceID, rerr.Error())
|
||||
return rerr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) updateResponder(resp *http.Response) (*compute.Disk, *retry.Error) {
|
||||
result := &compute.Disk{}
|
||||
err := autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated),
|
||||
autorest.ByUnmarshallingJSON(&result))
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
// Delete deletes a Disk by name.
|
||||
func (c *Client) Delete(ctx context.Context, resourceGroupName string, diskName string) *retry.Error {
|
||||
mc := metrics.NewMetricContext("disks", "delete", resourceGroupName, c.subscriptionID, "")
|
||||
|
||||
// Report errors if the client is rate limited.
|
||||
if !c.rateLimiterWriter.TryAccept() {
|
||||
mc.RateLimitedCount()
|
||||
return retry.GetRateLimitError(true, "DiskDelete")
|
||||
}
|
||||
|
||||
// Report errors if the client is throttled.
|
||||
if c.RetryAfterWriter.After(time.Now()) {
|
||||
mc.ThrottledCount()
|
||||
rerr := retry.GetThrottlingError("DiskDelete", "client throttled", c.RetryAfterWriter)
|
||||
return rerr
|
||||
}
|
||||
|
||||
rerr := c.deleteDisk(ctx, resourceGroupName, diskName)
|
||||
mc.Observe(rerr.Error())
|
||||
if rerr != nil {
|
||||
if rerr.IsThrottled() {
|
||||
// Update RetryAfterReader so that no more requests would be sent until RetryAfter expires.
|
||||
c.RetryAfterWriter = rerr.RetryAfter
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteDisk deletes a PublicIPAddress by name.
|
||||
func (c *Client) deleteDisk(ctx context.Context, resourceGroupName string, diskName string) *retry.Error {
|
||||
resourceID := armclient.GetResourceID(
|
||||
c.subscriptionID,
|
||||
resourceGroupName,
|
||||
"Microsoft.Compute/disks",
|
||||
diskName,
|
||||
)
|
||||
|
||||
return c.armClient.DeleteResource(ctx, resourceID, "")
|
||||
}
|
||||
|
||||
// ListByResourceGroup lists all the disks under a resource group.
|
||||
func (c *Client) ListByResourceGroup(ctx context.Context, resourceGroupName string) ([]compute.Disk, *retry.Error) {
|
||||
resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks",
|
||||
autorest.Encode("path", c.subscriptionID),
|
||||
autorest.Encode("path", resourceGroupName))
|
||||
|
||||
result := make([]compute.Disk, 0)
|
||||
page := &DiskListPage{}
|
||||
page.fn = c.listNextResults
|
||||
|
||||
resp, rerr := c.armClient.GetResource(ctx, resourceID, "")
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.request", resourceID, rerr.Error())
|
||||
return result, rerr
|
||||
}
|
||||
|
||||
var err error
|
||||
page.dl, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.respond", resourceID, err)
|
||||
return result, retry.GetError(resp, err)
|
||||
}
|
||||
|
||||
for {
|
||||
result = append(result, page.Values()...)
|
||||
|
||||
// Abort the loop when there's no nextLink in the response.
|
||||
if pointer.StringDeref(page.Response().NextLink, "") == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if err = page.NextWithContext(ctx); err != nil {
|
||||
klog.V(5).Infof("Received error in %s: resourceID: %s, error: %s", "disk.list.next", resourceID, err)
|
||||
return result, retry.GetError(page.Response().Response.Response, err)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// listNextResults retrieves the next set of results, if any.
|
||||
func (c *Client) listNextResults(ctx context.Context, lastResults compute.DiskList) (result compute.DiskList, err error) {
|
||||
req, err := c.diskListPreparer(ctx, lastResults)
|
||||
if err != nil {
|
||||
return result, autorest.NewErrorWithError(err, "diskclient", "listNextResults", nil, "Failure preparing next results request")
|
||||
}
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, rerr := c.armClient.Send(ctx, req)
|
||||
defer c.armClient.CloseResponse(ctx, resp)
|
||||
if rerr != nil {
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return result, autorest.NewErrorWithError(rerr.Error(), "diskclient", "listNextResults", resp, "Failure sending next results request")
|
||||
}
|
||||
|
||||
result, err = c.listResponder(resp)
|
||||
if err != nil {
|
||||
err = autorest.NewErrorWithError(err, "diskclient", "listNextResults", resp, "Failure responding to next results request")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// listResponder handles the response to the List request. The method always
|
||||
// closes the http.Response Body.
|
||||
func (c *Client) listResponder(resp *http.Response) (result compute.DiskList, err error) {
|
||||
err = autorest.Respond(
|
||||
resp,
|
||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||
autorest.ByUnmarshallingJSON(&result),
|
||||
autorest.ByClosing())
|
||||
result.Response = autorest.Response{Response: resp}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) diskListPreparer(ctx context.Context, lr compute.DiskList) (*http.Request, error) {
|
||||
if lr.NextLink == nil || len(pointer.StringDeref(lr.NextLink, "")) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
return autorest.Prepare((&http.Request{}).WithContext(ctx),
|
||||
autorest.AsJSON(),
|
||||
autorest.AsGet(),
|
||||
autorest.WithBaseURL(pointer.StringDeref(lr.NextLink, "")))
|
||||
}
|
||||
|
||||
// DiskListPage contains a page of Disk values.
|
||||
type DiskListPage struct {
|
||||
fn func(context.Context, compute.DiskList) (compute.DiskList, error)
|
||||
dl compute.DiskList
|
||||
}
|
||||
|
||||
// NextWithContext advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
func (page *DiskListPage) NextWithContext(ctx context.Context) (err error) {
|
||||
next, err := page.fn(ctx, page.dl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page.dl = next
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next advances to the next page of values. If there was an error making
|
||||
// the request the page does not advance and the error is returned.
|
||||
// Deprecated: Use NextWithContext() instead.
|
||||
func (page *DiskListPage) Next() error {
|
||||
return page.NextWithContext(context.Background())
|
||||
}
|
||||
|
||||
// NotDone returns true if the page enumeration should be started or is not yet complete.
|
||||
func (page DiskListPage) NotDone() bool {
|
||||
return !page.dl.IsEmpty()
|
||||
}
|
||||
|
||||
// Response returns the raw server response from the last page request.
|
||||
func (page DiskListPage) Response() compute.DiskList {
|
||||
return page.dl
|
||||
}
|
||||
|
||||
// Values returns the slice of values for the current page or nil if there are no values.
|
||||
func (page DiskListPage) Values() []compute.Disk {
|
||||
if page.dl.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
return *page.dl.Value
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
//go:build !providerless
|
||||
// +build !providerless
|
||||
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package diskclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-12-01/compute"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
azclients "k8s.io/legacy-cloud-providers/azure/clients"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/clients/armclient/mockarmclient"
|
||||
"k8s.io/legacy-cloud-providers/azure/retry"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
config := &azclients.ClientConfig{
|
||||
SubscriptionID: "sub",
|
||||
ResourceManagerEndpoint: "endpoint",
|
||||
Location: "eastus",
|
||||
RateLimitConfig: &azclients.RateLimitConfig{
|
||||
CloudProviderRateLimit: true,
|
||||
CloudProviderRateLimitQPS: 0.5,
|
||||
CloudProviderRateLimitBucket: 1,
|
||||
CloudProviderRateLimitQPSWrite: 0.5,
|
||||
CloudProviderRateLimitBucketWrite: 1,
|
||||
},
|
||||
Backoff: &retry.Backoff{Steps: 1},
|
||||
}
|
||||
|
||||
diskClient := New(config)
|
||||
assert.Equal(t, "sub", diskClient.subscriptionID)
|
||||
assert.NotEmpty(t, diskClient.rateLimiterReader)
|
||||
assert.NotEmpty(t, diskClient.rateLimiterWriter)
|
||||
}
|
||||
|
||||
func TestGetNotFound(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
expected := compute.Disk{Response: autorest.Response{}}
|
||||
result, rerr := diskClient.Get(context.TODO(), "rg", "disk1")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusNotFound, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestGetInternalError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
expected := compute.Disk{Response: autorest.Response{}}
|
||||
result, rerr := diskClient.Get(context.TODO(), "rg", "disk1")
|
||||
assert.Equal(t, expected, result)
|
||||
assert.NotNil(t, rerr)
|
||||
assert.Equal(t, http.StatusInternalServerError, rerr.HTTPStatusCode)
|
||||
}
|
||||
|
||||
func TestGetThrottle(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1"
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().GetResource(gomock.Any(), resourceID, "").Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
result, rerr := diskClient.Get(context.TODO(), "rg", "disk1")
|
||||
assert.Empty(t, result)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
disk := getTestDisk("disk1")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PutResource(gomock.Any(), pointer.StringDeref(disk.ID, ""), disk).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
rerr := diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk)
|
||||
assert.Nil(t, rerr)
|
||||
|
||||
response = &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
armClient.EXPECT().PutResource(gomock.Any(), pointer.StringDeref(disk.ID, ""), disk).Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
rerr = diskClient.CreateOrUpdate(context.TODO(), "rg", "disk1", disk)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resourceID := "/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/disk1"
|
||||
diskUpdate := getTestDiskUpdate()
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
response := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
|
||||
}
|
||||
armClient.EXPECT().PatchResource(gomock.Any(), resourceID, diskUpdate).Return(response, nil).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
rerr := diskClient.Update(context.TODO(), "rg", "disk1", diskUpdate)
|
||||
assert.Nil(t, rerr)
|
||||
|
||||
response = &http.Response{
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
|
||||
}
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
|
||||
armClient.EXPECT().PatchResource(gomock.Any(), resourceID, diskUpdate).Return(response, throttleErr).Times(1)
|
||||
armClient.EXPECT().CloseResponse(gomock.Any(), gomock.Any()).Times(1)
|
||||
rerr = diskClient.Update(context.TODO(), "rg", "disk1", diskUpdate)
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func getTestDiskUpdate() compute.DiskUpdate {
|
||||
return compute.DiskUpdate{
|
||||
DiskUpdateProperties: &compute.DiskUpdateProperties{
|
||||
DiskSizeGB: pointer.Int32(100),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
r := getTestDisk("disk1")
|
||||
armClient := mockarmclient.NewMockInterface(ctrl)
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(r.ID, ""), "").Return(nil).Times(1)
|
||||
|
||||
diskClient := getTestDiskClient(armClient)
|
||||
rerr := diskClient.Delete(context.TODO(), "rg", "disk1")
|
||||
assert.Nil(t, rerr)
|
||||
|
||||
throttleErr := &retry.Error{
|
||||
HTTPStatusCode: http.StatusTooManyRequests,
|
||||
RawError: fmt.Errorf("error"),
|
||||
Retriable: true,
|
||||
RetryAfter: time.Unix(100, 0),
|
||||
}
|
||||
armClient.EXPECT().DeleteResource(gomock.Any(), pointer.StringDeref(r.ID, ""), "").Return(throttleErr).Times(1)
|
||||
rerr = diskClient.Delete(context.TODO(), "rg", "disk1")
|
||||
assert.Equal(t, throttleErr, rerr)
|
||||
}
|
||||
|
||||
func getTestDisk(name string) compute.Disk {
|
||||
return compute.Disk{
|
||||
ID: pointer.String(fmt.Sprintf("/subscriptions/subscriptionID/resourceGroups/rg/providers/Microsoft.Compute/disks/%s", name)),
|
||||
Name: pointer.String(name),
|
||||
Location: pointer.String("eastus"),
|
||||
}
|
||||
}
|
||||
|
||||
func getTestDiskClient(armClient armclient.Interface) *Client {
|
||||
rateLimiterReader, rateLimiterWriter := azclients.NewRateLimiter(&azclients.RateLimitConfig{})
|
||||
return &Client{
|
||||
armClient: armClient,
|
||||
subscriptionID: "subscriptionID",
|
||||
rateLimiterReader: rateLimiterReader,
|
||||
rateLimiterWriter: rateLimiterWriter,
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package diskclient implements the client for Disks.
|
||||
package diskclient // import "k8s.io/legacy-cloud-providers/azure/clients/diskclient"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user