diff --git a/plugins/kb-adapter-rbdplugin/.gitignore b/plugins/kb-adapter-rbdplugin/.gitignore deleted file mode 100644 index 549ea2bd0..000000000 --- a/plugins/kb-adapter-rbdplugin/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Allowlisting gitignore template for GO projects prevents us -# from adding various unwanted local files, such as generated -# files, developer configurations or IDE-specific files etc. -# -# Recommended: Go.AllowList.gitignore - -# Ignore everything -* - -# But not these files... -!/.gitignore - -!*.go -!go.sum -!go.mod - -!README.md -!LICENSE - -# Files -!*.yaml -!Dockerfile -!Makefile -!*.sh - -# Test files -!*_test.go - -# Folders -scripts/** -!doc/** - -# ...even if they are in subdirectories -!*/ \ No newline at end of file diff --git a/plugins/kb-adapter-rbdplugin/LICENSE b/plugins/kb-adapter-rbdplugin/LICENSE deleted file mode 100644 index f49a4e16e..000000000 --- a/plugins/kb-adapter-rbdplugin/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. \ No newline at end of file diff --git a/plugins/kb-adapter-rbdplugin/LICENSES/apecloud/kubeblocks/LICENSE b/plugins/kb-adapter-rbdplugin/LICENSES/apecloud/kubeblocks/LICENSE deleted file mode 100644 index be3f7b28e..000000000 --- a/plugins/kb-adapter-rbdplugin/LICENSES/apecloud/kubeblocks/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/plugins/kb-adapter-rbdplugin/Makefile b/plugins/kb-adapter-rbdplugin/Makefile deleted file mode 100644 index 2e9b81f25..000000000 --- a/plugins/kb-adapter-rbdplugin/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# 默认目标 -.DEFAULT_GOAL := image - -# 测试目录,默认递归所有目录 -TESTDIR ?= ./... - -# 镜像标签,默认 latest -TAG ?= latest - -# 构建 Docker 镜像 -.PHONY: image -image: - deploy/docker/build.sh $(TAG) - -# 构建 Go 可执行文件 -.PHONY: build -build: - go mod download - go build -o bin/kb-adapter main.go - -# 运行测试 -.PHONY: test -test: - go test $(TESTDIR) - -# lint -.PHONY: lint -lint: - golangci-lint run - -# fmt -.PHONY: fmt -fmt: - golangci-lint fmt - -# delpoy -.PHONY: deploy -deploy: - kubectl apply -f deploy/k8s/deploy.yaml \ No newline at end of file diff --git a/plugins/kb-adapter-rbdplugin/README.md b/plugins/kb-adapter-rbdplugin/README.md deleted file mode 100644 index 053f5618f..000000000 --- a/plugins/kb-adapter-rbdplugin/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# [Kubeblock Adapter for Rainbond Plugin](https://github.com/furutachiKurea/kb-adapter-rbdplugin)(原 Block Mechanica) - -> 本项目原名为 "Block Mechanica",现已更名为 "Kubeblock Adapter for Rainbond Plugin", -> 后续项目中两个名称也许会共存,应认为其等价 - -Kubeblock Adapter for Rainbond Plugin Block Mechanica 是一个轻量化的 Kubernetes 服务,通过使用 Echo 编写的 API 服务实现 KubeBlocks 与 Rainbond 的集成 - -## How does it work? - -[如何实现 Rainbond 与 KubeBlocks 的集成](./doc/design_document.md) - -## 如何部署 - -[在 Rainbond 中部署 KubeBlocks 和 Kubeblock Adapter for Rainbond Plugin ](./doc/Deploy.md) - -## 如何在 Rainbond 中使用 KubeBlocks - -绝大部分情况下,都能像使用 Rainbond 组件一样使用通过 KubeBlocks 创建的数据库 - -当然也存在一些不同,详见 [在 Rainbond 中使用 KubeBlocks](./doc/Use_KubeBlocks_in_Rainbond.md) - -## 目录结构 - -```txt -📁 ./ -├── 📁 api/ -│ ├── 📁 handler/ -│ ├── 📁 req/ -│ └── 📁 res/ -├── 📁 deploy/ -│ ├── 📁 docker/ -│ └── 📁 k8s/ -├── 📁 doc/ -│ └── 📁 assets/ -├── 📁 internal/ -│ ├── 📁 config/ -│ ├── 📁 index/ -│ ├── 📁 k8s/ -│ ├── 📁 log/ -│ ├── 📁 model/ -│ ├── 📁 mono/ -│ └── 📁 testutil/ -└── 📁 service/ - ├── 📁 adapter/ - ├── 📁 backup/ - ├── 📁 builder/ - ├── 📁 cluster/ - ├── 📁 coordinator/ - ├── 📁 kbkit/ - ├── 📁 registry/ - └── 📁 resource/ -``` - -## Make - -- 构建 Docker 镜像(默认标签 latest) - - ```sh - make image - ``` - -- 构建 Docker 镜像并指定标签(如 v1.0.0) - - ```sh - make image TAG=v1.0.0 - ``` - -- 构建可执行文件到 bin/kb-adapter - - ```sh - make build - ``` - -- 运行所有测试 - - ```sh - make test - ``` - -- 运行指定目录下的测试(如 service 目录) - - ```sh - make test TESTDIR=./service/... - ``` - -## Contributing - -[开发仓库](https://github.com/furutachiKurea/kb-adapter-rbdplugin) - -欢迎提交 PR 和 Issue,感谢您的贡献! - -## License - -[Apache 2.0](./LICENSE) \ No newline at end of file diff --git a/plugins/kb-adapter-rbdplugin/api/handler/handler.go b/plugins/kb-adapter-rbdplugin/api/handler/handler.go deleted file mode 100644 index 51552a051..000000000 --- a/plugins/kb-adapter-rbdplugin/api/handler/handler.go +++ /dev/null @@ -1,481 +0,0 @@ -package handler - -import ( - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/api/req" - "github.com/furutachiKurea/kb-adapter-rbdplugin/api/res" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service" - - "github.com/labstack/echo/v4" -) - -// Handler 处理 API 请求,集合了 Block Mechanica 的各个服务 -type Handler struct { - svc service.Services -} - -// NewHandler - -func NewHandler(svc service.Services) *Handler { - return &Handler{svc: svc} -} - -// GetAddons 获取 Block Mechanica 支持的数据库类型 -// -// GET /v1/addons -func (h *Handler) GetAddons(c echo.Context) error { - ctx := c.Request().Context() - - addons, err := h.svc.GetAddons(ctx) - if err != nil { - return res.InternalError(err) - } - return res.ReturnSuccess(c, addons) -} - -// GetStorageClasses 集群中的 StorageClass -// -// GET /v1/storageclasses -func (h *Handler) GetStorageClasses(c echo.Context) error { - ctx := c.Request().Context() - storageClasses, err := h.svc.GetStorageClasses(ctx) - if err != nil { - return res.InternalError(fmt.Errorf("get storage classes: %w", err)) - } - return res.ReturnSuccess(c, storageClasses) -} - -// GetBackupRepos 集群中设置的 BackupRepo -// -// GET /v1/backuprepos -func (h *Handler) GetBackupRepos(c echo.Context) error { - ctx := c.Request().Context() - repos, err := h.svc.ListAvailableBackupRepos(ctx) - if err != nil { - return res.InternalError(fmt.Errorf("list available backup repos: %w", err)) - } - return res.ReturnSuccess(c, repos) -} - -// CreateBackupRepo 创建 BackupRepo 与其凭据 Secret。 -// -// POST /v1/backuprepos -func (h *Handler) CreateBackupRepo(c echo.Context) error { - ctx := c.Request().Context() - - var request model.BackupRepoInput - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - repo, err := h.svc.CreateBackupRepo(ctx, request) - if err != nil { - return res.InternalError(fmt.Errorf("create backup repo: %w", err)) - } - return res.ReturnSuccess(c, repo) -} - -// UpdateBackupRepo 更新 BackupRepo。请求中未传 secrets 时保留现有凭据。 -// -// PUT /v1/backuprepos/:name -func (h *Handler) UpdateBackupRepo(c echo.Context) error { - ctx := c.Request().Context() - - var request model.BackupRepoInput - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - repo, err := h.svc.UpdateBackupRepo(ctx, c.Param("name"), request) - if err != nil { - return res.InternalError(fmt.Errorf("update backup repo: %w", err)) - } - return res.ReturnSuccess(c, repo) -} - -// DeleteBackupRepo 删除 BackupRepo 与其凭据 Secret。 -// -// DELETE /v1/backuprepos/:name -func (h *Handler) DeleteBackupRepo(c echo.Context) error { - ctx := c.Request().Context() - - if err := h.svc.DeleteBackupRepo(ctx, c.Param("name")); err != nil { - return res.InternalError(fmt.Errorf("delete backup repo: %w", err)) - } - return res.ReturnSuccess(c, "Done") -} - -// CreateCluster 创建 KubeBlocks 数据库集群 -// -// POST /v1/clusters -// -// 完成之后不保证 Cluster 与 KubeBlocks Component 就绪 -func (h *Handler) CreateCluster(c echo.Context) error { - ctx := c.Request().Context() - - var request req.ClusterRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - log.Info("CreateCluster", log.Any("request", request)) - - modelReq := model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: request.Name, - Namespace: request.Namespace, - Type: request.Type, - Version: request.Version, - StorageClass: request.StorageClass, - TerminationPolicy: request.TerminationPolicy, - }, - ClusterResource: model.ClusterResource{ - CPU: request.CPU, - Memory: request.Memory, - Storage: request.Storage, - Replicas: request.Replicas, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: request.BackupRepo, - Schedule: model.BackupSchedule{ - Frequency: request.Schedule.Frequency, - DayOfWeek: request.Schedule.DayOfWeek, - Hour: request.Schedule.Hour, - Minute: request.Schedule.Minute, - }, - RetentionPeriod: request.RetentionPeriod, - }, - RBDService: model.RBDService{ - ServiceID: request.RBDService.ServiceID, - }, - } - - cluster, err := h.svc.CreateCluster(ctx, modelReq) - if err != nil { - return res.InternalError(fmt.Errorf("create cluster: %w", err)) - } - - return res.ReturnSuccess(c, cluster) -} - -// CancelClusterCreate 取消集群创建 -// -// DELETE /v1/clusters/:service-id -func (h *Handler) CancelClusterCreate(c echo.Context) error { - ctx := c.Request().Context() - - var request req.ClusterRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - rbdService := model.RBDService{ - ServiceID: request.RBDService.ServiceID, - } - - if err := h.svc.CancelClusterCreate(ctx, rbdService); err != nil { - return res.InternalError(fmt.Errorf("cancel cluster create: %w", err)) - } - - return res.ReturnSuccess(c, "Cancled") -} - -// DeleteCluster 删除 KubeBlocks 数据库集群 -// -// DELETE /v1/clusters -func (h *Handler) DeleteCluster(c echo.Context) error { - ctx := c.Request().Context() - - var request req.DeleteClustersRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - if err := h.svc.DeleteClusters(ctx, request.ServiceIDs); err != nil { - return res.InternalError(fmt.Errorf("delete clusters: %w", err)) - } - - return res.ReturnSuccess(c, "Deleted") -} - -// GetConnectInfo 获取 KubeBlocks 数据库集群的连接信息 -// -// GET /v1/clusters/connect-info -func (h *Handler) GetConnectInfo(c echo.Context) error { - ctx := c.Request().Context() - - var request req.ClusterRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - connectInfos, err := h.svc.GetConnectInfo(ctx, request.RBDService) - if err != nil { - return res.InternalError(fmt.Errorf("get connect info: %w", err)) - } - - response := &res.ConnectInfoRes{ - ConnectInfos: connectInfos, - Port: h.svc.GetClusterPort(ctx, request.RBDService.ServiceID), - } - - return res.ReturnSuccess(c, response) -} - -// GetClusterDetail 获取 KubeBlocks 数据库集群的详细信息 -// -// GET /v1/clusters/:service-id -func (h *Handler) GetClusterDetail(c echo.Context) error { - ctx := c.Request().Context() - - var rbdService model.RBDService - if err := c.Bind(&rbdService); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - detail, err := h.svc.GetClusterDetail(ctx, rbdService) - if err != nil { - return res.InternalError(fmt.Errorf("get cluster detail: %w", err)) - } - - return res.ReturnSuccess(c, detail) -} - -// ExpansionCluster 对 KubeBlocks 数据库集群进行伸缩操作 -// -// PUT /v1/clusters/:service-id/expansions -func (h *Handler) ExpansionCluster(c echo.Context) error { - ctx := c.Request().Context() - - var request req.ClusterRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - modelReq := model.ExpansionInput{ - RBDService: request.RBDService, - ClusterResource: model.ClusterResource{ - CPU: request.CPU, - Memory: request.Memory, - Storage: request.Storage, - Replicas: request.Replicas, - }, - } - - if err := h.svc.ExpansionCluster(ctx, modelReq); err != nil { - return res.InternalError(fmt.Errorf("expansion cluster: %w", err)) - } - - return res.ReturnSuccess(c, "Done") -} - -// ReScheduleBackup 重新调度 KubeBlocks 数据库集群的备份配置 -// -// PUT /v1/clusters/:service-id/backup-schedules -func (h *Handler) ReScheduleBackup(c echo.Context) error { - ctx := c.Request().Context() - - var request req.BackupRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - schedule := model.BackupScheduleInput{ - RBDService: request.RBDService, - ClusterBackup: model.ClusterBackup{ - BackupRepo: request.BackupRepo, - Schedule: request.Schedule, - RetentionPeriod: request.RetentionPeriod, - }, - } - - if err := h.svc.ReScheduleBackup(ctx, schedule); err != nil { - return res.InternalError(fmt.Errorf("reschedule backupe: %w", err)) - } - - return res.ReturnSuccess(c, "Done") -} - -// GetBackups 获取 KubeBlocks 数据库集群的备份列表 -// -// GET /v1/clusters/:service-id/backups -func (h *Handler) GetBackups(c echo.Context) error { - ctx := c.Request().Context() - - var query model.BackupListQuery - if err := c.Bind(&query); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - result, err := h.svc.ListBackups(ctx, query) - if err != nil { - return res.InternalError(fmt.Errorf("get backup list: %w", err)) - } - - return res.ReturnList(c, result.Total, query.Page, result.Items) -} - -// CreateBackup 创建 KubeBlocks 数据库集群的备份 -// -// POST /v1/clusters/:service-id/backups -func (h *Handler) CreateBackup(c echo.Context) error { - ctx := c.Request().Context() - - var request req.BackupRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - backupInput := model.BackupInput{ - RBDService: request.RBDService, - } - - if err := h.svc.BackupCluster(ctx, backupInput); err != nil { - return res.InternalError(fmt.Errorf("create backup: %w", err)) - } - - return res.ReturnSuccess(c, "Done") -} - -// DeleteBackups 批量删除 KubeBlocks 数据库集群的指定备份 -// -// DELETE /v1/clusters/:service-id/backups -func (h *Handler) DeleteBackups(c echo.Context) error { - ctx := c.Request().Context() - - var request req.DeleteBackupsRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - log.Info("Wanted to delete backups", log.Any("backups", request.Backups)) - - deleted, err := h.svc.DeleteBackups(ctx, request.RBDService, request.Backups) - if err != nil { - return res.InternalError(fmt.Errorf("delete backups: %w", err)) - } - - log.Info("Deleted backups", log.Any("deleted", deleted)) - - return res.ReturnSuccess(c, deleted) -} - -func (h *Handler) ManageCluster(c echo.Context) error { - ctx := c.Request().Context() - - var request req.ManageClusterLifecycleRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - result := h.svc.ManageClustersLifecycle(ctx, request.ManageClusterType(), request.ServiceIDs) - if len(result.Succeeded) == 0 { - return res.InternalError(res.NewBatchOperationError("manage clusters", result.Failed)) - } - - return res.ReturnSuccess(c, result) -} - -func (h *Handler) GetPodDetail(c echo.Context) error { - ctx := c.Request().Context() - - var request req.GetPodDetailRequest - log.Debug("GetPodDetail", log.Any("request", request)) - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - podDetail, err := h.svc.GetPodDetail(ctx, request.ServiceID, request.PodName) - if err != nil { - return res.InternalError(fmt.Errorf("get pod detail: %w", err)) - } - - return res.ReturnSuccess(c, podDetail) -} - -func (h *Handler) GetClusterEvents(c echo.Context) error { - ctx := c.Request().Context() - - var request req.GetClusterEventsRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - result, err := h.svc.GetClusterEvents(ctx, request.ServiceID, request.Pagination) - if err != nil { - return res.InternalError(fmt.Errorf("get cluster events: %w", err)) - } - - return res.ReturnList(c, result.Total, request.Page, result.Items) -} - -// GetClusterParameters 返回 service-id 对应的 KubeBlocks Cluster 的参数设置, -// 返回的数据结构还包括参数的约束。 -// ErrTargetNotFound 错误表示该数据库不支持参数设置,不应作为业务错误处理,只返回空列表。 -// -// GET /v1/clusters/:service-id/parameters -func (h *Handler) GetClusterParameters(c echo.Context) error { - ctx := c.Request().Context() - - var query model.ClusterParametersQuery - if err := c.Bind(&query); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - result, err := h.svc.GetClusterParameter(ctx, query) - if err != nil { - return res.InternalError(fmt.Errorf("get cluster parameters: %w", err)) - } - - return res.ReturnList(c, result.Total, query.Page, result.Items) -} - -// ChangeClusterParameter 变更 KubeBlocks 数据库集群的参数配置 -// 无论是否有参数变更,都应返回 200 -// -// POST /v1/clusters/:service-id/parameters -func (h *Handler) ChangeClusterParameter(c echo.Context) error { - ctx := c.Request().Context() - - var change model.ClusterParametersChange - if err := c.Bind(&change); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - log.Debug("ChangeClusterParameter", log.Any("change", change)) - - result, err := h.svc.ChangeClusterParameter(ctx, change) - if err != nil { - return res.InternalError(fmt.Errorf("change cluster parameters: %w", err)) - } - - log.Debug("parameter change response", - log.String("serviceID", change.ServiceID), - log.Any("appliedCount", result.Applied), - log.Any("invalidCount", result.Invalids), - ) - - return res.ReturnSuccess(c, result) -} - -func (h *Handler) RestoreClusterFromBackup(c echo.Context) error { - ctx := c.Request().Context() - - var request req.RestoreFromBackupRequest - if err := c.Bind(&request); err != nil { - return res.BadRequest(fmt.Errorf("bind request: %w", err)) - } - - restoredCluster, err := h.svc.RestoreFromBackup(ctx, request.ServiceID, request.NewServiceID, request.BackupName) - if err != nil { - return res.InternalError(fmt.Errorf("restore cluster from backup: %w", err)) - } - - response := &res.RestoreFromBackupRes{ - NewClusterName: restoredCluster, - } - - return res.ReturnSuccess(c, response) -} diff --git a/plugins/kb-adapter-rbdplugin/api/req/req.go b/plugins/kb-adapter-rbdplugin/api/req/req.go deleted file mode 100644 index b747055ce..000000000 --- a/plugins/kb-adapter-rbdplugin/api/req/req.go +++ /dev/null @@ -1,65 +0,0 @@ -// Package req 用于处理请求 -package req - -import ( - "strings" - - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" -) - -type ClusterRequest struct { - model.ClusterInfo - model.ClusterResource - model.ClusterBackup - RBDService model.RBDService `json:"rbdService"` -} - -type BackupRequest struct { - RBDService model.RBDService `json:"rbdService"` - model.ClusterBackup -} - -type DeleteClustersRequest struct { - ServiceIDs []string `json:"serviceIDs"` -} - -type DeleteBackupsRequest struct { - model.RBDService - Backups []string `json:"backups"` -} - -type ManageClusterLifecycleRequest struct { - Operation string `json:"operation"` - ServiceIDs []string `json:"service_ids"` -} - -type GetClusterEventsRequest struct { - model.RBDService - model.Pagination -} - -type RestoreFromBackupRequest struct { - model.RBDService - NewServiceID string `json:"new_service_id"` - BackupName string `json:"backup_name"` -} - -// ManageClusterType 将 ManageClusterLifecycleRequest.Operation 转换为 OpsType -func (m *ManageClusterLifecycleRequest) ManageClusterType() opsv1alpha1.OpsType { - switch strings.TrimSpace(strings.ToLower(m.Operation)) { - case "start": - return opsv1alpha1.StartType - case "stop": - return opsv1alpha1.StopType - case "restart": - return opsv1alpha1.RestartType - default: - return opsv1alpha1.OpsType(m.Operation) - } -} - -type GetPodDetailRequest struct { - ServiceID string `json:"service_id" param:"service-id"` - PodName string `json:"pod_name" param:"pod-name"` -} diff --git a/plugins/kb-adapter-rbdplugin/api/res/errors.go b/plugins/kb-adapter-rbdplugin/api/res/errors.go deleted file mode 100644 index 0c148201c..000000000 --- a/plugins/kb-adapter-rbdplugin/api/res/errors.go +++ /dev/null @@ -1,59 +0,0 @@ -package res - -import ( - "fmt" - "net/http" - "strings" - - "github.com/labstack/echo/v4" -) - -type BatchOperationError struct { - msg string - errors map[string]error -} - -func NewBatchOperationError(msg string, errors map[string]error) *BatchOperationError { - return &BatchOperationError{ - msg: msg, - errors: errors, - } -} - -func (e *BatchOperationError) Error() string { - var errMsgs []string - for serviceID, err := range e.errors { - errMsgs = append(errMsgs, fmt.Sprintf("%s: %v", serviceID, err)) - } - return fmt.Sprintf("%s: %s", e.msg, strings.Join(errMsgs, "; ")) -} - -// newError 创建一个 echo.HTTPError -func newError(code int, message string) error { - return echo.NewHTTPError(code, message) -} - -func InternalError(err error) error { - message := err.Error() - return newError(http.StatusInternalServerError, message) -} - -func BadRequest(err error) error { - message := err.Error() - return newError(http.StatusBadRequest, message) -} - -func NotFound(err error) error { - message := err.Error() - return newError(http.StatusNotFound, message) -} - -func Unauthorized(err error) error { - message := err.Error() - return newError(http.StatusUnauthorized, message) -} - -func Forbidden(err error) error { - message := err.Error() - return newError(http.StatusForbidden, message) -} diff --git a/plugins/kb-adapter-rbdplugin/api/res/res.go b/plugins/kb-adapter-rbdplugin/api/res/res.go deleted file mode 100644 index 3b0cab07a..000000000 --- a/plugins/kb-adapter-rbdplugin/api/res/res.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package res 用于构建响应和定义响应结构体 -// -// 在 handler 中应当使用 res 包返回响应或错误 -package res - -import ( - "net/http" - "reflect" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/labstack/echo/v4" -) - -type ConnectInfoRes struct { - ConnectInfos []model.ConnectInfo `json:"connect_infos"` - Port int `json:"port"` -} - -type RestoreFromBackupRes struct { - NewClusterName string `json:"new_service"` -} - -// Response 定义用于正确返回的 JSON -type Response struct { - Bean any `json:"bean,omitempty"` - List any `json:"list,omitempty"` - ListAllNumber int `json:"number,omitempty"` - Page int `json:"page,omitempty"` -} - -// ReturnSuccess - -func ReturnSuccess(c echo.Context, data any) error { - if data == nil { - return c.JSON(http.StatusOK, Response{Bean: nil}) - } - - // TODO 优化反射 - v := reflect.ValueOf(data) - if v.Kind() == reflect.Slice { - return c.JSON(http.StatusOK, Response{List: data}) - } - - return c.JSON(http.StatusOK, Response{Bean: data}) -} - -// ReturnList 返回分页列表 -func ReturnList(c echo.Context, total, page int, list any) error { - return c.JSON(http.StatusOK, Response{ - List: list, - ListAllNumber: total, - Page: page, - }) -} diff --git a/plugins/kb-adapter-rbdplugin/api/router.go b/plugins/kb-adapter-rbdplugin/api/router.go deleted file mode 100644 index 1a377c9ea..000000000 --- a/plugins/kb-adapter-rbdplugin/api/router.go +++ /dev/null @@ -1,60 +0,0 @@ -package api - -import ( - "net/http" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/config" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/api/handler" - - "github.com/labstack/echo/v4" -) - -// setupRouter 设置路由 -func setupRouter(v1 *echo.Group, h *handler.Handler) { - v1.GET("/addons", h.GetAddons) - v1.GET("/storageclasses", h.GetStorageClasses) - v1.GET("/backuprepos", h.GetBackupRepos) - v1.POST("/backuprepos", h.CreateBackupRepo) - v1.PUT("/backuprepos/:name", h.UpdateBackupRepo) - v1.DELETE("/backuprepos/:name", h.DeleteBackupRepo) - - cluster := v1.Group("/clusters") - { - cluster.POST("", h.CreateCluster) - cluster.DELETE("", h.DeleteCluster) - cluster.GET("/connect-infos", h.GetConnectInfo) - cluster.GET("/:service-id", h.GetClusterDetail) - cluster.PUT("/:service-id", h.ExpansionCluster) - cluster.PUT("/:service-id/backup-schedules", h.ReScheduleBackup) - cluster.GET("/:service-id/backups", h.GetBackups) - cluster.POST("/:service-id/backups", h.CreateBackup) - cluster.DELETE("/:service-id/backups", h.DeleteBackups) - cluster.POST("/actions", h.ManageCluster) - cluster.GET("/:service-id/pods/:pod-name/details", h.GetPodDetail) - cluster.GET("/:service-id/events", h.GetClusterEvents) - cluster.GET("/:service-id/parameters", h.GetClusterParameters) - cluster.POST("/:service-id/parameters", h.ChangeClusterParameter) - cluster.POST("/:service-id/restores", h.RestoreClusterFromBackup) - } -} - -// setupHealthRoutes 健康检查路由 -func setupHealthRoutes(e *echo.Echo, cfg *config.ServerConfig) { - // ready - e.GET(cfg.ReadinessPath, func(c echo.Context) error { - return c.JSON(http.StatusOK, echo.Map{ - "status": "ready", - "timestamp": time.Now().Unix(), - }) - }) - - // live - e.GET(cfg.LivenessPath, func(c echo.Context) error { - return c.JSON(http.StatusOK, echo.Map{ - "status": "alive", - "timestamp": time.Now().Unix(), - }) - }) -} diff --git a/plugins/kb-adapter-rbdplugin/api/server.go b/plugins/kb-adapter-rbdplugin/api/server.go deleted file mode 100644 index ee56bc29a..000000000 --- a/plugins/kb-adapter-rbdplugin/api/server.go +++ /dev/null @@ -1,128 +0,0 @@ -// Package api Block Mechanica 提供的 API 服务 -package api - -import ( - "context" - "errors" - "net/http" - - "github.com/creasty/defaults" - "github.com/furutachiKurea/kb-adapter-rbdplugin/api/handler" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/config" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - ctrl "sigs.k8s.io/controller-runtime" -) - -// Server 将 API 服务封装为 controller-runtime 的 Runnable -type Server struct { - echo *echo.Echo - handler *handler.Handler - config *config.ServerConfig -} - -func NewAPIServer(h *handler.Handler) *Server { - e := echo.New() - cfg := config.MustLoad() - return &Server{echo: e, handler: h, config: cfg} -} - -// Start 实现 manager.Runnable -func (r *Server) Start(ctx context.Context) error { - if err := StartServerWithConfig(ctx, r.echo, r.handler, r.config); err != nil { - return err - } - return nil -} - -// NeedLeaderElection 实现 manager.LeaderElectionRunnable -func (r *Server) NeedLeaderElection() bool { - return false -} - -// RegisterServer 创建 Server 并注册至 manager -func RegisterServer(ctx context.Context, mgr ctrl.Manager, svcs service.Services) error { - h := handler.NewHandler(svcs) - apiServer := NewAPIServer(h) - return mgr.Add(apiServer) -} - -// StartServerWithConfig 使用配置启动服务器 -func StartServerWithConfig(ctx context.Context, e *echo.Echo, handler *handler.Handler, cfg *config.ServerConfig) error { - // custom echo - e.HTTPErrorHandler = customErrorHandler() - e.Binder = customBinder() - - // Middleware - e.Use(middleware.Recover()) - e.Use(log.EchoZap()) // 使用 zap 日志中间件 - - // 健康检查路由 - setupHealthRoutes(e, cfg) - - // 设置路由 - v1 := e.Group("/v1") - setupRouter(v1, handler) - - // 启动服务器 - 直接启动,让 controller-runtime 管理生命周期 - serverAddr := cfg.Host + ":" + cfg.Port - return e.Start(serverAddr) -} - -// customErrorHandler 自定义 echo.HTTPErrorHandler -// -// 将错误处理返回的 JSON 格式设置为 -// -// { -// "code": code, -// "msg": msg, -// } -func customErrorHandler() echo.HTTPErrorHandler { - return func(err error, c echo.Context) { - code := http.StatusInternalServerError - msg := "Internal Server Error" - var he *echo.HTTPError - if errors.As(err, &he) { - code = he.Code - if m, ok := he.Message.(string); ok { - msg = m - } else if m, ok := he.Message.(error); ok { - msg = m.Error() - } - } - _ = c.JSON(code, echo.Map{ - "code": code, - "msg": msg, - }) - } -} - -// customBinder 自定义 echo.Binder -// -// 使用 creasty/defaults 设置默认值,在结构体中通过 `default` 标签设置 -func customBinder() echo.Binder { - return &defaultsBinder{ - binder: &echo.DefaultBinder{}, - } -} - -// defaultsBinder 自定义 echo.Binder, 允许使用 creasty/defaults 设置默认值 -type defaultsBinder struct { - binder *echo.DefaultBinder -} - -func (b *defaultsBinder) Bind(i any, c echo.Context) error { - // 标准绑定:处理 JSON、查询参数、路径参数等 - if err := b.binder.Bind(i, c); err != nil { - return err - } - - // 默认值设置:为未提供的字段设置默认值 - if err := defaults.Set(i); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "failed to set defaults: "+err.Error()) - } - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/deploy/docker/Dockerfile b/plugins/kb-adapter-rbdplugin/deploy/docker/Dockerfile deleted file mode 100644 index d406935c2..000000000 --- a/plugins/kb-adapter-rbdplugin/deploy/docker/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:1.24 AS builder - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o kb-adapter main.go - -FROM gcr.io/distroless/static - -WORKDIR / - -COPY --from=builder /app/kb-adapter /kb-adapter - -EXPOSE 8080 - -ENTRYPOINT ["/kb-adapter"] diff --git a/plugins/kb-adapter-rbdplugin/deploy/docker/build.sh b/plugins/kb-adapter-rbdplugin/deploy/docker/build.sh deleted file mode 100755 index bb490c8ae..000000000 --- a/plugins/kb-adapter-rbdplugin/deploy/docker/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# 设置变量 -IMAGE_NAME="kb-adapter" -TAG=${1:-latest} -DOCKERFILE_PATH="deploy/docker/Dockerfile" - -echo "开始构建 Docker 镜像..." -echo "镜像名称: ${IMAGE_NAME}" -echo "标签: ${TAG}" -echo "Dockerfile 路径: ${DOCKERFILE_PATH}" - -# 构建镜像 -docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${TAG} . - -if [ $? -eq 0 ]; then - echo "镜像构建成功!" - echo "镜像信息:" - docker images ${IMAGE_NAME}:${TAG} - - echo "" - echo "运行命令示例:" - echo "docker run -p 8080:8080 ${IMAGE_NAME}:${TAG}" -else - echo "镜像构建失败!" - exit 1 -fi diff --git a/plugins/kb-adapter-rbdplugin/deploy/k8s/deploy.yaml b/plugins/kb-adapter-rbdplugin/deploy/k8s/deploy.yaml deleted file mode 100644 index 4e01f0752..000000000 --- a/plugins/kb-adapter-rbdplugin/deploy/k8s/deploy.yaml +++ /dev/null @@ -1,151 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kb-adapter-rbdplugin - namespace: rbd-plugins -spec: - replicas: 1 - selector: - matchLabels: - app: kb-adapter-rbdplugin - template: - metadata: - labels: - app: kb-adapter-rbdplugin - spec: - serviceAccountName: kb-adapter-rbdplugin-sa - containers: - - name: kb-adapter-rbdplugin - image: crpi-aoz6mrz2bbre0gxx.cn-hangzhou.personal.cr.aliyuncs.com/block-mechanica/kb-adapter:v0.1.0 - ports: - - containerPort: 8080 - envFrom: - - configMapRef: - name: kb-adapter-rbdplugin-config - env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - livenessProbe: - httpGet: - path: /livez - port: 8080 - initialDelaySeconds: 30 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - resources: - requests: - cpu: "100m" - memory: "128Mi" - limits: - cpu: "500m" - memory: "512Mi" - ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: kb-adapter-rbdplugin-config - namespace: rbd-plugins -data: - HOST: "0.0.0.0" - PORT: "8080" - SERVICE_NAME: "kb-adapter-rbdplugin" - READINESS_PATH: "/readyz" - LIVENESS_PATH: "/livez" - ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: kb-adapter-rbdplugin-sa - namespace: rbd-plugins ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: kb-adapter-rbdplugin-role -rules: - - apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["configmaps"] - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "list", "watch", "create", "update", "delete"] - - apiGroups: [""] - resources: ["events"] - verbs: ["list", "watch", "create"] - - apiGroups: ["apps"] - resources: ["deployments"] - verbs: ["list", "watch"] - - apiGroups: ["storage.k8s.io"] - resources: ["storageclasses"] - verbs: ["get", "list", "watch"] - - apiGroups: ["apps.kubeblocks.io"] - resources: ["clusters"] - verbs: ["get", "list", "create", "delete", "patch", "watch"] - - apiGroups: ["apps.kubeblocks.io"] - resources: ["componentdefinitions"] - verbs: ["get", "list", "watch"] - - apiGroups: ["apps.kubeblocks.io"] - resources: ["componentversions"] - verbs: ["list", "watch"] - - apiGroups: ["operations.kubeblocks.io"] - resources: ["opsrequests"] - verbs: ["get", "list", "create", "delete", "patch", "watch"] - - apiGroups: ["dataprotection.kubeblocks.io"] - resources: ["backups"] - verbs: ["list", "delete", "watch"] - - apiGroups: ["dataprotection.kubeblocks.io"] - resources: ["backuprepos"] - verbs: ["get", "list", "watch", "create", "update", "delete"] - - apiGroups: ["workloads.kubeblocks.io"] - resources: ["instancesets"] - verbs: ["list", "watch"] - - apiGroups: ["parameters.kubeblocks.io"] - resources: ["paramconfigrenderers"] - verbs: ["list", "watch"] - - apiGroups: ["parameters.kubeblocks.io"] - resources: ["parametersdefinitions"] - verbs: ["get", "list", "watch"] - - apiGroups: ["coordination.k8s.io"] - resources: ["leases"] - verbs: ["get", "list", "watch", "create", "update", "patch"] - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: kb-adapter-rbdplugin-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: kb-adapter-rbdplugin-role -subjects: - - kind: ServiceAccount - name: kb-adapter-rbdplugin-sa - namespace: rbd-plugins - ---- -apiVersion: v1 -kind: Service -metadata: - name: kb-adapter-rbdplugin - namespace: rbd-plugins -spec: - selector: - app: kb-adapter-rbdplugin - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - type: ClusterIP diff --git a/plugins/kb-adapter-rbdplugin/doc/Deploy.md b/plugins/kb-adapter-rbdplugin/doc/Deploy.md deleted file mode 100644 index 19b4cd19d..000000000 --- a/plugins/kb-adapter-rbdplugin/doc/Deploy.md +++ /dev/null @@ -1,169 +0,0 @@ -# 在 Rainbond 中部署 KubeBlocks - -> [KubeBlocks](https://github.com/apecloud/kubeblocks) is an open-source Kubernetes operator for databases (more specifically, for stateful applications, including databases and middleware like message queues), enabling users to run and manage multiple types of databases on Kubernetes. - -你可以通过 [Kubeblock Adapter for Rainbond Plugin](https://github.com/furutachiKurea/kb-adapter-rbdplugin) 实现 KubeBlocks 在 Rainbond 中的集成。在绝大部分情况下,你都能像使用 Rainbond 组件一样管理通过 KubeBlocks 创建的数据库 - -## 安装 KubeBlocks - -安装过程参考 KubeBlocks 的[安装文档](https://kubeblocks.io/docs/release-1_0_1/user_docs/overview/install-kubeblocks),这里我们简要说明通过 Helm 安装 KubeBlocks 的步骤,下面的内容大部分来自 [KubeBlocks 官方文档](https://kubeblocks.io/docs/release-1_0_1/user_docs/overview/introduction) - -### [前提条件](https://kubeblocks.io/docs/release-1_0_1/user_docs/overview/install-kubeblocks#prerequisites) - -| Component 组件 | Database 数据库 | Recommendation 推荐配置 | -| :---------------- | :-------------- | :---------------------------------------- | -| **Control Plane** | - | 1 个节点(4 核,4 GB 内存,50GB 存储) | -| **Data Plane ** | MySQL | 2 个节点(2 核 CPU,4GB 内存,50GB 存储) | -| | PostgreSQL | 2 个节点(2 核 CPU,4GB 内存,50GB 存储) | -| | Redis | 2 个节点(2 核 CPU,4GB 内存,50GB 存储) | -| | MongoDB | 3 个节点(2 核 CPU,4GB 内存,50GB 存储) | - -> - Kubernetes 集群(建议 v1.21+版本)——如需可创建测试集群 -> - `kubectl` 已安装并配置 v1.21+版本,具备集群访问权限 -> - 已安装 Helm([安装指南](https://helm.sh/docs/intro/install/)) -> - 已安装快照控制器 ([安装指南](https://kubeblocks.io/docs/release-1_0_1/user_docs/references/install-snapshot-controller)) - -### 安装 KubeBlocks - -```shell -# 安装 CRDs -kubectl create -f https://github.com/apecloud/kubeblocks/releases/download/v1.0.1/kubeblocks_crds.yaml - -# 设置 Helm Repository -helm repo add kubeblocks https://apecloud.github.io/helm-charts -helm repo update - -# 部署 KubeBlocks -helm install kubeblocks kubeblocks/kubeblocks --namespace kb-system --create-namespace --version=v1.0.1 - -# 可以设置使用 KubeBlocks 提供的镜像源 -helm install kubeblocks kubeblocks/kubeblocks --namespace kb-system --create-namespace --version=v1.0.1 \ ---set image.registry=apecloud-registry.cn-zhangjiakou.cr.aliyuncs.com \ ---set dataProtection.image.registry=apecloud-registry.cn-zhangjiakou.cr.aliyuncs.com \ ---set addonChartsImage.registry=apecloud-registry.cn-zhangjiakou.cr.aliyuncs.com -``` - -**注意,KubeBlocks 的 Addon 需要单独设置镜像源**, 参见: - -可以在部署时通过指定配置文件来自动创建 BackupRepo: - -在部署时创建 BackupRepo 会[简单](#配置-backuprepo)很多,下面以 Rainbond 自动创建的 minio 为例: - -Rainbond 自动创建的 minio 账号密码为: `admin/admin1234`,你需要手动创建 `ACCESS KEY` 和 `SECRET KEY` 并创建一个 Bucket - -```yaml -# backuprepo.yaml -backupRepo: - create: true - storageProvider: minio - config: - bucket: - endpoint: http://minio-service.rbd-system.svc.cluster.local:9000 - secrets: - accessKeyId: - secretAccessKey: -``` - -部署时使用 - -```shell -helm install kubeblocks kubeblocks/kubeblocks --namespace kb-system --create-namespace --version=v1.0.1 \ --f backuprepo.yaml -``` - -### 验证安装 - -执行: - -```shell -kubectl -n kb-system get pods -``` - -预期输出: - -```shell -NAME READY STATUS RESTARTS AGE -kubeblocks-7cf7745685-ddlwk 1/1 Running 0 4m39s -kubeblocks-dataprotection-95fbc79cc-b544l 1/1 Running 0 4m39s -``` - -如果 KubeBlocks 工作负载全部就绪,则表示 KubeBlocks 已成功安装。 - -如果你没有在安装 KubeBlocks 时跳过 Addon 的自动安装的话,KubeBlocks 会自动安装一部分 Addon - -**注意**:在 Rainbond 上能够使用的数据库类型取决于你安装的 KubeBlocks Addon 和 Block Mechanica 的支持,目前支持 MySQL semisync、PostgreSQL、Redis replication、RabbitMQ - -### 配置 [BackupRepo](https://kubeblocks.io/docs/release-1_0_1/user_docs/concepts/backup-and-restore/backup/backup-repo) - -> backupRepo is the storage repository for backup data. Currently, KubeBlocks supports configuring various object storage services as backup repositories, including OSS (Alibaba Cloud Object Storage Service), S3 (Amazon Simple Storage Service), COS (Tencent Cloud Object Storage), GCS (Google Cloud Storage), OBS (Huawei Cloud Object Storage), Azure Blob Storage, MinIO, and other S3-compatible services. - -你至少需要配置好一个 BackupRepo 才能使用 KubeBlocks 的备份功能 - -你可以参考官方提供的示例创建你的 BackupRepo,注意,如果你将 `accessMethod` 设置为了 `Mount`,你需要在你可能需要用到备份功能的 namespace 中都配置好 access key - -下面是一个使用 `accessMethod: Tool` 的 S3 BackupRepo 示例,来自 KubeBlocks 官方文档 - -```shell -# Create a secret to save the access key for S3 -kubectl create secret generic s3-credential-for-backuprepo \ - -n kb-system \ - --from-literal=accessKeyId= \ - --from-literal=secretAccessKey= - -# Create the BackupRepo resource -kubectl apply -f - <<-'EOF' -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: BackupRepo -metadata: - name: my-repo - annotations: - dataprotection.kubeblocks.io/is-default-repo: "true" -spec: - storageProviderRef: s3 - accessMethod: Tool - pvReclaimPolicy: Retain - volumeCapacity: 100Gi - config: - bucket: test-kb-backup - endpoint: "" - mountOptions: --memory-limit 1000 --dir-mode 0777 --file-mode 0666 - region: cn-northwest-1 - credential: - name: s3-credential-for-backuprepo - namespace: kb-system - pathPrefix: "" -EOF -``` - -你可以通过 `kubectl get backuprepo` 获取到你创建的 BackupRepo 的状态,如果遇到问题请查看 KubeBlocks 官方文档: - -## 安装 Kubeblock Adapter for Rainbond Plugin (原 Block Mechanica) - -- 使用 Kubeblock Adapter for Rainbond Plugin 提供的镜像 - -```shell -git clone https://github.com/furutachiKurea/kb-adapter-rbdplugin.git && cd kb-adapter-rbdplugin -make deploy -# or -kubectl apply -f https://raw.githubusercontent.com/furutachiKurea/kb-adapter-rbdplugin/refs/heads/main/deploy/k8s/deploy.yaml -``` - -- 或者通过手动构建镜像以使用最新版本: - -```shell -git clone https://github.com/furutachiKurea/kb-adapter-rbdplugin.git && cd kb-adapter-rbdplugin -make image -# 然后 push 到你的镜像仓库 -``` - -更新 `deploy/k8s/deploy.yaml` 中的镜像地址,然后执行 - -```shell -make deploy -``` - -Block Mechanica 需要部署在 rbd-plugins namespace 中,为了简化安装, rbd-api 中硬编码了 kb-adapter-rbdplugin 使用的 namespace,所以不要修改 `deploy.yaml` 中除镜像地址以外的内容,未来待 Rainbond 的插件体系完善之后将会有所优化 - -## 在 Rainbond 中使用 KubeBlocks - -接下来只需要像使用 Rainbond 一样使用通过 KubeBlocks 创建的数据库即可,具体参见[使用文档](Use_KubeBlocks_in_Rainbond.md) diff --git a/plugins/kb-adapter-rbdplugin/doc/Use_KubeBlocks_in_Rainbond.md b/plugins/kb-adapter-rbdplugin/doc/Use_KubeBlocks_in_Rainbond.md deleted file mode 100644 index 901675cb9..000000000 --- a/plugins/kb-adapter-rbdplugin/doc/Use_KubeBlocks_in_Rainbond.md +++ /dev/null @@ -1,136 +0,0 @@ -# 如何在 Rainbond 使用 KubeBlocks - -## 创建 KubeBlocks 数据库集群 - -KubeBlocks 数据库集群的创建与常规 Rainbond 组件的创建略有不同,数据库集群并没有常规 Rainbond 组件的构建源检测、端口设置、连接信息设置等操作,而是在完成数据库的相关设置之后直接完成创建,Rainbond 会自动完成端口和连接信息的设置 - -### 通过创建入口进入数据库集群的创建流程 - -当你安装了 Block Mechanica 和 KubeBlocks 并且存在至少一个可用的 KubeBlocks Addon(你可以创建的数据库集群的类型)时,就能够在 Rainbond 选择组件创建方式的页面中看到 `创建数据库集群` 的入口 - -![image-20251016132703363](./assets/image-20251016132703363.png) - -如果不显示数据库创建的入口: -检查 Block Mechanica 是否正常运行并且确保至少存在一个 Block Mechanica 支持的 KubeBlocks Addon 为 `Enabled` 状态,Block Mechanica 目前支持:`MySQL`, `PostgreSQL`, `Redis`, `RabbitMQ` - -```shell -# 查找是否存在可用的 Block Mechanica Pod -kubectl get pod -n rbd-system - -# 查找 KubeBlocks Addons -kubectl get addons.extensions.kubeblocks.io -``` - -### 数据库集群创建流程 - -设置数据库集群名称并选择数据库类型 - -当前 MySQL 使用的 topology 为 [semisync](https://kubeblocks.io/docs/release-1_0_1/kubeblocks-for-mysql/03-topologies/01-semisync),Redis 使用 [replication](https://kubeblocks.io/docs/release-1_0_1/kubeblocks-for-redis/03-topologies/02-replication) - -![image-20251016144852193](./assets/image-20251016144852193.png) - -用户可以为数据库集群设置资源分配等基础信息,对于支持备份的数据库类型,将会提供备份策略,当用户选定 backuprepo 之后将会启用备份功能并开启周期自动备份 - -![image-20251016145103541](./assets/image-20251016145103541.png)![image-20251016145111505](./assets/image-20251016145111505.png) - -几点说明: - -1. 对于多 component 结构的 Cluster 比如 Redis (sentinel & redis),会使用相同的资源分配设置,当用户对数据库集群进行伸缩时,Block Mechanica 会尽最大努力的保证这一点 - -2. [StorageClass](https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/) 会影响到数据库集群的储存容量扩容功能,只有当 StorageClass 的 `ALLOWVOLUMEEXPANSION` 为 `true` 时,扩容功能才能正常生效。该选项在完成数据库集群创建之后**不可修改** - - ```shell - kubectl get storageclass - ``` - -3. 对于备份策略,只有手动为数据库集群选定了使用的 BackupRepo 时才会被启用,对于不支持备份的数据库如 RabbitMQ 则不会显示这一选项卡 - -4. 如果 BackupRepo 一栏为空,请确保集群中存在至少一个 backuprepo 且 status 为 `Ready` - ```shell - kubectl get backuprepo - NAME STATUS STORAGEPROVIDER ACCESSMETHOD DEFAULT AGE - kubeblocks-backuprepo Ready minio Tool true 22d - ``` - -**在完成上述的一系列设置之后,点击 `创建数据库组件` 之后即可完成组件创建** - -## 数据库集群运维 - -与常规的 Rainbond 组件不同,数据库集群的状态由 KubeBlocks 进行管理,Rainbond 只负责将用户的操作转发给 Block Mechanica 并由 Block Mechanica 通过 k8s API 让 KubeBlocks 完成数据库集群的运维操作。 - -数据库集群运维的绝大部分内容与常规的 Rainbond 组件一致,但是不支持 `日志`、`环境配置` 选项卡,除此之外不同的部分将在下面一一说明: - -### 总览 - -对于数据库集群的状态,与常规 Rainbond 组件不同的是,**只有**当数据库集群进入 `运行中` 时,才能正常使用,详见 [KubeBlocks 文档](https://kubeblocks.io/docs/release-1_0_1/user_docs/references/api-reference/cluster#apps.kubeblocks.io/v1.ClusterPhase) - -对于 KubeBlocks 在 Rainbond 中操作记录功能的实现,受限于 KubeBlocks 与 Rainbond 的集成方式,目前对于数据库集群的部分运维操作基于 KubeBlocks 在对于具体的数据库集群进行运维时使用的 OpsRequest 实现,故对于部分不通过 OpsRequest 进行的运维操作则无法展示,如 `备份` 选项卡中的备份策略的修改、备份的删除操作。 - -![image-20251016151942615](./assets/image-20251016151942615.png) - - - -### 伸缩 - -KubeBlocks 在 Rainbond 中不支持 `自动伸缩` 、各个实例资源占用的实时展示和伸缩记录 - -对于用户进行的伸缩操作,Block Mechanica 会为其创建一个 OpsRequest 用于实现数据库集群的伸缩。储存容量的扩容需要在创建数据库集群时设置的 StorageClass 支持扩容才能生效 - -![image-20251016155419021](./assets/image-20251016155419021.png) - -### 备份 - -只有当用户为数据库集群设置了 BackupRepo 之后才能够正常的进行手动备份,当前暂时无法单独启用手动备份或者自动备份 - -用户能够通过备份列表中进入 `Completed` 的备份恢复数据库集群,从备份恢复数据库集群将会基于此备份创建一个**新的数据库集群** - -对于备份策略的修改和已有备份的删除,**KubeBlocks 不支持通过 OpsRequest 完成**,故不会在 `总览` 的 `操作记录` 中展示 - -对于不支持备份操作的数据库类型,如 RabbitMQ,将无法在此处进行任何操作 - -![image-20251016155052401](./assets/image-20251016155052401.png) - -### 监控 - -目前 Rainbond 中只支持对于数据库集群的 `资源监控` - -![image-20251016155503985](./assets/image-20251016155503985.png) - -### 依赖 - -Rainbond 将在完成数据库集群创建之后自动在组件连接信息中添加数据库的默认用户和密码 - -这部分的组件连接信息的使用与常规 Rainbond 组件的[使用方法](https://www.rainbond.com/docs/how-to-guides/app-ops/dependon)一致 - -数据库集群在 Rainbond 中不支持将其他 Rainbond 组件添加为依赖的操作,移除了 `依赖外部组件` 选项卡 - -![image-20251016160722595](./assets/image-20251016160722595.png) - -### 高级设置 - -相比于常规的 Rainbond 组件,KubeBlocks 数据库集群不支持高级设置中的 `储存`、`插件`、`构建源`、`其他设置`,但是额外支持了 `参数配置`,对于支持数据库参数配置的类型,可以在此处设置数据库参数,此外并不是所有的数据库类型都支持参数配置,如 RabbitMQ - -![image-20251016161055609](./assets/image-20251016161055609.png) - -#### 端口 - -对于端口设置,目前 KubeBlocks 数据库集群仅支持 TCP 协议 - -#### 参数配置 - -对于数据库参数的修改,Rainbond 和 Block Mechanica 只对用户输入的值做最低限度的校验,具体成功与否取决于 KubeBlocks OpsRequest 的执行结果,请参考各个数据库类型的官方文档进行修改 - -如果该数据库类型不支持进行参数配置,则该表将显示为空 - -Block Mechanica 判断可展示的数据库参数和其当前值的逻辑为: -从对应数据库的 `parametersdefinitions.parameters.kubeblocks.io` 中获取定义,并从数据库集群创建时对应创建的 configmap 中获取配置文件中设置的值(对应 `redis.conf`, `postgresql.conf`, etc.),对二者**取交集**作为在 Rainbond 中展示的数据 - -![image-20251016161955879](./assets/image-20251016161955879.png) - -### 其他 - -与常规 Rainbond 组件不同,数据库集群不支持 `构建`、`更新(滚动)`、`访问`、`Web终端` 按钮,对于手动访问数据库进行设置一类的操作,KubeBlocks 的文档中更加推荐通过 service 进行访问 - -![image-20251016162928955](./assets/image-20251016162928955.png) - -**KubeBlocks 数据库集群目展示前支持的功能有限,除去前面明确说明的部分,部分 Rainbond 功能可能因为各种原因不被支持,如由于 Rainbond 与 KubeBlocks 的设计差异,构建、滚动更新操作将不会得到支持;在 Rainbond 的团队空间中的储存分配量并不会计算为 KubeBlocks 数据库集群分配的储存量;以目前 KubeBlocks 在 Rainbond 中的集成度,`应用模板` 及其相关功能暂不被支持** diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/Block_Mechanica.png b/plugins/kb-adapter-rbdplugin/doc/assets/Block_Mechanica.png deleted file mode 100644 index 46f789933..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/Block_Mechanica.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/architecture.png b/plugins/kb-adapter-rbdplugin/doc/assets/architecture.png deleted file mode 100644 index 8a63b13a1..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/architecture.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/backup.png b/plugins/kb-adapter-rbdplugin/doc/assets/backup.png deleted file mode 100644 index 71d3cb37e..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/backup.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/connect.png b/plugins/kb-adapter-rbdplugin/doc/assets/connect.png deleted file mode 100644 index 6ef775f13..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/connect.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/database-config.png b/plugins/kb-adapter-rbdplugin/doc/assets/database-config.png deleted file mode 100644 index 2e650d660..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/database-config.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/database-create.png b/plugins/kb-adapter-rbdplugin/doc/assets/database-create.png deleted file mode 100644 index 0d20aa678..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/database-create.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/expansion.png b/plugins/kb-adapter-rbdplugin/doc/assets/expansion.png deleted file mode 100644 index 048cc23cd..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/expansion.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016132703363.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016132703363.png deleted file mode 100644 index 09024c9b1..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016132703363.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016144852193.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016144852193.png deleted file mode 100644 index 68d896db4..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016144852193.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145103541.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145103541.png deleted file mode 100644 index b19c3cdbc..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145103541.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145111505.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145111505.png deleted file mode 100644 index 9e13c0eba..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016145111505.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016151942615.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016151942615.png deleted file mode 100644 index a75a80adf..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016151942615.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155052401.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155052401.png deleted file mode 100644 index b1b304466..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155052401.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155419021.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155419021.png deleted file mode 100644 index 4bd08d020..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155419021.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155503985.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155503985.png deleted file mode 100644 index 75da2476c..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016155503985.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016160722595.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016160722595.png deleted file mode 100644 index ef55cdabd..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016160722595.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161055609.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161055609.png deleted file mode 100644 index ca86608f9..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161055609.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161955879.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161955879.png deleted file mode 100644 index e57e3e782..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016161955879.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016162928955.png b/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016162928955.png deleted file mode 100644 index 1dd87c0a7..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/image-20251016162928955.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/monitor.png b/plugins/kb-adapter-rbdplugin/doc/assets/monitor.png deleted file mode 100644 index b984d89ca..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/monitor.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/overview.png b/plugins/kb-adapter-rbdplugin/doc/assets/overview.png deleted file mode 100644 index d83c7de80..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/overview.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/parameter.png b/plugins/kb-adapter-rbdplugin/doc/assets/parameter.png deleted file mode 100644 index b7b62bb14..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/parameter.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/port.png b/plugins/kb-adapter-rbdplugin/doc/assets/port.png deleted file mode 100644 index 7a624246a..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/port.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/assets/wizard.png b/plugins/kb-adapter-rbdplugin/doc/assets/wizard.png deleted file mode 100644 index 15c619fbe..000000000 Binary files a/plugins/kb-adapter-rbdplugin/doc/assets/wizard.png and /dev/null differ diff --git a/plugins/kb-adapter-rbdplugin/doc/design_document.md b/plugins/kb-adapter-rbdplugin/doc/design_document.md deleted file mode 100644 index 24179a1ff..000000000 --- a/plugins/kb-adapter-rbdplugin/doc/design_document.md +++ /dev/null @@ -1,200 +0,0 @@ -# 设计文档 && 技术文档:KubeBlocks 在 Rainbond 中的集成 - -## 1. 目标与业务流程 - -### 1.1 目标 - -提供与 Rainbond 组件使用**体验一致**的图形化 KubeBlocks 数据库集群创建与运维操作 - -允许用户通过 Rainbond 图形化创建 KubeBlocks 数据库集群,同时拥有与 Rainbond 组件基本一致的使用体验 - -### 1.2 业务流程 - -```mermaid ---- -config: - theme: neutral - layout: elk ---- -flowchart TD - subgraph s2["rainbond-console"] - n3["kubeblocks component"] - end - subgraph s1["rbd-app-ui"] - n2["rainbond-ui"] - s2 - end - subgraph s3["k8s"] - n4["rbd-api"] - n5["rbd-worker"] - n6["block-mechanica"] - n7["KubeBlocks Operator && KubeBlocks CR"] - end - n2 -- 基于组件类型显示 kb 相关的页面 --> s2 - s1 --> n4 - n4 -- 端口、连接信息等 Rainbond 功能 --> n5 - n4 -- Cluster 的创建与运维等 KubeBlocks 相关功能 --> n6 - n6 --> n7 - n3@{ shape: rect} - -``` - - - -用户对 KubeBlocks workload 进行的各种运维操作将被 rbd-api 转发给 Block Mechanica,**由 Block Mechanica 负责 KubeBlocks 数据库集群具体 workload 的运维相关操作** - -## 2. 架构设计 - -### 2.1 Block Mechanica - -![architecture](./assets/architecture.png) - -Block Mechanica 负责执行对 KubeBlocks Cluster 实际 workload 的运维与创建操作,而涉及到将 KubeBlocks Cluster 暴露给其他 Rainbond 组件的内容则仍然交由 Rainbond 进行管理 - -Block Mechanica 通过 Echo 构建的 API 服务接收转发自 rbd-api 的相关请求,在涉及到 KubeBlocks 相关 CR 的 watch 操作时,Block Mechanica 相当于 KubeBlocks 与 Rainbond 之间的适配器,将 watch 到的信息转换成 Rainbond 使用的形式 - -### 2.2 Rainbond 组件与 KubeBlocks Cluster 之间的桥梁:`KubeBlocks Component` - -`KubeBlocks Component` 为新增的 Rainbond 组件类型,用于实现 Rainbond 组件对 KubeBlocks Cluster 的集成,Rainbond 只负责 `KubeBlocks Component` 在 Rainbond 中的连接信息、端口设置、资源监控,其他对于具体 workload 的运维操作将会调用 Block Mechanica API 通过 Block Mechanica 调用 KubeBlocks API 进行运维操作 - -`KubeBlocks Component` 与 KubeBlocks Cluster 之间通过 `label.service_id` 进行关联 - -## 3. 功能与界面 - -### 3.1 功能设计 - -![Block_Mechanica](./assets/Block_Mechanica.png) - -### 3.2 界面与交互 - -在安装了 Block Mechanica 之后,在创建组件页面会额外提供创建数据库的入口 - -![image-20250702160503671](./assets/wizard.png) - -用户将能够通过此入口进入数据库集群的创建界面,UX 上将延续 Rainbond 创建组件的流程 - -![image-20250928165255362](./assets/database-create.png) - -用户能够配置数据库集群的资源分配、storageclass、版本,对于支持备份的数据库类型,还能够通过选择备份使用的 backuprepo 来启用备份功能,可以设置定时备份规则 - -![image-20250928165541798](./assets/database-config.png) - -与常规 Rainbond 组件不同的是,KubeBlocks 数据库集群在这一步之后将会开始创建,并设置好端口和连接信息,不需要再额外手动设置。对于某些数据库拓扑,可能需要等待一段时间待储存数据库连接信息的 secret 被创建好之后才能完成 Rainbond 中数据库集群的创建 - -KubeBlocks 数据库集群在 Rainbond 中作为 `kubeblocks_component` 类型的组件通过插件 `block-mechanica` 进行集成,在使用逻辑上与常规 Rainbond 组件略有不同。对于多组件结构的 KubeBlocks Cluster,所有副本(pod)都将一起展示在实例列表中,伸缩时各个组件使用相同的资源分配;`kubeblocks_component` 不支持构建、版本管理、web 终端、外部访问、日志、自动伸缩、环境配置,`监控` tab 中只支持资源监控,`高级设置`只支持`端口`。同时新增两个 tab:备份、高级设置->参数配置 - -KubeBlocks 中的运维操作除修改备份策略以外均使用 **KubeBlocks OpsRequest** 进行完成,运行逻辑与 Rainbond 原生组件不同,除去生命周期管理相关的 OpsRequest/操作,都需要在组件进入 `运行中` 状态时才能正常运行 - -![image-20250928172142795](./assets/overview.png) - -存储容量仅支持扩容,且需要在创建数据库时选择的 `storageclass` `ALLOWVOLUMEEXPANSION` 为 true - -![image-20250928171353264](./assets/expansion.png) - -从备份恢复数据库时,会创建一个新的 `kubeblocks_component` - -![image-20250928171424115](./assets/backup.png) - -![image-20250928171439161](./assets/monitor.png) - -![image-20250928171454217](./assets/connect.png) - -![image-20250928171525737](./assets/port.png) - -![image-20250928171541756](./assets/parameter.png) - -## 4. 技术细节 - -### 4.1 新组件类型 `kubeblocks_component` - -为了实现 KubeBlocks 在 Rainbond 中的集成,我们在 Rainbond 中新增组件类型 `kubeblocks_component`,该类型只向数据库(rbd-db)中写入基础的组件信息、端口和组件连接信息,只由 rbd-worker 创建 service,其余生命周期相关的操作在 rbd-worker 中直接跳过,均由 rbd-api 转发给 Block Mechanica 实现 - -`kubeblocks_component` 通过 rbd-worker 在进行 `Rainbond 应用 -> k8s 资源` 时为其应用不同的 service selector 创建规则实现通过 `kubeblocks_component` 对于 KubeBlocks Cluster 的连接: - -```go -// generateKubeBlocksSelector generate kubeblocks selector -func (a *AppServiceBuild) generateKubeBlocksSelector() map[string]string { - var ( - peer = map[string]bool{ - "rabbitmq": true, - } - clusterName = "cluster-name" - componentName = "component-name" - ) - - // get k8s_component_name from database - if a.service != nil && a.service.K8sComponentName != "" { - k8sComponentName := a.service.K8sComponentName - lastDashIndex := strings.LastIndex(k8sComponentName, "-") - if lastDashIndex != -1 && lastDashIndex < len(k8sComponentName)-1 { - clusterName = k8sComponentName[:lastDashIndex] - componentName = k8sComponentName[lastDashIndex+1:] - } - } - - selector := map[string]string{ - "app.kubernetes.io/instance": clusterName, - "app.kubernetes.io/managed-by": "kubeblocks", - "apps.kubeblocks.io/component-name": componentName, - } - if _, ok := peer[componentName]; !ok { - selector["kubeblocks.io/role"] = "primary" - } - - return selector -} -``` - -为兼容 selector 创建,用户在 Rainbond 中创建的 KubeBlocks Component 英文名将强制添加后缀 `**-{component}**(即用户选择的数据库类型) - -**可以这么认为:对于 Rainbond 来说,KubeBlocks Component 并不会通过 rbd-worker 转换成 KubeBlocks Cluster,而只是一个管理了一个 service,Rainbond 组件通过这个 service 连接到数据库,在事实上完成了常规的 Rainbond 组件的功能;而生命周期相关的管理仍交由 Block Mechanica 完成** - -在此设计下,通过 Rainbond 创建的数据库集群的可用性由 KubeBlocks 保障,Block Mechanica 只负责作为 Rainbond 与 KubeBlocks 之间的连接器 - -### 4.2 KubeBlocks Component 和 KubeBlocks Cluster 的关联是如何实现的 - -KubeBlocks Cluster 在创建的时候会被添加上与 `kubeblocks_component` 相同的 `service_id` 标签。每个 `kubeblocks_component` 的 `service_id` 都唯一对应一个 KubeBlocks Cluster,Block Mechanica 通过这个 `service_id` 来找到对应的 KubeBlocks Cluster 进行运维操作 - -### 4.3 Adapter 模式 - -```mermaid -graph TD - A[数据库类型选择] --> B[Registry.Cluster] - B --> C[ClusterAdapter] - C --> D[Builder 构建 Cluster] - C --> E[Coordinator 协调配置] - - subgraph "数据库适配器" - G[MySQL Adapter] - H[PostgreSQL Adapter] - I[Redis Adapter] - J[RabbitMQ Adapter] - K[...] - end - - B -.-> G - B -.-> H - B -.-> I - B -.-> J -``` - -- **统一接口** - - 通过 `ClusterAdapter` 为不同数据库提供统一的创建接口 - - 每个数据库类型实现自己的 `Builder` 和 `Coordinator` - -- **职责分离** - - `Builder`: 负责构建 KubeBlocks Cluster - - `Coordinator`: 负责数据库特定的配置协调(端口、认证、备份等等) - -- **扩展数据库支持** - 1. 实现 `Builder` 和 `Coordinator` - 2. 在 Registry 中注册 - 3. 自动获得完整的集群管理能力 - -### 4.4 从备份恢复 - -用户在 Rainbond 中从备份恢复集群时,会先在 Rainbond 中创建一个新的 `kubeblocks_component`,然后通过 Block Mechanica 创建 KubeBlocks OpsRequest 来恢复集群,并将其与新的 `kubeblocks_component` [关联](#42-kubeblocks-component-和-kubeblocks-cluster-的关联是如何实现的)起来 - -### 4.5 Parameter 配置 - -KubeBlocks 提供了 `parametersdefinitions.parameters.kubeblocks.io` 来定义数据库的可用参数和约束,Block Mechanica 会通过 [mergeEntriesAndConstraints](../service/cluster/parameter.go) 合并来自 KubeBlocks 的参数约束和通过 `configmap` 中数据库配置文件获取到的参数,作为最后暴露给用户的可配置数据库参数 diff --git a/plugins/kb-adapter-rbdplugin/go.mod b/plugins/kb-adapter-rbdplugin/go.mod deleted file mode 100644 index 3db0bbc9a..000000000 --- a/plugins/kb-adapter-rbdplugin/go.mod +++ /dev/null @@ -1,98 +0,0 @@ -module github.com/furutachiKurea/kb-adapter-rbdplugin - -go 1.24.6 - -require ( - github.com/apecloud/kubeblocks v1.0.1 - github.com/creasty/defaults v1.8.0 - github.com/go-logr/zapr v1.3.0 - github.com/labstack/echo/v4 v4.13.4 - github.com/sahilm/fuzzy v0.1.1 - github.com/stretchr/testify v1.11.1 - go.uber.org/zap v1.27.0 - golang.org/x/sync v0.17.0 - gopkg.in/ini.v1 v1.67.0 - k8s.io/api v0.34.1 - k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 - sigs.k8s.io/controller-runtime v0.22.1 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.25.1 // indirect - github.com/go-openapi/swag/cmdutils v0.25.1 // indirect - github.com/go-openapi/swag/conv v0.25.1 // indirect - github.com/go-openapi/swag/fileutils v0.25.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect - github.com/go-openapi/swag/jsonutils v0.25.1 // indirect - github.com/go-openapi/swag/loading v0.25.1 // indirect - github.com/go-openapi/swag/mangling v0.25.1 // indirect - github.com/go-openapi/swag/netutils v0.25.1 // indirect - github.com/go-openapi/swag/stringutils v0.25.1 // indirect - github.com/go-openapi/swag/typeutils v0.25.1 // indirect - github.com/go-openapi/swag/yamlutils v0.25.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/jinzhu/copier v0.4.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.17.0 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/x448/float16 v0.8.4 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/oauth2 v0.31.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/time v0.13.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect -) diff --git a/plugins/kb-adapter-rbdplugin/go.sum b/plugins/kb-adapter-rbdplugin/go.sum deleted file mode 100644 index 7eab1777b..000000000 --- a/plugins/kb-adapter-rbdplugin/go.sum +++ /dev/null @@ -1,255 +0,0 @@ -github.com/apecloud/kubeblocks v1.0.1 h1:t/EPcBYn+83U5lOYBOnexcWomXas6Zz87plMYetEYoc= -github.com/apecloud/kubeblocks v1.0.1/go.mod h1:+p1wpv2KIfet//EWTTyPDYsvvZdkq0eRkjZjmPh2i/M= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= -github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= -github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= -github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= -github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= -github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= -github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= -github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= -github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= -github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= -github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= -github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= -github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= -github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= -github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= -github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= -github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= -github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= -github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= -github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= -github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= -github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= -github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= -github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= -github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= -github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= -github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= -golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= -gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= -gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/plugins/kb-adapter-rbdplugin/internal/config/config.go b/plugins/kb-adapter-rbdplugin/internal/config/config.go deleted file mode 100644 index 48a1a8e10..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/config/config.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package config -package config - -import ( - "fmt" - "os" - "strconv" - "strings" -) - -// ServerConfig echo server config -type ServerConfig struct { - Host string `json:"host" yaml:"host"` - Port string `json:"port" yaml:"port"` - ReadinessPath string `json:"readiness_path" yaml:"readiness_path"` - LivenessPath string `json:"liveness_path" yaml:"liveness_path"` -} - -// LoadConfigFromEnv 从环境变量加载配置 -func LoadConfigFromEnv() *ServerConfig { - config := &ServerConfig{ - Host: getEnvOrDefault("HOST", "0.0.0.0"), - Port: getEnvOrDefault("PORT", "8080"), - ReadinessPath: getEnvOrDefault("READINESS_PATH", "/readyz"), - LivenessPath: getEnvOrDefault("LIVENESS_PATH", "/livez"), - } - return config -} - -// Validate 验证配置 -func (c *ServerConfig) Validate() error { - if c.Host == "" { - return fmt.Errorf("host cannot be empty") - } - if c.Port == "" { - return fmt.Errorf("port cannot be empty") - } - - // Validate port is a valid number - portNum, err := strconv.Atoi(c.Port) - if err != nil { - return fmt.Errorf("port must be a valid integer: %w", err) - } - - // Validate port range - if portNum < 1 || portNum > 65535 { - return fmt.Errorf("port must be between 1 and 65535, got %d", portNum) - } - - if c.ReadinessPath == "" { - return fmt.Errorf("readiness_path cannot be empty") - } - if c.LivenessPath == "" { - return fmt.Errorf("liveness_path cannot be empty") - } - - return nil -} - -// MustLoad 加载配置 -func MustLoad() *ServerConfig { - cfg := LoadConfigFromEnv() - if err := cfg.Validate(); err != nil { - panic(fmt.Sprintf("configuration validation failed: %v", err)) - } - return cfg -} - -// getEnvOrDefault 获取环境变量,如果不存在则返回默认值 -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -// InDevelopment 是否为开发环境 -func InDevelopment() bool { - env := strings.ToLower(os.Getenv("ENV")) - return env == "dev" || env == "development" -} diff --git a/plugins/kb-adapter-rbdplugin/internal/config/config_test.go b/plugins/kb-adapter-rbdplugin/internal/config/config_test.go deleted file mode 100644 index b1622145d..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/config/config_test.go +++ /dev/null @@ -1,438 +0,0 @@ -package config - -import ( - "os" - "strings" - "testing" -) - -// capability_id: rainbond.kb-adapter.server-config.validate -func TestServerConfig_Validate(t *testing.T) { - tests := []struct { - name string - config *ServerConfig - wantErr bool - errMsg string - }{ - { - name: "valid config", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "8080", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: false, - }, - { - name: "empty host", - config: &ServerConfig{ - Host: "", - Port: "8080", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "host cannot be empty", - }, - { - name: "empty port", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "port cannot be empty", - }, - { - name: "invalid port format", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "abc", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "port must be a valid integer", - }, - { - name: "port zero", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "0", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "port must be between 1 and 65535", - }, - { - name: "negative port", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "-1", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "port must be between 1 and 65535", - }, - { - name: "port out of range", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "70000", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "port must be between 1 and 65535", - }, - { - name: "empty readiness path", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "8080", - ReadinessPath: "", - LivenessPath: "/livez", - }, - wantErr: true, - errMsg: "readiness_path cannot be empty", - }, - { - name: "empty liveness path", - config: &ServerConfig{ - Host: "0.0.0.0", - Port: "8080", - ReadinessPath: "/readyz", - LivenessPath: "", - }, - wantErr: true, - errMsg: "liveness_path cannot be empty", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.config.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && err != nil && tt.errMsg != "" { - if !strings.Contains(err.Error(), tt.errMsg) { - t.Errorf("Validate() error = %v, want error containing %v", err, tt.errMsg) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.server-config.load-from-env -func TestLoadConfigFromEnv(t *testing.T) { - tests := []struct { - name string - envVars map[string]string - unsetAll bool - expected *ServerConfig - }{ - { - name: "all environment variables set", - envVars: map[string]string{ - "HOST": "test-host", - "PORT": "9090", - "READINESS_PATH": "/custom/ready", - "LIVENESS_PATH": "/custom/live", - }, - expected: &ServerConfig{ - Host: "test-host", - Port: "9090", - ReadinessPath: "/custom/ready", - LivenessPath: "/custom/live", - }, - }, - { - name: "no environment variables set - use defaults", - unsetAll: true, - expected: &ServerConfig{ - Host: "0.0.0.0", - Port: "8080", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - }, - { - name: "partial environment variables set", - envVars: map[string]string{ - "HOST": "custom-host", - "PORT": "3000", - }, - expected: &ServerConfig{ - Host: "custom-host", - Port: "3000", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - }, - { - name: "empty environment variables should use defaults", - envVars: map[string]string{ - "HOST": "", - "PORT": "", - "READINESS_PATH": "", - "LIVENESS_PATH": "", - }, - expected: &ServerConfig{ - Host: "0.0.0.0", - Port: "8080", - ReadinessPath: "/readyz", - LivenessPath: "/livez", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - originalEnv := make(map[string]string) - envKeys := []string{"HOST", "PORT", "READINESS_PATH", "LIVENESS_PATH"} - for _, key := range envKeys { - originalEnv[key] = os.Getenv(key) - } - - defer func() { - for _, key := range envKeys { - if originalValue, exists := originalEnv[key]; exists && originalValue != "" { - os.Setenv(key, originalValue) - } else { - os.Unsetenv(key) - } - } - }() - - if tt.unsetAll { - for _, key := range envKeys { - os.Unsetenv(key) - } - } else { - for _, key := range envKeys { - os.Unsetenv(key) - } - for key, value := range tt.envVars { - if value != "" { - os.Setenv(key, value) - } - } - } - - cfg := LoadConfigFromEnv() - - if cfg.Host != tt.expected.Host { - t.Errorf("LoadConfigFromEnv() Host = %v, want %v", cfg.Host, tt.expected.Host) - } - if cfg.Port != tt.expected.Port { - t.Errorf("LoadConfigFromEnv() Port = %v, want %v", cfg.Port, tt.expected.Port) - } - if cfg.ReadinessPath != tt.expected.ReadinessPath { - t.Errorf("LoadConfigFromEnv() ReadinessPath = %v, want %v", cfg.ReadinessPath, tt.expected.ReadinessPath) - } - if cfg.LivenessPath != tt.expected.LivenessPath { - t.Errorf("LoadConfigFromEnv() LivenessPath = %v, want %v", cfg.LivenessPath, tt.expected.LivenessPath) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.server-config.must-load -func TestMustLoad(t *testing.T) { - tests := []struct { - name string - envVars map[string]string - shouldPanic bool - panicMsg string - }{ - { - name: "valid config should not panic", - envVars: map[string]string{ - "HOST": "0.0.0.0", - "PORT": "8080", - "READINESS_PATH": "/readyz", - "LIVENESS_PATH": "/livez", - }, - shouldPanic: false, - }, - { - name: "invalid port should panic", - envVars: map[string]string{ - "HOST": "0.0.0.0", - "PORT": "invalid", - "READINESS_PATH": "/readyz", - "LIVENESS_PATH": "/livez", - }, - shouldPanic: true, - panicMsg: "configuration validation failed", - }, - { - name: "empty host should panic", - envVars: map[string]string{ - "HOST": "", - "PORT": "8080", - "READINESS_PATH": "/readyz", - "LIVENESS_PATH": "/livez", - }, - shouldPanic: false, - }, - { - name: "port out of range should panic", - envVars: map[string]string{ - "HOST": "0.0.0.0", - "PORT": "70000", - "READINESS_PATH": "/readyz", - "LIVENESS_PATH": "/livez", - }, - shouldPanic: true, - panicMsg: "configuration validation failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - originalEnv := make(map[string]string) - envKeys := []string{"HOST", "PORT", "READINESS_PATH", "LIVENESS_PATH"} - for _, key := range envKeys { - originalEnv[key] = os.Getenv(key) - } - - defer func() { - for _, key := range envKeys { - if originalValue, exists := originalEnv[key]; exists && originalValue != "" { - os.Setenv(key, originalValue) - } else { - os.Unsetenv(key) - } - } - }() - - for _, key := range envKeys { - os.Unsetenv(key) - } - for key, value := range tt.envVars { - os.Setenv(key, value) - } - - defer func() { - r := recover() - if tt.shouldPanic { - if r == nil { - t.Errorf("MustLoad() should have panicked but did not") - return - } - panicStr, ok := r.(string) - if !ok { - t.Errorf("MustLoad() panic type = %T, want string", r) - return - } - if tt.panicMsg != "" && !strings.Contains(panicStr, tt.panicMsg) { - t.Errorf("MustLoad() panic message = %v, want containing %v", panicStr, tt.panicMsg) - } - } else { - if r != nil { - t.Errorf("MustLoad() should not have panicked but got: %v", r) - } - } - }() - - cfg := MustLoad() - if !tt.shouldPanic && cfg == nil { - t.Errorf("MustLoad() returned nil config") - } - }) - } -} - -// capability_id: rainbond.kb-adapter.server-config.dev-mode -func TestInDevelopment(t *testing.T) { - tests := []struct { - name string - envValue string - unsetEnv bool - expected bool - }{ - { - name: "ENV=dev returns true", - envValue: "dev", - expected: true, - }, - { - name: "ENV=development returns true", - envValue: "development", - expected: true, - }, - { - name: "ENV=DEV returns true (case insensitive)", - envValue: "DEV", - expected: true, - }, - { - name: "ENV=DEVELOPMENT returns true (case insensitive)", - envValue: "DEVELOPMENT", - expected: true, - }, - { - name: "ENV=Dev returns true (mixed case)", - envValue: "Dev", - expected: true, - }, - { - name: "ENV=prod returns false", - envValue: "prod", - expected: false, - }, - { - name: "ENV=production returns false", - envValue: "production", - expected: false, - }, - { - name: "ENV=test returns false", - envValue: "test", - expected: false, - }, - { - name: "ENV empty string returns false", - envValue: "", - expected: false, - }, - { - name: "ENV not set returns false", - unsetEnv: true, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - originalEnv := os.Getenv("ENV") - - defer func() { - if originalEnv != "" { - os.Setenv("ENV", originalEnv) - } else { - os.Unsetenv("ENV") - } - }() - - if tt.unsetEnv { - os.Unsetenv("ENV") - } else { - os.Setenv("ENV", tt.envValue) - } - - result := InDevelopment() - if result != tt.expected { - t.Errorf("InDevelopment() = %v, want %v", result, tt.expected) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/internal/index/index.go b/plugins/kb-adapter-rbdplugin/internal/index/index.go deleted file mode 100644 index 5ff184345..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/index/index.go +++ /dev/null @@ -1,133 +0,0 @@ -// Package index 用于在 controller-runtime 中注册字段索引 -package index - -import ( - "context" - "fmt" - - "sigs.k8s.io/controller-runtime/pkg/client" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - opv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - ctrl "sigs.k8s.io/controller-runtime" -) - -const ( - // ServiceIDLabel 通过此键关联 KubeBlocks Component 和 Cluster - ServiceIDLabel = "service_id" - - // InstanceLabel 用于索引 Pod 和 Backup 的 instance - InstanceLabel = "app.kubernetes.io/instance" - - // ServiceIDField 通过 service_id 索引 KubeBlocks Component 和 Cluster - ServiceIDField = "index.serviceID" - - // NamespaceInstanceField namespace/instance, - // 用于索引 Cluster 的 Pod(Cluster 副本)和 Backup - NamespaceInstanceField = "index.namespace.instance" - - // NamespaceClusterOpsTypeField namespace/cluster/opsType, - // 用于索引 OpsRequest - NamespaceClusterOpsTypeField = "index.namespace.cluster.opsType" - - // NamespaceClusterComponentField namespace/cluster/component, - // 用于索引 InstanceSet - NamespaceClusterComponentField = "index.namespace.cluster.component" - - // NamespacePodNameField namespace/podName, - // 用于索引 Pod 相关的 Event - NamespacePodNameField = "index.namespace.podName" -) - -// Register 在缓存注册字段索引。 -func Register(ctx context.Context, mgr ctrl.Manager) error { - indexer := mgr.GetFieldIndexer() - - // 为 KubeBlocks Cluster 按 service_id 建立索引 - if err := indexer.IndexField(ctx, &kbappsv1.Cluster{}, ServiceIDField, func(obj client.Object) []string { - labels := obj.GetLabels() - if labels == nil { - return nil - } - if v, ok := labels[ServiceIDLabel]; ok && v != "" { - return []string{v} - } - return nil - }); err != nil { - return err - } - - // 为 Deployment 按 service_id 建立索引 - if err := indexer.IndexField(ctx, &appsv1.Deployment{}, ServiceIDField, func(obj client.Object) []string { - labels := obj.GetLabels() - if labels == nil { - return nil - } - if v, ok := labels[ServiceIDLabel]; ok && v != "" { - return []string{v} - } - return nil - }); err != nil { - return err - } - - // 为 pod 按 namespace/instance 建立索引 - if err := indexer.IndexField(ctx, &corev1.Pod{}, NamespaceInstanceField, func(obj client.Object) []string { - pod := obj.(*corev1.Pod) - if instance, ok := pod.Labels[InstanceLabel]; ok && instance != "" { - return []string{fmt.Sprintf("%s/%s", pod.Namespace, instance)} - } - return nil - }); err != nil { - return err - } - - // 为 backup 按 namespace/instance 建立索引 - if err := indexer.IndexField(ctx, &datav1alpha1.Backup{}, NamespaceInstanceField, func(obj client.Object) []string { - backup := obj.(*datav1alpha1.Backup) - if instance, ok := backup.Labels[InstanceLabel]; ok && instance != "" { - return []string{fmt.Sprintf("%s/%s", backup.Namespace, instance)} - } - return nil - }); err != nil { - return err - } - - // 为 opsrequest 按 namespace/clusterName/opsType 建立索引 - if err := indexer.IndexField(ctx, &opv1alpha1.OpsRequest{}, NamespaceClusterOpsTypeField, func(obj client.Object) []string { - ops := obj.(*opv1alpha1.OpsRequest) - return []string{fmt.Sprintf("%s/%s/%s", ops.Namespace, ops.Spec.ClusterName, ops.Spec.Type)} - }); err != nil { - return err - } - - // 为 InstanceSet 按 namespace/cluster/component 建立索引 - if err := indexer.IndexField(ctx, &workloadsv1.InstanceSet{}, NamespaceClusterComponentField, func(obj client.Object) []string { - instanceSet := obj.(*workloadsv1.InstanceSet) - if clusterName, ok := instanceSet.Labels[InstanceLabel]; ok && clusterName != "" { - if componentName, ok := instanceSet.Labels["apps.kubeblocks.io/component-name"]; ok { - return []string{fmt.Sprintf("%s/%s/%s", instanceSet.Namespace, clusterName, componentName)} - } - } - return nil - }); err != nil { - return err - } - - // 为 Event 按 namespace/podName 建立索引 - if err := indexer.IndexField(ctx, &corev1.Event{}, NamespacePodNameField, func(obj client.Object) []string { - event := obj.(*corev1.Event) - if event.InvolvedObject.Kind == "Pod" && event.InvolvedObject.Name != "" { - return []string{fmt.Sprintf("%s/%s", event.Namespace, event.InvolvedObject.Name)} - } - return nil - }); err != nil { - return err - } - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/internal/k8s/manager.go b/plugins/kb-adapter-rbdplugin/internal/k8s/manager.go deleted file mode 100644 index 484cc71ee..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/k8s/manager.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package k8s 负责与 Kubernetes controller-runtime 的集成与管理 -package k8s - -import ( - "context" - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/api" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/config" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - parametersv1alpha1 "github.com/apecloud/kubeblocks/apis/parameters/v1alpha1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/metrics/server" -) - -var _scheme = runtime.NewScheme() - -func init() { - utilruntime.Must(corev1.AddToScheme(_scheme)) - utilruntime.Must(storagev1.AddToScheme(_scheme)) - utilruntime.Must(kbappsv1.AddToScheme(_scheme)) - utilruntime.Must(datav1alpha1.AddToScheme(_scheme)) - utilruntime.Must(opsv1alpha1.AddToScheme(_scheme)) - utilruntime.Must(parametersv1alpha1.AddToScheme(_scheme)) - utilruntime.Must(appsv1.AddToScheme(_scheme)) - utilruntime.Must(workloadsv1.AddToScheme(_scheme)) -} - -// NewManager 创建 ctrl.Manager 实例 -// -// 设置 logger 为 zap -func NewManager() (ctrl.Manager, error) { - enableLeaderElection := !config.InDevelopment() - - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: _scheme, - LeaderElection: enableLeaderElection, - LeaderElectionID: "kb-adapter-leader-election", - LeaderElectionNamespace: "rbd-system", - Metrics: server.Options{ - BindAddress: ":9090", - }, - }) - if err != nil { - return nil, fmt.Errorf("create manager: %w", err) - } - - return mgr, nil -} - -// Setup 初始化 manage -func Setup(ctx context.Context, mgr ctrl.Manager, svcs service.Services) error { - if err := index.Register(ctx, mgr); err != nil { - return fmt.Errorf("register indexes: %w", err) - } - - // 注册 API server runnable(由 Setup 统一收口) - if err := api.RegisterServer(ctx, mgr, svcs); err != nil { - return fmt.Errorf("register api server: %w", err) - } - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/internal/log/echo.go b/plugins/kb-adapter-rbdplugin/internal/log/echo.go deleted file mode 100644 index 387de6e69..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/log/echo.go +++ /dev/null @@ -1,33 +0,0 @@ -package log - -import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -// EchoZap 使用 zap 的 echo RequestLogger 中间件 -// -// https://echo.labstack.com/docs/middleware/logger#new-requestlogger-middleware -func EchoZap() echo.MiddlewareFunc { - return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogMethod: true, - LogURI: true, - LogStatus: true, - LogError: true, - LogRemoteIP: true, - LogUserAgent: true, - LogLatency: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { - getLoggerWithCallerSkip().Info("request", - String("method", v.Method), - String("URI", v.URI), - Int("status", v.Status), - Err(v.Error), - String("remote_ip", v.RemoteIP), - String("user_agent", v.UserAgent), - Duration("latency", v.Latency), - ) - return nil - }, - }) -} diff --git a/plugins/kb-adapter-rbdplugin/internal/log/log.go b/plugins/kb-adapter-rbdplugin/internal/log/log.go deleted file mode 100644 index dceb516e6..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/log/log.go +++ /dev/null @@ -1,211 +0,0 @@ -// Package log 提供日志记录相关功能、配置 -package log - -import ( - "os" - "sync" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/config" - - "github.com/go-logr/zapr" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - ctrl "sigs.k8s.io/controller-runtime" -) - -var ( - globalLogger *zap.Logger - once sync.Once -) - -type Field = zap.Field - -// InitLogger 初始化全局 logger 实例 -// 自动检测环境:默认生产模式,只有在 ENV=dev 时才使用开发模式 -// 同时输出到控制台和 logs/bm.log 文件中 -func InitLogger() { - once.Do(func() { - development := config.InDevelopment() - - // 创建 logs 目录 - if err := os.MkdirAll("logs", 0755); err != nil { - createConsoleOnlyLogger(development) - return - } - - consoleEncoder := createConsoleEncoder(development) - - fileEncoder := createFileEncoder(development) - - consoleCore := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), getLogLevel(development)) - - fileCore, err := createFileCore(fileEncoder, development) - if err != nil { - createConsoleOnlyLogger(development) - return - } - - core := zapcore.NewTee(consoleCore, fileCore) - - logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) - - globalLogger = logger - - // 为 controller-runtime 设置 logger - ctrl.SetLogger(zapr.NewLogger(logger)) - }) -} - -// getLogLevel 根据环境获取日志级别 -func getLogLevel(development bool) zapcore.Level { - if development { - return zapcore.DebugLevel - } - return zapcore.InfoLevel -} - -// createConsoleEncoder 创建控制台编码器 -func createConsoleEncoder(development bool) zapcore.Encoder { - encoderConfig := zap.NewDevelopmentEncoderConfig() - if development { - encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder - } else { - encoderConfig = zap.NewProductionEncoderConfig() - } - encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000") - return zapcore.NewConsoleEncoder(encoderConfig) -} - -// createFileEncoder 创建文件编码器 -func createFileEncoder(development bool) zapcore.Encoder { - if development { - // 开发环境,去除颜色 - encoderConfig := zap.NewDevelopmentEncoderConfig() - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder - encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000") - return zapcore.NewConsoleEncoder(encoderConfig) - } else { - // 生产环境:使用 JSON 格式 - encoderConfig := zap.NewProductionEncoderConfig() - encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder - encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000") - return zapcore.NewJSONEncoder(encoderConfig) - } -} - -// createFileCore 创建文件输出核心 -func createFileCore(encoder zapcore.Encoder, development bool) (zapcore.Core, error) { - file, err := os.OpenFile("logs/bm.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - return zapcore.NewCore(encoder, zapcore.AddSync(file), getLogLevel(development)), nil -} - -// createConsoleOnlyLogger 创建仅控制台输出的 logger -func createConsoleOnlyLogger(development bool) { - encoder := createConsoleEncoder(development) - core := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), getLogLevel(development)) - logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) - globalLogger = logger - ctrl.SetLogger(zapr.NewLogger(logger)) -} - -// getLogger 获取全局 logger 实例 -func getLogger() *zap.Logger { - if globalLogger == nil { - InitLogger() - } - return globalLogger -} - -// 日志记录 - -// getLoggerWithCallerSkip 获取带有调用位置跳过的 logger,默认跳过 1 层。 -// -// 确保正确记录 log 位置 -func getLoggerWithCallerSkip() *zap.Logger { - return getLoggerWithCustomCallerSkip(1) -} - -// getLoggerWithCustomCallerSkip 返回一个带有自定义调用位置跳过层数的 logger, -// skip 参数指定需要跳过的调用栈层数 -func getLoggerWithCustomCallerSkip(skip int) *zap.Logger { - return getLogger().WithOptions(zap.AddCallerSkip(skip)) -} - -func Info(msg string, fields ...Field) { - getLoggerWithCallerSkip().Info(msg, fields...) -} - -func Error(msg string, fields ...Field) { - getLoggerWithCallerSkip().Error(msg, fields...) -} - -func Debug(msg string, fields ...Field) { - getLoggerWithCallerSkip().Debug(msg, fields...) -} - -func Warn(msg string, fields ...Field) { - getLoggerWithCallerSkip().Warn(msg, fields...) -} - -func Fatal(msg string, fields ...Field) { - getLoggerWithCallerSkip().Fatal(msg, fields...) -} - -func With(fields ...Field) *zap.Logger { - return getLogger().With(fields...) -} - -func Sync() error { - return getLogger().Sync() -} - -// 常用字段函数,避免额外导入 zap - -func String(key, val string) Field { - return zap.String(key, val) -} - -func Int(key string, val int) Field { - return zap.Int(key, val) -} - -func Int32(key string, val int32) Field { - return zap.Int32(key, val) -} - -func Int64(key string, val int64) Field { - return zap.Int64(key, val) -} - -func Float64(key string, val float64) Field { - return zap.Float64(key, val) -} - -func Bool(key string, val bool) Field { - return zap.Bool(key, val) -} - -// Err 创建错误 Field -// -// 用于兼容命名冲突 -func Err(err error) Field { - return zap.Error(err) -} - -func Duration(key string, val time.Duration) Field { - return zap.Duration(key, val) -} - -func Time(key string, val time.Time) Field { - return zap.Time(key, val) -} - -func Any(key string, val any) Field { - return zap.Any(key, val) -} diff --git a/plugins/kb-adapter-rbdplugin/internal/model/backup.go b/plugins/kb-adapter-rbdplugin/internal/model/backup.go deleted file mode 100644 index a6d7f812b..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/model/backup.go +++ /dev/null @@ -1,55 +0,0 @@ -package model - -import ( - "time" - - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// BackupScheduleInput 用于更新备份策略 -type BackupScheduleInput struct { - RBDService - ClusterBackup -} - -// BackupInput 用于创建备份 -type BackupInput struct { - RBDService -} - -// BackupListQuery 用于获取备份列表 -type BackupListQuery struct { - RBDService - Pagination -} - -// BackupItem 用户备份 -type BackupItem struct { - Name string `json:"name"` - Status datav1alpha1.BackupPhase `json:"status"` - Time time.Time `json:"time"` -} - -type BackupRepo struct { - Name string `json:"name"` - Type string `json:"type"` - AccessMethod datav1alpha1.AccessMethod `json:"accessMethod"` - Phase datav1alpha1.BackupRepoPhase `json:"phase"` - GeneratedStorageClassName string `json:"generatedStorageClassName,omitempty"` - BackupPVCName string `json:"backupPVCName,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -type BackupRepoInput struct { - Name string `json:"name"` - StorageProvider string `json:"storageProviderRef"` - AccessMethod datav1alpha1.AccessMethod `json:"accessMethod"` - PVReclaimPolicy corev1.PersistentVolumeReclaimPolicy `json:"pvReclaimPolicy"` - VolumeCapacity string `json:"volumeCapacity"` - Config map[string]string `json:"config"` - Credential corev1.SecretReference `json:"credential"` - Secrets map[string]string `json:"secrets,omitempty"` - PathPrefix string `json:"pathPrefix"` -} diff --git a/plugins/kb-adapter-rbdplugin/internal/model/cluster.go b/plugins/kb-adapter-rbdplugin/internal/model/cluster.go deleted file mode 100644 index 821095a2b..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/model/cluster.go +++ /dev/null @@ -1,436 +0,0 @@ -package model - -import ( - "fmt" - "strconv" - "strings" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -const ( - Hourly BackupFrequency = "hourly" - Daily BackupFrequency = "daily" - Weekly BackupFrequency = "weekly" -) - -// BackupFrequency 备份频率类型 -type BackupFrequency string - -// ClusterInput 创建集群请求 - 组合了创建集群需要的所有信息 -type ClusterInput struct { - ClusterInfo - ClusterResource - ClusterBackup - RBDService RBDService `json:"rbdService"` -} - -// ExpansionInput 集群扩容请求 -type ExpansionInput struct { - RBDService - ClusterResource -} - -// ClusterInfo KubeBlocks Cluster 的基本信息 -type ClusterInfo struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Type string `json:"type"` - Version string `json:"version"` - StorageClass string `json:"storageClass"` - - // TerminationPolicy 删除策略 - // - // 尽管在 rbd-app-ui 中的设置项是与备份数据的删除策略,但实际上是 Cluster.spec 中的内容,故放在 ClusterInfo 中 - // - // https://kubeblocks.io/docs/preview/user_docs/references/api-reference/cluster#apps.kubeblocks.io/v1.TerminationPolicyType - TerminationPolicy kbappsv1.TerminationPolicyType `json:"terminationPolicy"` -} - -// RBDService rainbond service 标识 -type RBDService struct { - ServiceID string `json:"service_id" param:"service-id"` -} - -// ClusterBackup KubeBlocks Cluster 的备份相关配置 -type ClusterBackup struct { - BackupRepo string `json:"backupRepo"` - Schedule BackupSchedule `json:"schedule"` - RetentionPeriod datav1alpha.RetentionPeriod `json:"retentionPeriod"` -} - -// ClusterResource 创建 KubeBlocks Cluster 时的资源规格 -type ClusterResource struct { - CPU string `json:"cpu"` - Memory string `json:"memory"` - Storage string `json:"storage"` - Replicas int32 `json:"replicas"` -} - -// ParsedResources 解析后的资源信息 -type ParsedResources struct { - CPU resource.Quantity - Memory resource.Quantity - Storage resource.Quantity -} - -// ParseResources 解析字符串形式的资源配置为 resource.Quantity 类型 -func (cr *ClusterResource) ParseResources() (*ParsedResources, error) { - cpuQuantity, err := resource.ParseQuantity(cr.CPU) - if err != nil { - return nil, fmt.Errorf("invalid CPU quantity %q: %w", cr.CPU, err) - } - - memoryQuantity, err := resource.ParseQuantity(cr.Memory) - if err != nil { - return nil, fmt.Errorf("invalid memory quantity %q: %w", cr.Memory, err) - } - - storageQuantity, err := resource.ParseQuantity(cr.Storage) - if err != nil { - return nil, fmt.Errorf("invalid storage quantity %q: %w", cr.Storage, err) - } - - return &ParsedResources{ - CPU: cpuQuantity, - Memory: memoryQuantity, - Storage: storageQuantity, - }, nil -} - -// BackupSchedule backup schedule 配置 -type BackupSchedule struct { - Frequency BackupFrequency `json:"frequency"` - DayOfWeek int32 `json:"dayOfWeek"` - Hour int32 `json:"hour"` - Minute int32 `json:"minute"` -} - -// Cron 生成 cron 表达式 -func (s *BackupSchedule) Cron() string { - switch s.Frequency { - case Hourly: - return fmt.Sprintf("%d * * * *", s.Minute) - case Daily: - return fmt.Sprintf("%d %d * * *", s.Minute, s.Hour) - case Weekly: - return fmt.Sprintf("%d %d * * %d", s.Minute, s.Hour, s.DayOfWeek) - default: - return "" - } -} - -// Uncron 从 cron 表达式解析成 BackupSchedule, -// 支持 hourly, daily, weekly -func (s *BackupSchedule) Uncron(cronExpr string) error { - if cronExpr == "" { - return fmt.Errorf("empty cron expression") - } - - // 简单的解析逻辑,假设格式正确 - // 实际项目中可能需要使用更复杂的 cron 解析库 - parts := strings.Fields(cronExpr) - if len(parts) != 5 { - return fmt.Errorf("invalid cron expression format: %s", cronExpr) - } - - minute := parts[0] - hour := parts[1] - dayOfWeek := parts[4] - - // 解析分钟 - if minute == "*" { - s.Minute = 0 - } else { - if m, err := strconv.Atoi(minute); err == nil { - s.Minute = int32(m) - } else { - return fmt.Errorf("invalid minute in cron: %s", minute) - } - } - - // 解析小时 - if hour == "*" { - s.Hour = 0 - } else { - if h, err := strconv.Atoi(hour); err == nil { - s.Hour = int32(h) - } else { - return fmt.Errorf("invalid hour in cron: %s", hour) - } - } - - // 解析星期几 - if dayOfWeek == "*" { - s.DayOfWeek = 0 - s.Frequency = Daily - } else { - if d, err := strconv.Atoi(dayOfWeek); err == nil { - s.DayOfWeek = int32(d) - s.Frequency = Weekly - } else { - return fmt.Errorf("invalid day of week in cron: %s", dayOfWeek) - } - } - - // 判断频率类型 - if s.DayOfWeek > 0 { - s.Frequency = Weekly - } else if s.Hour > 0 { - s.Frequency = Daily - } else { - s.Frequency = Hourly - } - - return nil -} - -// ConnectInfo 数据库连接信息 -type ConnectInfo struct { - User string `json:"user"` - Password string `json:"password"` -} - -// ClusterDetail Cluster 的详细信息 -type ClusterDetail struct { - Basic BasicInfo `json:"basic"` - Resource ClusterResourceStatus `json:"resource"` - Backup BackupInfo `json:"backup"` -} - -// BasicInfo Cluster 的基本信息 -type BasicInfo struct { - ClusterInfo - RBDService - Status ClusterStatus `json:"status"` - Replicas []Status `json:"replicas"` - IsSupportBackup bool `json:"support_backup"` - IsSupportParameter bool `json:"support_parameter"` -} - -// ClusterResourceStatus Cluster 的实际资源状态信息 -type ClusterResourceStatus struct { - // CPU:m(毫核) - CPUMilli int64 `json:"cpu"` - - // 内存:Mi(兆字节) - MemoryMi int64 `json:"memory"` - - // 磁盘:Gi(吉字节) - StorageGi int64 `json:"storage"` - - // 副本数 - Replicas int32 `json:"replicas"` -} - -// BackupInfo Cluster 的备份信息 -type BackupInfo struct { - ClusterBackup -} - -// ClusterStatus Cluster 的状态信息 -type ClusterStatus struct { - Status string `json:"status"` - StatusCN string `json:"status_cn"` - // StartTime Cluster 最后一次进入 Running/Ready 的时间,ISO 8601 格式(UTC) - StartTime string `json:"start_time,omitempty"` -} - -// Status 副本状态信息 -type Status struct { - Name string `json:"name"` - Component string `json:"component,omitempty"` - Status corev1.PodPhase `json:"status"` - Ready bool `json:"ready"` - Containers []ReplicaContainer `json:"containers,omitempty"` -} - -// ReplicaContainer 副本中的容器 -type ReplicaContainer struct { - Name string `json:"name"` -} - -// ComponentName 组件名称 -type ComponentName string - -// ExpansionContext 伸缩操作的上下文 -// -// Components 中记录了各个组件的 Desire Status,支持多组件不同规格伸缩 -type ExpansionContext struct { - Cluster *kbappsv1.Cluster - Components map[ComponentName]ComponentExpansionContext // 组件名称 -> 伸缩操作的上下文 -} - -// ComponentExpansionContext 单个组件伸缩操作的上下文 -type ComponentExpansionContext struct { - // 水平伸缩 - CurrentReplicas int32 - DesiredReplicas int32 - - // 垂直伸缩 - CurrentCPU resource.Quantity - CurrentMem resource.Quantity - DesiredCPU resource.Quantity - DesiredMem resource.Quantity - - // 存储扩容 - HasPVC bool - VolumeTplName string - CurrentStorage resource.Quantity - DesiredStorage resource.Quantity - StorageClassRef *string -} - -// HorizontalScalingOpsParams 用于水平伸缩的 OpsRequest -// 支持多组件同规格伸缩 -type HorizontalScalingOpsParams struct { - Cluster *kbappsv1.Cluster - Components []ComponentHorizontalScaling -} - -// ComponentHorizontalScaling 单个组件水平伸缩 -type ComponentHorizontalScaling struct { - Name string - DeltaReplicas int32 -} - -// VerticalScalingOpsParams 用于垂直伸缩的 OpsRequest -// 支持多组件同规格伸缩 -type VerticalScalingOpsParams struct { - Cluster *kbappsv1.Cluster - Components []ComponentVerticalScaling -} - -// ComponentVerticalScaling 单个组件垂直伸缩 -type ComponentVerticalScaling struct { - Name string - CPU resource.Quantity - Memory resource.Quantity -} - -// VolumeExpansionOpsParams 用于存储扩容的 OpsRequest -// 支持多组件同规格扩容 -type VolumeExpansionOpsParams struct { - Cluster *kbappsv1.Cluster - Components []ComponentVolumeExpansion -} - -// ComponentVolumeExpansion 单个组件存储扩容 -type ComponentVolumeExpansion struct { - Name string - VolumeClaimTemplateName string - Storage resource.Quantity -} - -type BatchOperationResult struct { - Succeeded []string `json:"succeeded"` - Failed map[string]error `json:"failed"` -} - -func NewBatchOperationResult() *BatchOperationResult { - return &BatchOperationResult{ - Succeeded: make([]string, 0), - Failed: make(map[string]error), - } -} - -func (result *BatchOperationResult) AddSucceeded(serviceID string) { - result.Succeeded = append(result.Succeeded, serviceID) -} - -func (result *BatchOperationResult) AddFailed(serviceID string, err error) { - result.Failed[serviceID] = err -} - -// PodDetail 用于适配 Rainbond 中的 Instance -// -// 形如: -// -// "bean": { -// "name": "pod-xxx-abcdef", -// "node_ip": "10.0.0.1", -// "start_time": "2023-07-20T08:21:00Z", -// "ip": "172.20.0.2", -// "version": "v1.2.3", -// -// "namespace": "default", -// -// "status": { -// "type_str": "running", -// "reason": "ContainersNotReady", -// "message": "Waiting for container to start", -// "advice": "OutOfMemory" -// }, -// "containers": [ -// { -// "component_def": "postgresql-12-1.0.0", // 来自 cluster 而不是从 pod 获取 -// "limit_memory": "512Mi", -// "limit_cpu": "0.5", -// "started": "2023-07-20T08:22:00Z", -// "state": "Running", -// "reason": "" -// } -// ], -// "events": [ -// { -// "type": "Normal", -// "reason": "Pulled", -// "age": "5m", -// "message": "Successfully pulled image" -// } -// ] -// } -// } -type PodDetail struct { - Name string `json:"name"` - NodeIP string `json:"node_ip"` - StartTime string `json:"start_time"` - IP string `json:"ip"` - Version string `json:"version"` // Cluster.spec.componentSpecs.componentDef:componentDef: postgresql-12-1.0.0 - Namespace string `json:"namespace"` - Status PodStatus `json:"status"` - Containers []Container `json:"containers"` - Events []PodEvent `json:"events"` -} - -// PodStatus 当前 Pod 的状态 -type PodStatus struct { - TypeStr string `json:"type_str"` - Reason string `json:"reason"` - Message string `json:"message"` - Advice string `json:"advice"` -} - -// Container Pod 中的 Container 信息 -type Container struct { - ComponentDef string `json:"component_def"` - LimitMemory string `json:"limit_memory"` - LimitCPU string `json:"limit_cpu"` - Started string `json:"started"` - State string `json:"state"` - Reason string `json:"reason"` -} - -// PodEvent Pod 中的 Event 信息 -type PodEvent struct { - Type string `json:"type"` - Reason string `json:"reason"` - Age string `json:"age"` - Message string `json:"message"` -} - -// EventItem 用于适配 Rainbond 的操作记录 -type EventItem struct { - OpsName string `json:"event_id"` - OpsType string `json:"opt_type"` - UserName string `json:"user_name,omitempty"` - Status string `json:"status"` - FinalStatus string `json:"final_status"` - Message string `json:"message"` - Reason string `json:"reason"` - CreateTime string `json:"create_time"` - EndTime string `json:"end_time"` -} diff --git a/plugins/kb-adapter-rbdplugin/internal/model/common.go b/plugins/kb-adapter-rbdplugin/internal/model/common.go deleted file mode 100644 index 3c296ebe5..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/model/common.go +++ /dev/null @@ -1,30 +0,0 @@ -package model - -type Pagination struct { - Page int `query:"page" default:"1"` - PageSize int `query:"page_size" default:"6"` -} - -func (p *Pagination) Validate() { - if p.Page <= 0 { - p.Page = 1 - } - - if p.PageSize <= 0 { - p.PageSize = 6 - } - - if p.PageSize > 100 { - p.PageSize = 100 - } -} - -type Search struct { - Keyword string `query:"keyword"` -} - -// PaginatedResult 分页查询结果 -type PaginatedResult[T any] struct { - Items []T `json:"items"` // 当前页数据 - Total int `json:"total"` // 总数据量 -} diff --git a/plugins/kb-adapter-rbdplugin/internal/model/doc.go b/plugins/kb-adapter-rbdplugin/internal/model/doc.go deleted file mode 100644 index 183851377..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/model/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package model 提供业务模型定义,供业务层使用 -package model diff --git a/plugins/kb-adapter-rbdplugin/internal/model/resource.go b/plugins/kb-adapter-rbdplugin/internal/model/resource.go deleted file mode 100644 index 507684014..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/model/resource.go +++ /dev/null @@ -1,74 +0,0 @@ -package model - -const ( - ParameterTypeString ParameterType = "string" - ParameterTypeInteger ParameterType = "integer" - ParameterTypeNumber ParameterType = "number" - ParameterTypeBoolean ParameterType = "boolean" -) - -type ParameterType string - -type StorageClasses []string - -// Addon 表示 KubeBlocks 支持的数据库类型及其版本 -type Addon struct { - Type string `json:"type"` - Version []string `json:"version"` - IsSupportBackup bool `json:"support_backup"` -} - -// ParameterSets 保存静态/动态/不可变参数集合。 -type ParameterSets struct { - Static map[string]bool - Dynamic map[string]bool - Immutable map[string]bool -} - -// ParameterEntry 通过 configmap 获取到的实际被设置的 parameter -type ParameterEntry struct { - Name string `json:"name"` // 参数名称 - Value any `json:"value"` // Value 参数值 -} - -// Parameter 参数信息 -// -// 标明参数的名称、值、数据类型、最小值、最大值、枚举值、描述、是否为动态参数、是否为必填项、是否为 immutable -// -// 字段 Value 需要遵守 Parameter 中的约束 -// -// 在提供给 Rainbond 使用时,需要将来自 ParametersDefinition 的默认值使用实际的 ParameterEntry 覆盖 -type Parameter struct { - ParameterEntry - Type ParameterType `json:"type"` // Type 参数数据类型(受限集合) - MinValue *float64 `json:"min_value,omitempty"` // MinValue 参数最小值, 仅数值类型有效,除此之外的为 nil - MaxValue *float64 `json:"max_value,omitempty"` // MaxValue 参数最大值, 仅数值类型有效,除此之外的为 nil - EnumValues []string `json:"enum_values,omitempty"` // EnumValues 参数枚举值, 仅枚举类型有效,除此之外的为 nil - Description string `json:"description"` // Description 参数描述 - IsDynamic bool `json:"is_dynamic"` // IsDynamic 是否为动态参数, 动态参数支持热更新,静态参数需要重启数据库 - IsRequired bool `json:"is_required"` // IsRequired 是否为必填参数 - IsImmutable bool `json:"is_immutable,omitempty"` // IsImmutable 是否为不可变参数(只在内部使用/校验) -} - -type ClusterParametersChange struct { - RBDService - Parameters []ParameterEntry `json:"changes"` -} - -// ParameterChangeResult 参数变更结果 -type ParameterChangeResult struct { - Applied []string `json:"applied"` // 成功应用的参数名称列表 - Invalids []ParameterChangeError `json:"invalids"` // 校验失败的参数 -} - -// ParameterChangeError 参数变更错误 -type ParameterChangeError struct { - Name string `json:"name"` // 参数名称 - Code string `json:"code"` // 错误码 -} - -type ClusterParametersQuery struct { - RBDService - Pagination - Search -} diff --git a/plugins/kb-adapter-rbdplugin/internal/mono/mono.go b/plugins/kb-adapter-rbdplugin/internal/mono/mono.go deleted file mode 100644 index 298942be4..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/mono/mono.go +++ /dev/null @@ -1,91 +0,0 @@ -// Package mono contains some useful utilities. -package mono - -import ( - "crypto/rand" - "fmt" - "math/big" - "sort" - - corev1 "k8s.io/api/core/v1" -) - -// Filter 过滤切片 -func Filter[T any](in []T, fn func(T) bool) []T { - out := make([]T, 0, len(in)) - for _, v := range in { - if fn(v) { - out = append(out, v) - } - } - return out -} - -// Sorted 对字符串切片进行排序并返回新切片 -// 确保返回确定性顺序,不修改原切片 -func Sorted(slice []string) []string { - result := make([]string, len(slice)) - copy(result, slice) - sort.Strings(result) - return result -} - -// FilterThenSort 先过滤再排序,返回确定性顺序的结果 -// -// 参数顺序:数据 -> 过滤条件 -> 排序条件 -func FilterThenSort[T any](in []T, filterFn func(T) bool, lessFn func(T, T) bool) []T { - filtered := Filter(in, filterFn) - sort.Slice(filtered, func(i, j int) bool { - return lessFn(filtered[i], filtered[j]) - }) - return filtered -} - -// GetSecretField 从 Secret 中获取指定字段的值 -func GetSecretField(secret *corev1.Secret, field string) (string, error) { - data, exists := secret.Data[field] - if !exists { - return "", fmt.Errorf("field %s not found in secret %s/%s", field, secret.Namespace, secret.Name) - } - - // 检查数据是否为空 - if len(data) == 0 { - return "", fmt.Errorf("field %s is empty in secret %s/%s", field, secret.Namespace, secret.Name) - } - - return string(data), nil -} - -func GeneratePWD(length int) string { - const ( - upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - lower = "abcdefghijklmnopqrstuvwxyz" - digits = "0123456789" - symbols = "-_" - ) - charset := upper + lower + digits + symbols - - pwd := make([]byte, 0, length) - - randChar := func(s string) byte { - n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(s)))) - return s[int(n.Int64())] - } - - pwd = append(pwd, randChar(upper)) - pwd = append(pwd, randChar(lower)) - pwd = append(pwd, randChar(digits)) - pwd = append(pwd, randChar(symbols)) - - for len(pwd) < length { - pwd = append(pwd, randChar(charset)) - } - - for i := len(pwd) - 1; i > 0; i-- { - jBig, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) - j := int(jBig.Int64()) - pwd[i], pwd[j] = pwd[j], pwd[i] - } - - return string(pwd) -} diff --git a/plugins/kb-adapter-rbdplugin/internal/testutil/builder.go b/plugins/kb-adapter-rbdplugin/internal/testutil/builder.go deleted file mode 100644 index 5a7848576..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/testutil/builder.go +++ /dev/null @@ -1,682 +0,0 @@ -package testutil - -import ( - "time" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - opv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" -) - -const ( - // TestServiceID 默认的测试 service_id - TestServiceID = "test-service-id" - // TestNamespace 默认的测试命名空间 - TestNamespace = "default" -) - -// Resources 创建资源列表 -func Resources(cpu, memory string) corev1.ResourceList { - return corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(cpu), - corev1.ResourceMemory: resource.MustParse(memory), - } -} - -// ClusterBuilder 链式构建 Cluster -type ClusterBuilder struct { - cluster *kbappsv1.Cluster -} - -// NewClusterBuilder 创建 ClusterBuilder -func NewClusterBuilder(name, namespace string) *ClusterBuilder { - return &ClusterBuilder{ - cluster: &kbappsv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: make(map[string]string), - }, - Spec: kbappsv1.ClusterSpec{}, - }, - } -} - -// WithClusterDef 设置 cluster definition -func (cb *ClusterBuilder) WithClusterDef(clusterDef string) *ClusterBuilder { - cb.cluster.Spec.ClusterDef = clusterDef - return cb -} - -// WithServiceID 设置 service_id 标签 -func (cb *ClusterBuilder) WithServiceID(serviceID string) *ClusterBuilder { - cb.cluster.Labels[index.ServiceIDLabel] = serviceID - return cb -} - -// WithComponent 添加组件 -func (cb *ClusterBuilder) WithComponent(name, componentDef string) *ClusterBuilder { - cb.cluster.Spec.ComponentSpecs = append(cb.cluster.Spec.ComponentSpecs, kbappsv1.ClusterComponentSpec{ - Name: name, - ComponentDef: componentDef, - Replicas: 1, - }) - return cb -} - -// WithComponentServiceVersion 设置组件的 serviceVersion -func (cb *ClusterBuilder) WithComponentServiceVersion(componentName, serviceVersion string) *ClusterBuilder { - for i := range cb.cluster.Spec.ComponentSpecs { - if cb.cluster.Spec.ComponentSpecs[i].Name == componentName { - cb.cluster.Spec.ComponentSpecs[i].ServiceVersion = serviceVersion - break - } - } - return cb -} - -// WithComponentReplicas 设置组件副本数 -func (cb *ClusterBuilder) WithComponentReplicas(componentName string, replicas int32) *ClusterBuilder { - for i := range cb.cluster.Spec.ComponentSpecs { - if cb.cluster.Spec.ComponentSpecs[i].Name == componentName { - cb.cluster.Spec.ComponentSpecs[i].Replicas = replicas - break - } - } - return cb -} - -// WithComponentResources 设置组件资源 -func (cb *ClusterBuilder) WithComponentResources(componentName string, requests, limits corev1.ResourceList) *ClusterBuilder { - for i := range cb.cluster.Spec.ComponentSpecs { - if cb.cluster.Spec.ComponentSpecs[i].Name == componentName { - cb.cluster.Spec.ComponentSpecs[i].Resources = corev1.ResourceRequirements{ - Requests: requests, - Limits: limits, - } - break - } - } - return cb -} - -// WithComponentVolumeClaimTemplate 为组件添加存储声明模板 -func (cb *ClusterBuilder) WithComponentVolumeClaimTemplate(componentName, volumeName, storageClass, storageSize string) *ClusterBuilder { - for i := range cb.cluster.Spec.ComponentSpecs { - if cb.cluster.Spec.ComponentSpecs[i].Name == componentName { - template := kbappsv1.ClusterComponentVolumeClaimTemplate{ - Name: volumeName, - Spec: kbappsv1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.MustParse(storageSize), - }, - }, - }, - } - if storageClass != "" { - template.Spec.StorageClassName = &storageClass - } - cb.cluster.Spec.ComponentSpecs[i].VolumeClaimTemplates = append( - cb.cluster.Spec.ComponentSpecs[i].VolumeClaimTemplates, template) - break - } - } - return cb -} - -// WithSystemAccountSecret 为第一个 ComponentSpec 配置 SystemAccount -func (cb *ClusterBuilder) WithSystemAccountSecret(accountName, secretName string) *ClusterBuilder { - if len(cb.cluster.Spec.ComponentSpecs) == 0 { - return cb - } - - cb.cluster.Spec.ComponentSpecs[0].SystemAccounts = []kbappsv1.ComponentSystemAccount{ - { - Name: accountName, - SecretRef: &kbappsv1.ProvisionSecretRef{ - Name: secretName, - Namespace: cb.cluster.Namespace, - }, - }, - } - return cb -} - -// WithPhase 设置 Cluster 状态 -func (cb *ClusterBuilder) WithPhase(phase kbappsv1.ClusterPhase) *ClusterBuilder { - cb.cluster.Status.Phase = phase - return cb -} - -// WithTerminationPolicy 设置 Cluster 终止策略 -func (cb *ClusterBuilder) WithTerminationPolicy(policy kbappsv1.TerminationPolicyType) *ClusterBuilder { - cb.cluster.Spec.TerminationPolicy = policy - return cb -} - -// WithBackup 设置 Cluster 备份配置 -func (cb *ClusterBuilder) WithBackup(backup *kbappsv1.ClusterBackup) *ClusterBuilder { - cb.cluster.Spec.Backup = backup - return cb -} - -// Build 构建 Cluster 对象 -func (cb *ClusterBuilder) Build() *kbappsv1.Cluster { - return cb.cluster -} - -// Cluster 预设模板函数 - -// NewMySQLCluster 创建一个 MySQL 集群的预设模板 -func NewMySQLCluster(name, namespace string) *ClusterBuilder { - return NewClusterBuilder(name, namespace). - WithClusterDef("mysql"). - WithComponent("mysql", "mysql-8.0") -} - -// NewPostgreSQLCluster 创建一个 PostgreSQL 集群的预设模板 -func NewPostgreSQLCluster(name, namespace string) *ClusterBuilder { - return NewClusterBuilder(name, namespace). - WithClusterDef("postgresql"). - WithComponent("postgresql", "postgresql-14") -} - -// NewRedisCluster 创建一个 Redis 集群的预设模板 (包含 redis + sentinel 两个组件) -func NewRedisCluster(name, namespace string) *ClusterBuilder { - return NewClusterBuilder(name, namespace). - WithClusterDef("redis"). - WithComponent("redis", "redis-5-1.0.1"). - WithComponentServiceVersion("redis", "5.0.12"). - WithComponent("redis-sentinel", "redis-sentinel-8-1.0.1"). - WithComponentServiceVersion("redis-sentinel", "8.2.1") -} - -// BackupBuilder 链式构建 Backup -type BackupBuilder struct { - backup *datav1alpha1.Backup -} - -// NewBackupBuilder 创建 BackupBuilder -func NewBackupBuilder(name, namespace string) *BackupBuilder { - return &BackupBuilder{ - backup: &datav1alpha1.Backup{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: make(map[string]string), - }, - Spec: datav1alpha1.BackupSpec{}, - }, - } -} - -// WithBackupPolicyRef 设置备份策略引用 -func (bb *BackupBuilder) WithBackupPolicyRef(policyName string) *BackupBuilder { - bb.backup.Spec.BackupPolicyName = policyName - return bb -} - -// WithBackupMethod 设置备份方法 -func (bb *BackupBuilder) WithBackupMethod(method string) *BackupBuilder { - bb.backup.Spec.BackupMethod = method - return bb -} - -// WithInstanceName 设置实例名称标签 -func (bb *BackupBuilder) WithInstanceName(instance string) *BackupBuilder { - bb.backup.Labels[index.InstanceLabel] = instance - return bb -} - -// WithServiceID 设置 service_id 标签 -func (bb *BackupBuilder) WithServiceID(serviceID string) *BackupBuilder { - bb.backup.Labels[index.ServiceIDLabel] = serviceID - return bb -} - -// WithClusterInstance 设置集群实例标签 -func (bb *BackupBuilder) WithClusterInstance(clusterName string) *BackupBuilder { - bb.backup.Labels[constant.AppInstanceLabelKey] = clusterName - return bb -} - -// WithPhase 设置 Backup 状态 -func (bb *BackupBuilder) WithPhase(phase datav1alpha1.BackupPhase) *BackupBuilder { - bb.backup.Status.Phase = phase - return bb -} - -// WithCreationTime 设置创建时间 -func (bb *BackupBuilder) WithCreationTime(t time.Time) *BackupBuilder { - bb.backup.CreationTimestamp = metav1.Time{Time: t} - return bb -} - -// WithStartTime 设置开始时间 -func (bb *BackupBuilder) WithStartTime(t time.Time) *BackupBuilder { - bb.backup.Status.StartTimestamp = &metav1.Time{Time: t} - return bb -} - -// WithDeletionTimestamp 设置删除时间戳(模拟正在删除的对象) -func (bb *BackupBuilder) WithDeletionTimestamp() *BackupBuilder { - now := metav1.Time{Time: time.Now()} - bb.backup.DeletionTimestamp = &now - // fake client 要求有 DeletionTimestamp 的对象必须有 finalizers - bb.backup.Finalizers = []string{"test.finalizer/cleanup"} - return bb -} - -// Build 构建 Backup 对象 -func (bb *BackupBuilder) Build() *datav1alpha1.Backup { - return bb.backup -} - -// SecretBuilder 链式构建 Secret -type SecretBuilder struct { - secret *corev1.Secret -} - -// NewSecretBuilder 创建 SecretBuilder -func NewSecretBuilder(name, namespace string) *SecretBuilder { - return &SecretBuilder{ - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: make(map[string]string), - }, - Type: corev1.SecretTypeOpaque, - Data: make(map[string][]byte), - }, - } -} - -// WithType 设置 Secret 类型 -func (sb *SecretBuilder) WithType(secretType corev1.SecretType) *SecretBuilder { - sb.secret.Type = secretType - return sb -} - -// WithData 设置 Secret data 字段(字节数组) -func (sb *SecretBuilder) WithData(key string, value []byte) *SecretBuilder { - sb.secret.Data[key] = value - return sb -} - -// WithStringData 设置 Secret data 字段(字符串,自动转换为字节数组) -func (sb *SecretBuilder) WithStringData(key, value string) *SecretBuilder { - sb.secret.Data[key] = []byte(value) - return sb -} - -// WithLabels 设置 Secret labels -func (sb *SecretBuilder) WithLabels(labels map[string]string) *SecretBuilder { - for k, v := range labels { - sb.secret.Labels[k] = v - } - return sb -} - -// WithServiceID 设置 service_id 标签 -func (sb *SecretBuilder) WithServiceID(serviceID string) *SecretBuilder { - sb.secret.Labels[index.ServiceIDLabel] = serviceID - return sb -} - -// WithImmutable 设置 Secret 是否不可变 -func (sb *SecretBuilder) WithImmutable(immutable bool) *SecretBuilder { - sb.secret.Immutable = ptr.To(immutable) - return sb -} - -// Build 构建 Secret 对象 -func (sb *SecretBuilder) Build() *corev1.Secret { - return sb.secret -} - -// OpsRequestBuilder 提供链式 API 来构建 OpsRequest 对象 -type OpsRequestBuilder struct { - opsRequest *opv1alpha1.OpsRequest -} - -// NewOpsRequestBuilder 创建一个新的 OpsRequestBuilder -func NewOpsRequestBuilder(name, namespace string) *OpsRequestBuilder { - return &OpsRequestBuilder{ - opsRequest: &opv1alpha1.OpsRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: make(map[string]string), - }, - Spec: opv1alpha1.OpsRequestSpec{}, - }, - } -} - -// WithClusterName 设置目标集群名称 -func (orb *OpsRequestBuilder) WithClusterName(clusterName string) *OpsRequestBuilder { - orb.opsRequest.Spec.ClusterName = clusterName - return orb -} - -// WithType 设置操作类型 -func (orb *OpsRequestBuilder) WithType(opsType opv1alpha1.OpsType) *OpsRequestBuilder { - orb.opsRequest.Spec.Type = opsType - orb.opsRequest.Labels[constant.OpsRequestTypeLabelKey] = string(opsType) - return orb -} - -// WithCancel 设置取消操作 -func (orb *OpsRequestBuilder) WithCancel() *OpsRequestBuilder { - orb.opsRequest.Spec.Cancel = true - return orb -} - -// WithPhase 设置操作状态 -func (orb *OpsRequestBuilder) WithPhase(phase opv1alpha1.OpsPhase) *OpsRequestBuilder { - orb.opsRequest.Status.Phase = phase - return orb -} - -// WithServiceID 设置 service_id 标签 -func (orb *OpsRequestBuilder) WithServiceID(serviceID string) *OpsRequestBuilder { - orb.opsRequest.Labels[index.ServiceIDLabel] = serviceID - return orb -} - -// WithInstanceLabel 设置 cluster instance 标签 -func (orb *OpsRequestBuilder) WithInstanceLabel(clusterName string) *OpsRequestBuilder { - orb.opsRequest.Labels[index.InstanceLabel] = clusterName - return orb -} - -// WithRestore 设置 Restore 操作的 Spec -func (orb *OpsRequestBuilder) WithRestore(backupName string) *OpsRequestBuilder { - orb.opsRequest.Spec.Restore = &opv1alpha1.Restore{ - BackupName: backupName, - VolumeRestorePolicy: "Serial", - DeferPostReadyUntilClusterRunning: true, - } - return orb -} - -// WithRestart 设置 Restart 操作的 Spec -func (orb *OpsRequestBuilder) WithRestart(componentName string) *OpsRequestBuilder { - orb.opsRequest.Spec.RestartList = []opv1alpha1.ComponentOps{ - { - ComponentName: componentName, - }, - } - return orb -} - -// Build 构建 OpsRequest 对象 -func (orb *OpsRequestBuilder) Build() *opv1alpha1.OpsRequest { - return orb.opsRequest -} - -// StorageClassBuilder 链式构建 StorageClass -type StorageClassBuilder struct { - storageClass *storagev1.StorageClass -} - -// NewStorageClassBuilder 创建 StorageClassBuilder -func NewStorageClassBuilder(name string) *StorageClassBuilder { - return &StorageClassBuilder{ - storageClass: &storagev1.StorageClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Provisioner: "test-provisioner", - }, - } -} - -// WithAllowVolumeExpansion 设置是否允许存储扩容 -func (scb *StorageClassBuilder) WithAllowVolumeExpansion(allow bool) *StorageClassBuilder { - scb.storageClass.AllowVolumeExpansion = &allow - return scb -} - -// WithProvisioner 设置存储提供者 -func (scb *StorageClassBuilder) WithProvisioner(provisioner string) *StorageClassBuilder { - scb.storageClass.Provisioner = provisioner - return scb -} - -// Build 构建 StorageClass 对象 -func (scb *StorageClassBuilder) Build() *storagev1.StorageClass { - return scb.storageClass -} - -// BackupRepoBuilder 链式构建 BackupRepo -type BackupRepoBuilder struct { - backupRepo *datav1alpha1.BackupRepo -} - -// NewBackupRepoBuilder 创建 BackupRepoBuilder -func NewBackupRepoBuilder(name string) *BackupRepoBuilder { - return &BackupRepoBuilder{ - backupRepo: &datav1alpha1.BackupRepo{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: datav1alpha1.BackupRepoSpec{}, - Status: datav1alpha1.BackupRepoStatus{}, - }, - } -} - -// WithStorageProvider 设置存储提供者 -func (brb *BackupRepoBuilder) WithStorageProvider(provider string) *BackupRepoBuilder { - brb.backupRepo.Spec.StorageProviderRef = provider - return brb -} - -// WithAccessMethod 设置访问方法 -func (brb *BackupRepoBuilder) WithAccessMethod(method datav1alpha1.AccessMethod) *BackupRepoBuilder { - brb.backupRepo.Spec.AccessMethod = method - return brb -} - -// WithPhase 设置 BackupRepo 状态 -func (brb *BackupRepoBuilder) WithPhase(phase datav1alpha1.BackupRepoPhase) *BackupRepoBuilder { - brb.backupRepo.Status.Phase = phase - return brb -} - -// Build 构建 BackupRepo 对象 -func (brb *BackupRepoBuilder) Build() *datav1alpha1.BackupRepo { - return brb.backupRepo -} - -// InstanceSetBuilder 链式构建 InstanceSet -type InstanceSetBuilder struct { - instanceSet *workloadsv1.InstanceSet -} - -// NewInstanceSetBuilder 创建 InstanceSetBuilder -func NewInstanceSetBuilder(name, namespace string) *InstanceSetBuilder { - return &InstanceSetBuilder{ - instanceSet: &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: make(map[string]string), - Annotations: make(map[string]string), - }, - Spec: workloadsv1.InstanceSetSpec{}, - Status: workloadsv1.InstanceSetStatus{}, - }, - } -} - -// WithClusterInstance 设置集群实例标签 -func (isb *InstanceSetBuilder) WithClusterInstance(clusterName string) *InstanceSetBuilder { - isb.instanceSet.Labels[constant.AppInstanceLabelKey] = clusterName - return isb -} - -// WithComponentName 设置组件名称标签 -func (isb *InstanceSetBuilder) WithComponentName(componentName string) *InstanceSetBuilder { - isb.instanceSet.Labels["apps.kubeblocks.io/component-name"] = componentName - return isb -} - -// WithComponentAnnotation 设置组件注解 -func (isb *InstanceSetBuilder) WithComponentAnnotation(component string) *InstanceSetBuilder { - isb.instanceSet.Annotations["app.kubernetes.io/component"] = component - return isb -} - -// WithServiceVersionAnnotation 设置服务版本注解 -func (isb *InstanceSetBuilder) WithServiceVersionAnnotation(serviceVersion string) *InstanceSetBuilder { - isb.instanceSet.Annotations["apps.kubeblocks.io/service-version"] = serviceVersion - return isb -} - -// WithReplicas 设置副本数 -func (isb *InstanceSetBuilder) WithReplicas(replicas int32) *InstanceSetBuilder { - isb.instanceSet.Spec.Replicas = &replicas - return isb -} - -// WithInstanceStatus 设置实例状态 -func (isb *InstanceSetBuilder) WithInstanceStatus(podNames ...string) *InstanceSetBuilder { - instanceStatus := make([]workloadsv1.InstanceStatus, len(podNames)) - for i, podName := range podNames { - instanceStatus[i] = workloadsv1.InstanceStatus{ - PodName: podName, - } - } - isb.instanceSet.Status.InstanceStatus = instanceStatus - return isb -} - -// WithAvailableReplicas 设置可用副本数 -func (isb *InstanceSetBuilder) WithAvailableReplicas(available int32) *InstanceSetBuilder { - isb.instanceSet.Status.AvailableReplicas = available - return isb -} - -// WithReadyReplicas 设置就绪副本数 -func (isb *InstanceSetBuilder) WithReadyReplicas(ready int32) *InstanceSetBuilder { - isb.instanceSet.Status.ReadyReplicas = ready - return isb -} - -// Build 构建 InstanceSet 对象 -func (isb *InstanceSetBuilder) Build() *workloadsv1.InstanceSet { - return isb.instanceSet -} - -// NewParameterEntry - -func NewParameterEntry(name string, value any) model.ParameterEntry { - return model.ParameterEntry{Name: name, Value: value} -} - -// ParameterConstraintBuilder 链式构建参数约束 -type ParameterConstraintBuilder struct { - param model.Parameter -} - -// NewParameterConstraint 创建参数约束构建器 -func NewParameterConstraint(name string) *ParameterConstraintBuilder { - return &ParameterConstraintBuilder{ - param: model.Parameter{ - ParameterEntry: model.ParameterEntry{Name: name}, - }, - } -} - -// WithType 设置参数类型 -func (pcb *ParameterConstraintBuilder) WithType(paramType model.ParameterType) *ParameterConstraintBuilder { - pcb.param.Type = paramType - return pcb -} - -// WithRange 设置参数范围 -func (pcb *ParameterConstraintBuilder) WithRange(min, max *float64) *ParameterConstraintBuilder { - pcb.param.MinValue = min - pcb.param.MaxValue = max - return pcb -} - -// WithEnumValues 设置枚举值 -func (pcb *ParameterConstraintBuilder) WithEnumValues(enums []string) *ParameterConstraintBuilder { - pcb.param.EnumValues = enums - return pcb -} - -// WithDynamic 设置是否为动态参数 -func (pcb *ParameterConstraintBuilder) WithDynamic(dynamic bool) *ParameterConstraintBuilder { - pcb.param.IsDynamic = dynamic - return pcb -} - -// WithRequired 设置是否为必填参数 -func (pcb *ParameterConstraintBuilder) WithRequired(required bool) *ParameterConstraintBuilder { - pcb.param.IsRequired = required - return pcb -} - -// WithImmutable 设置是否为不可变参数 -func (pcb *ParameterConstraintBuilder) WithImmutable(immutable bool) *ParameterConstraintBuilder { - pcb.param.IsImmutable = immutable - return pcb -} - -// Build 构建参数约束 -func (pcb *ParameterConstraintBuilder) Build() model.Parameter { - return pcb.param -} - -// Parameter 工厂函数 - -// CreateTypicalMySQLParameterEntries 创建典型的 MySQL 参数条目 -func CreateTypicalMySQLParameterEntries() []model.ParameterEntry { - return []model.ParameterEntry{ - NewParameterEntry("max_connections", 100), - NewParameterEntry("innodb_buffer_pool_size", "128M"), - NewParameterEntry("sql_mode", "STRICT_TRANS_TABLES"), - NewParameterEntry("autocommit", "ON"), - NewParameterEntry("query_cache_size", 0), // 这个参数在约束中不存在,应被过滤 - } -} - -// CreateTypicalMySQLParameterConstraints 创建典型的 MySQL 参数约束 -func CreateTypicalMySQLParameterConstraints() map[string]model.Parameter { - return map[string]model.Parameter{ - "max_connections": NewParameterConstraint("max_connections"). - WithType(model.ParameterTypeInteger). - WithRange(ptr.To(1.0), ptr.To(100000.0)). - WithDynamic(true). - Build(), - "innodb_buffer_pool_size": NewParameterConstraint("innodb_buffer_pool_size"). - WithType(model.ParameterTypeString). - WithImmutable(true). - Build(), - "sql_mode": NewParameterConstraint("sql_mode"). - WithType(model.ParameterTypeString). - WithEnumValues([]string{`"STRICT_TRANS_TABLES"`, `"NO_ZERO_DATE"`}). - WithDynamic(true). - Build(), - "autocommit": NewParameterConstraint("autocommit"). - WithType(model.ParameterTypeBoolean). - WithDynamic(true). - Build(), - } -} diff --git a/plugins/kb-adapter-rbdplugin/internal/testutil/doc.go b/plugins/kb-adapter-rbdplugin/internal/testutil/doc.go deleted file mode 100644 index 24cd683ee..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/testutil/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package testutil 提供测试工具 -// -// testutil 中只提供用于简化测试的通用型辅助函数,具体的断言、构建应当在具体的测试文件中进行实现, -// 避免 testutil 中的函数过于复杂或承担过多的责任。 -package testutil diff --git a/plugins/kb-adapter-rbdplugin/internal/testutil/k8s.go b/plugins/kb-adapter-rbdplugin/internal/testutil/k8s.go deleted file mode 100644 index 47c81de1e..000000000 --- a/plugins/kb-adapter-rbdplugin/internal/testutil/k8s.go +++ /dev/null @@ -1,287 +0,0 @@ -package testutil - -import ( - "context" - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - opv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - parametersv1alpha1 "github.com/apecloud/kubeblocks/apis/parameters/v1alpha1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -// _scheme 包含所有测试需要的 API 类型 -var _scheme = func() *runtime.Scheme { - s := runtime.NewScheme() - _ = kbappsv1.AddToScheme(s) - _ = datav1alpha1.AddToScheme(s) - _ = corev1.AddToScheme(s) - _ = appsv1.AddToScheme(s) - _ = storagev1.AddToScheme(s) - _ = opv1alpha1.AddToScheme(s) - _ = parametersv1alpha1.AddToScheme(s) - _ = workloadsv1.AddToScheme(s) - return s -}() - -// NewFakeClient 创建支持字段索引的测试客户端(统一入口) -func NewFakeClient(objs ...client.Object) client.Client { - return NewFakeClientWithIndexes(objs...) -} - -// NewFakeClientWithIndexes 创建一个支持字段索引的测试客户端 -func NewFakeClientWithIndexes(objs ...client.Object) client.Client { - builder := fake.NewClientBuilder().WithScheme(_scheme).WithObjects(objs...) - - // 添加 Cluster 的 service_id 索引 - builder = builder.WithIndex(&kbappsv1.Cluster{}, index.ServiceIDField, func(obj client.Object) []string { - labels := obj.GetLabels() - if labels == nil { - return nil - } - if v, ok := labels[index.ServiceIDLabel]; ok && v != "" { - return []string{v} - } - return nil - }) - - // 添加 Deployment 的 service_id 索引 - builder = builder.WithIndex(&appsv1.Deployment{}, index.ServiceIDField, func(obj client.Object) []string { - labels := obj.GetLabels() - if labels == nil { - return nil - } - if v, ok := labels[index.ServiceIDLabel]; ok && v != "" { - return []string{v} - } - return nil - }) - - // 添加 OpsRequest 的 namespace/cluster/opsType 索引 - builder = builder.WithIndex(&opv1alpha1.OpsRequest{}, index.NamespaceClusterOpsTypeField, func(obj client.Object) []string { - opsRequest := obj.(*opv1alpha1.OpsRequest) - if opsRequest.Spec.ClusterName != "" { - return []string{fmt.Sprintf("%s/%s/%s", opsRequest.Namespace, opsRequest.Spec.ClusterName, opsRequest.Spec.Type)} - } - return nil - }) - - // 添加 Backup 的 namespace/instance 索引 - builder = builder.WithIndex(&datav1alpha1.Backup{}, index.NamespaceInstanceField, func(obj client.Object) []string { - backup := obj.(*datav1alpha1.Backup) - if instance, ok := backup.Labels[index.InstanceLabel]; ok && instance != "" { - return []string{fmt.Sprintf("%s/%s", backup.Namespace, instance)} - } - return nil - }) - - // 添加 Pod 的 namespace/instance 索引 - builder = builder.WithIndex(&corev1.Pod{}, index.NamespaceInstanceField, func(obj client.Object) []string { - pod := obj.(*corev1.Pod) - if instance, ok := pod.Labels[index.InstanceLabel]; ok && instance != "" { - return []string{fmt.Sprintf("%s/%s", pod.Namespace, instance)} - } - return nil - }) - - // 添加 InstanceSet 的 namespace/cluster/component 索引 - builder = builder.WithIndex(&workloadsv1.InstanceSet{}, index.NamespaceClusterComponentField, func(obj client.Object) []string { - instanceSet := obj.(*workloadsv1.InstanceSet) - if clusterName, ok := instanceSet.Labels[index.InstanceLabel]; ok && clusterName != "" { - if componentName, ok := instanceSet.Labels["apps.kubeblocks.io/component-name"]; ok { - return []string{fmt.Sprintf("%s/%s/%s", instanceSet.Namespace, clusterName, componentName)} - } - } - return nil - }) - - // 添加 Pod 事件的 namespace/pod 索引 - builder = builder.WithIndex(&corev1.Event{}, index.NamespacePodNameField, func(obj client.Object) []string { - event := obj.(*corev1.Event) - if event.InvolvedObject.Kind == "Pod" && event.InvolvedObject.Name != "" { - return []string{fmt.Sprintf("%s/%s", event.Namespace, event.InvolvedObject.Name)} - } - return nil - }) - - return builder.Build() -} - -// CreateObjects 创建多个对象 -func CreateObjects(ctx context.Context, c client.Client, objs []client.Object) error { - for _, obj := range objs { - if err := c.Create(ctx, obj); err != nil { - return err - } - } - return nil -} - -// ErrorClientBuilder 允许按需配置操作失败的 fake client -type ErrorClientBuilder struct { - client client.Client - createErr error - createTypeErrs map[string]error // 按类型的 Create 错误 - listErr error - getErr error - updateErr error - patchErr error - deleteErr error - deleteAllOfErr error -} - -// NewErrorClientBuilder 创建可配置失败行为的 client builder -func NewErrorClientBuilder(objs ...client.Object) *ErrorClientBuilder { - return &ErrorClientBuilder{ - client: NewFakeClient(objs...), - createTypeErrs: make(map[string]error), - } -} - -// WithCreateError 指定 Create 操作的错误 -func (b *ErrorClientBuilder) WithCreateError(err error) *ErrorClientBuilder { - b.createErr = err - return b -} - -// WithCreateErrorForType 为特定类型的对象指定 Create 错误 -func (b *ErrorClientBuilder) WithCreateErrorForType(obj client.Object, err error) *ErrorClientBuilder { - typeName := fmt.Sprintf("%T", obj) - b.createTypeErrs[typeName] = err - return b -} - -// WithListError 指定 List 操作的错误 -func (b *ErrorClientBuilder) WithListError(err error) *ErrorClientBuilder { - b.listErr = err - return b -} - -// WithGetError 指定 Get 操作的错误 -func (b *ErrorClientBuilder) WithGetError(err error) *ErrorClientBuilder { - b.getErr = err - return b -} - -// WithUpdateError 指定 Update 操作的错误 -func (b *ErrorClientBuilder) WithUpdateError(err error) *ErrorClientBuilder { - b.updateErr = err - return b -} - -// WithPatchError 指定 Patch 操作的错误 -func (b *ErrorClientBuilder) WithPatchError(err error) *ErrorClientBuilder { - b.patchErr = err - return b -} - -// WithDeleteError 指定 Delete 操作的错误 -func (b *ErrorClientBuilder) WithDeleteError(err error) *ErrorClientBuilder { - b.deleteErr = err - return b -} - -// WithDeleteAllOfError 指定 DeleteAllOf 操作的错误 -func (b *ErrorClientBuilder) WithDeleteAllOfError(err error) *ErrorClientBuilder { - b.deleteAllOfErr = err - return b -} - -// Build 构建最终的 client -func (b *ErrorClientBuilder) Build() client.Client { - return &errorClient{ - Client: b.client, - createErr: b.createErr, - createTypeErrs: b.createTypeErrs, - listErr: b.listErr, - getErr: b.getErr, - updateErr: b.updateErr, - patchErr: b.patchErr, - deleteErr: b.deleteErr, - deleteAllOfErr: b.deleteAllOfErr, - } -} - -type errorClient struct { - client.Client - createErr error - createTypeErrs map[string]error - listErr error - getErr error - updateErr error - patchErr error - deleteErr error - deleteAllOfErr error -} - -// Create 实现 Create 方法,根据配置返回错误 -func (f *errorClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { - // 如果指定了类型特定的错误,则返回该错误 - typeName := fmt.Sprintf("%T", obj) - if err, ok := f.createTypeErrs[typeName]; ok { - return err - } - - if f.createErr != nil { - return f.createErr - } - return f.Client.Create(ctx, obj, opts...) -} - -// List 实现 List 方法,根据配置返回错误 -func (f *errorClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - if f.listErr != nil { - return f.listErr - } - return f.Client.List(ctx, list, opts...) -} - -// Get 实现 Get 方法,根据配置返回错误 -func (f *errorClient) Get(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - if f.getErr != nil { - return f.getErr - } - return f.Client.Get(ctx, key, obj, opts...) -} - -// Update 实现 Update 方法,根据配置返回错误 -func (f *errorClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { - if f.updateErr != nil { - return f.updateErr - } - return f.Client.Update(ctx, obj, opts...) -} - -// Patch 实现 Patch 方法,根据配置返回错误 -func (f *errorClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { - if f.patchErr != nil { - return f.patchErr - } - return f.Client.Patch(ctx, obj, patch, opts...) -} - -// Delete 实现 Delete 方法,根据配置返回错误 -func (f *errorClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { - if f.deleteErr != nil { - return f.deleteErr - } - return f.Client.Delete(ctx, obj, opts...) -} - -// DeleteAllOf 实现 DeleteAllOf 方法,根据配置返回错误 -func (f *errorClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { - if f.deleteAllOfErr != nil { - return f.deleteAllOfErr - } - return f.Client.DeleteAllOf(ctx, obj, opts...) -} diff --git a/plugins/kb-adapter-rbdplugin/main.go b/plugins/kb-adapter-rbdplugin/main.go deleted file mode 100644 index fee88d91f..000000000 --- a/plugins/kb-adapter-rbdplugin/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os/signal" - "syscall" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/config" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/k8s" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service" -) - -func main() { - cfg := config.MustLoad() - - log.InitLogger() - defer log.Sync() - - log.Info("configuration loaded successfully", - log.String("host", cfg.Host), - log.String("port", cfg.Port)) - - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGKILL) - defer stop() - - if err := run(ctx); err != nil { - log.Fatal("application exited", log.Err(err)) - } -} - -func run(ctx context.Context) error { - mgr, err := k8s.NewManager() - if err != nil { - return fmt.Errorf("create manager: %w", err) - } - - services := service.New(mgr.GetClient()) - - if err := k8s.Setup(ctx, mgr, services); err != nil { - return fmt.Errorf("setup manager: %w", err) - } - - if err := mgr.Start(ctx); err != nil { - return fmt.Errorf("start manager: %w", err) - } - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/adapter/adapter.go b/plugins/kb-adapter-rbdplugin/service/adapter/adapter.go deleted file mode 100644 index b55d92b65..000000000 --- a/plugins/kb-adapter-rbdplugin/service/adapter/adapter.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package adapter 提供 KubeBlocks 的适配器实现 -package adapter - -import ( - "errors" - "fmt" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" -) - -var ( - ErrAdapterNotImplemented = errors.New("adapter not implemented") -) - -// ClusterBuilder 用于在 Rainbond 中 KubeBlocks Cluster 的创建 -type ClusterBuilder interface { - // BuildCluster 构建 Cluster struct - BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) -} - -// Coordinator 用于协调 KubeBlocks 和 Rainbond -type Coordinator interface { - // TargetPort 返回 KubeBlocks Cluster 的连接端口, - // 用于配置 KubeBlocksComponent 将连接转发至 Cluster 的 service - TargetPort() int - - // GetSecretName 返回该数据库类型的 Secret 命名格式 - GetSecretName(clusterName string) string - - // GetBackupMethod 返回该数据库类型支持的备份方法 - GetBackupMethod() string - - // GetParametersConfigMap 返回该类型的 Cluster 用于储存参数配置的 ConfigMap 名称, - // 并非所有的数据库类型都支持参数配置,不支持则返回 nil - GetParametersConfigMap(clusterName string) *string - - // ParseParameters 解析 ConfigMap 中的配置文件参数 - // configData 为 ConfigMap 的 data 字段,包含各种配置文件内容 - ParseParameters(configData map[string]string) ([]model.ParameterEntry, error) - - // SystemAccount 返回该数据库类型在使用 custom secret 时使用的 systemAccount.name 和数据库账户名称, - // 返回 nil 则表示不启用 custom secret - SystemAccount() *string -} - -// ClusterAdapter -// -// 必须实现的字段: -// -// - Builder -// -// - Coordinator -type ClusterAdapter struct { - Builder ClusterBuilder - Coordinator Coordinator -} - -// Validate 验证 ClusterAdapter 的完整性, -// 确保所有必须实现的接口字段都被正确设置 -func (ca *ClusterAdapter) Validate() error { - if ca.Builder == nil { - return fmt.Errorf("ClusterBuilder: %w", ErrAdapterNotImplemented) - } - - if ca.Coordinator == nil { - return fmt.Errorf("coordinator: %w", ErrAdapterNotImplemented) - } - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/backup/backup.go b/plugins/kb-adapter-rbdplugin/service/backup/backup.go deleted file mode 100644 index 4d3033c9f..000000000 --- a/plugins/kb-adapter-rbdplugin/service/backup/backup.go +++ /dev/null @@ -1,550 +0,0 @@ -package backup - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - ReasonBackupRunning = "备份正在执行中,无法删除" -) - -// Service 提供 BackupRepo 相关操作 -// 依赖 controller-runtime client -type Service struct { - client client.Client -} - -func NewService(c client.Client) *Service { - return &Service{ - client: c, - } -} - -// ListAvailableBackupRepos 返回集群内所有 BackupRepo,并保留 Ready/Failed/PreChecking 等状态。 -func (s *Service) ListAvailableBackupRepos(ctx context.Context) ([]*model.BackupRepo, error) { - return s.listBackupRepos(ctx) -} - -func (s *Service) CreateBackupRepo(ctx context.Context, input model.BackupRepoInput) (*model.BackupRepo, error) { - if err := validateBackupRepoInput(input, true); err != nil { - return nil, err - } - - if err := s.upsertBackupRepoSecret(ctx, input); err != nil { - return nil, err - } - - repo, err := buildBackupRepo(input) - if err != nil { - return nil, err - } - if err := s.client.Create(ctx, repo); err != nil { - return nil, fmt.Errorf("create BackupRepo %s: %w", input.Name, err) - } - return backupRepoToModel(repo), nil -} - -func (s *Service) UpdateBackupRepo(ctx context.Context, name string, input model.BackupRepoInput) (*model.BackupRepo, error) { - name = strings.TrimSpace(name) - if name == "" { - return nil, fmt.Errorf("backup repo name is required") - } - if input.Name == "" { - input.Name = name - } - if input.Name != name { - return nil, fmt.Errorf("backup repo name %s does not match path %s", input.Name, name) - } - if err := validateBackupRepoInput(input, false); err != nil { - return nil, err - } - - var repo datav1alpha1.BackupRepo - if err := s.client.Get(ctx, types.NamespacedName{Name: name}, &repo); err != nil { - return nil, fmt.Errorf("get BackupRepo %s: %w", name, err) - } - if input.StorageProvider != "" && input.StorageProvider != repo.Spec.StorageProviderRef { - return nil, fmt.Errorf("storageProviderRef is immutable") - } - if err := s.upsertBackupRepoSecret(ctx, input); err != nil { - return nil, err - } - - updated, err := buildBackupRepo(input) - if err != nil { - return nil, err - } - repo.Spec.AccessMethod = updated.Spec.AccessMethod - repo.Spec.VolumeCapacity = updated.Spec.VolumeCapacity - repo.Spec.PVReclaimPolicy = updated.Spec.PVReclaimPolicy - repo.Spec.Config = updated.Spec.Config - repo.Spec.Credential = updated.Spec.Credential - repo.Spec.PathPrefix = updated.Spec.PathPrefix - if err := s.client.Update(ctx, &repo); err != nil { - return nil, fmt.Errorf("update BackupRepo %s: %w", name, err) - } - return backupRepoToModel(&repo), nil -} - -func (s *Service) DeleteBackupRepo(ctx context.Context, name string) error { - name = strings.TrimSpace(name) - if name == "" { - return fmt.Errorf("backup repo name is required") - } - if err := s.ensureBackupRepoNotInUse(ctx, name); err != nil { - return err - } - - var repo datav1alpha1.BackupRepo - if err := s.client.Get(ctx, types.NamespacedName{Name: name}, &repo); err != nil { - return fmt.Errorf("get BackupRepo %s: %w", name, err) - } - if err := s.client.Delete(ctx, &repo); err != nil { - return fmt.Errorf("delete BackupRepo %s: %w", name, err) - } - if repo.Spec.Credential != nil && repo.Spec.Credential.Name != "" && repo.Spec.Credential.Namespace != "" { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: repo.Spec.Credential.Name, - Namespace: repo.Spec.Credential.Namespace, - }, - } - if err := s.client.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) { - return fmt.Errorf("delete BackupRepo credential %s/%s: %w", secret.Namespace, secret.Name, err) - } - } - return nil -} - -// ReScheduleBackup 重新调度 Cluster 的备份配置 -// -// 通过 Patch cluster 中的备份字段来实现 back schedule 的更新 -func (s *Service) ReScheduleBackup(ctx context.Context, schedule model.BackupScheduleInput) error { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, schedule.ServiceID) - if err != nil { - return fmt.Errorf("get cluster by service_id: %w", err) - } - - // Determine backup method based on cluster type (required by KubeBlocks) - adapter := registry.Cluster[kbkit.ClusterType(cluster)] - backupMethod := adapter.Coordinator.GetBackupMethod() - - needUpdate := false - var patchData map[string]any - if schedule.BackupRepo == "" { - if cluster.Spec.Backup != nil { - patchData = map[string]any{ - "spec": map[string]any{ - "backup": nil, - }, - } - needUpdate = true - } - } else { - if cluster.Spec.Backup == nil { - enabled := true - patchData = map[string]any{ - "spec": map[string]any{ - "backup": map[string]any{ - "repoName": schedule.BackupRepo, - "enabled": &enabled, - "method": backupMethod, - "cronExpression": schedule.Schedule.Cron(), - "retentionPeriod": schedule.RetentionPeriod, - }, - }, - } - needUpdate = true - } else { - backupPatch := make(map[string]any) - - if cluster.Spec.Backup.RepoName != schedule.BackupRepo { - backupPatch["repoName"] = schedule.BackupRepo - } - - if cluster.Spec.Backup.CronExpression != schedule.Schedule.Cron() { - backupPatch["cronExpression"] = schedule.Schedule.Cron() - } - - if cluster.Spec.Backup.RetentionPeriod != schedule.RetentionPeriod { - backupPatch["retentionPeriod"] = schedule.RetentionPeriod - } - - if cluster.Spec.Backup.Enabled == nil || !*cluster.Spec.Backup.Enabled { - enabled := true - backupPatch["enabled"] = &enabled - } - - // Always ensure method is present to satisfy validation - backupPatch["method"] = backupMethod - - if len(backupPatch) > 0 { - patchData = map[string]any{ - "spec": map[string]any{ - "backup": backupPatch, - }, - } - needUpdate = true - } - } - } - - if !needUpdate { - return nil - } - - patchBytes, err := json.Marshal(patchData) - if err != nil { - return fmt.Errorf("marshal patch data: %w", err) - } - - // Patch backup 配置 - if err := s.client.Patch(ctx, cluster, client.RawPatch(types.MergePatchType, patchBytes)); err != nil { - return fmt.Errorf("patch cluster backup configuration: %w", err) - } - - return nil -} - -// BackupCluster 执行集群备份操作 -// -// 参考:https://kubeblocks.io/docs/preview/kubeblocks-for-mysql/05-backup-restore/02-create-full-backup -func (s *Service) BackupCluster(ctx context.Context, req model.BackupInput) error { - log.Debug("Starting backup operation", - log.String("service_id", req.ServiceID), - ) - - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, req.ServiceID) - if err != nil { - return fmt.Errorf("get cluster by service_id: %w", err) - } - - if cluster.Status.Phase == kbappsv1.StoppedClusterPhase || cluster.Status.Phase == kbappsv1.StoppingClusterPhase { - return fmt.Errorf("cluster %s/%s is not running", cluster.Namespace, cluster.Name) - } - - adapter := registry.Cluster[kbkit.ClusterType(cluster)] - - if cluster.Spec.Backup == nil || !*cluster.Spec.Backup.Enabled { - return fmt.Errorf("backup is not enabled for cluster %s", cluster.Name) - } - - backupMethod := adapter.Coordinator.GetBackupMethod() - - if err := kbkit.CreateBackupOpsRequest(ctx, s.client, cluster, backupMethod); err != nil { - return fmt.Errorf("create backup opsrequest: %w", err) - } - - log.Info("Created backup OpsRequest", - log.String("cluster", cluster.Name), - log.String("backup_method", backupMethod)) - - return nil -} - -// ListBackups 返回给定的 Cluster 的备份列表 -func (s *Service) ListBackups(ctx context.Context, query model.BackupListQuery) (*model.PaginatedResult[model.BackupItem], error) { - query.Validate() - - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, query.ServiceID) - if err != nil { - return nil, err - } - - backups, err := getBackupsByIndex(ctx, s.client, cluster.Name, cluster.Namespace) - if err != nil { - return nil, err - } - - sortBackupsByTime(backups) - - backupList := make([]model.BackupItem, 0, len(backups)) - for _, backup := range backups { - backupTime := backup.CreationTimestamp.UTC() - if backup.Status.StartTimestamp != nil { - backupTime = backup.Status.StartTimestamp.UTC() - } - - backupPhase := datav1alpha1.BackupPhaseNew - if backup.Status.Phase != "" { - backupPhase = backup.Status.Phase - } - - backupList = append(backupList, model.BackupItem{ - Name: backup.Name, - Status: backupPhase, - Time: backupTime, - }) - } - - result := kbkit.Paginate(backupList, query.Page, query.PageSize) - - log.Debug("paginated backup list", - log.String("cluster", cluster.Name), - log.Any("backupList", backupList), - ) - - return &model.PaginatedResult[model.BackupItem]{ - Items: result, - Total: len(backupList), - }, nil -} - -// listBackupRepos 返回所有命名空间下的 BackupRepo 信息 -func (s *Service) listBackupRepos(ctx context.Context) ([]*model.BackupRepo, error) { - var repoList datav1alpha1.BackupRepoList - if err := s.client.List(ctx, &repoList); err != nil { - return nil, fmt.Errorf("list BackupRepo: %w", err) - } - result := make([]*model.BackupRepo, 0, len(repoList.Items)) - for _, item := range repoList.Items { - result = append(result, backupRepoToModel(&item)) - } - return result, nil -} - -func validateBackupRepoInput(input model.BackupRepoInput, requireSecret bool) error { - if strings.TrimSpace(input.Name) == "" { - return fmt.Errorf("backup repo name is required") - } - if strings.TrimSpace(input.StorageProvider) == "" { - return fmt.Errorf("storageProviderRef is required") - } - if input.Config == nil { - return fmt.Errorf("config is required") - } - if strings.TrimSpace(input.Config["bucket"]) == "" { - return fmt.Errorf("config.bucket is required") - } - if strings.TrimSpace(input.Config["endpoint"]) == "" { - return fmt.Errorf("config.endpoint is required") - } - if input.Credential.Name == "" || input.Credential.Namespace == "" { - return fmt.Errorf("credential.name and credential.namespace are required") - } - if requireSecret && (input.Secrets["accessKeyId"] == "" || input.Secrets["secretAccessKey"] == "") { - return fmt.Errorf("accessKeyId and secretAccessKey are required") - } - return nil -} - -func buildBackupRepo(input model.BackupRepoInput) (*datav1alpha1.BackupRepo, error) { - accessMethod := input.AccessMethod - if accessMethod == "" { - accessMethod = datav1alpha1.AccessMethodTool - } - reclaimPolicy := input.PVReclaimPolicy - if reclaimPolicy == "" { - reclaimPolicy = corev1.PersistentVolumeReclaimRetain - } - capacityText := strings.TrimSpace(input.VolumeCapacity) - if capacityText == "" { - capacityText = "100Gi" - } - capacity, err := resource.ParseQuantity(capacityText) - if err != nil { - return nil, fmt.Errorf("parse volumeCapacity %s: %w", capacityText, err) - } - - config := make(map[string]string, len(input.Config)) - for k, v := range input.Config { - config[k] = v - } - - return &datav1alpha1.BackupRepo{ - ObjectMeta: metav1.ObjectMeta{ - Name: input.Name, - }, - Spec: datav1alpha1.BackupRepoSpec{ - StorageProviderRef: input.StorageProvider, - AccessMethod: accessMethod, - VolumeCapacity: capacity, - PVReclaimPolicy: reclaimPolicy, - Config: config, - Credential: &corev1.SecretReference{ - Name: input.Credential.Name, - Namespace: input.Credential.Namespace, - }, - PathPrefix: input.PathPrefix, - }, - }, nil -} - -func (s *Service) upsertBackupRepoSecret(ctx context.Context, input model.BackupRepoInput) error { - if len(input.Secrets) == 0 { - return nil - } - - key := types.NamespacedName{Name: input.Credential.Name, Namespace: input.Credential.Namespace} - var secret corev1.Secret - err := s.client.Get(ctx, key, &secret) - if apierrors.IsNotFound(err) { - secret = corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: input.Credential.Name, - Namespace: input.Credential.Namespace, - }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{}, - } - for k, v := range input.Secrets { - secret.Data[k] = []byte(v) - } - if err := s.client.Create(ctx, &secret); err != nil { - return fmt.Errorf("create BackupRepo credential %s/%s: %w", key.Namespace, key.Name, err) - } - return nil - } - if err != nil { - return fmt.Errorf("get BackupRepo credential %s/%s: %w", key.Namespace, key.Name, err) - } - if secret.Data == nil { - secret.Data = map[string][]byte{} - } - for k, v := range input.Secrets { - secret.Data[k] = []byte(v) - } - if err := s.client.Update(ctx, &secret); err != nil { - return fmt.Errorf("update BackupRepo credential %s/%s: %w", key.Namespace, key.Name, err) - } - return nil -} - -func (s *Service) ensureBackupRepoNotInUse(ctx context.Context, name string) error { - var clusterList kbappsv1.ClusterList - if err := s.client.List(ctx, &clusterList); err != nil { - return fmt.Errorf("list clusters for BackupRepo usage: %w", err) - } - for _, cluster := range clusterList.Items { - if cluster.Spec.Backup != nil && cluster.Spec.Backup.RepoName == name { - return fmt.Errorf("backup repo %s is in use by cluster %s/%s", name, cluster.Namespace, cluster.Name) - } - } - return nil -} - -func backupRepoToModel(item *datav1alpha1.BackupRepo) *model.BackupRepo { - return &model.BackupRepo{ - Name: item.Name, - Type: item.Spec.StorageProviderRef, - AccessMethod: item.Spec.AccessMethod, - Phase: item.Status.Phase, - GeneratedStorageClassName: item.Status.GeneratedStorageClassName, - BackupPVCName: item.Status.BackupPVCName, - Conditions: item.Status.Conditions, - } -} - -// getBackupsByIndex 使用索引查询 Backup,失败时回退到标签查询 -func getBackupsByIndex(ctx context.Context, c client.Client, clusterName, namespace string) ([]datav1alpha1.Backup, error) { - var backupList datav1alpha1.BackupList - - indexKey := fmt.Sprintf("%s/%s", namespace, clusterName) - if err := c.List(ctx, &backupList, client.MatchingFields{index.NamespaceInstanceField: indexKey}); err == nil { - return backupList.Items, nil - } - - selector := client.MatchingLabels{constant.AppInstanceLabelKey: clusterName} - if err := c.List(ctx, &backupList, selector, client.InNamespace(namespace)); err != nil { - return nil, fmt.Errorf("list backups for cluster %s in namespace %s: %w", clusterName, namespace, err) - } - - return backupList.Items, nil -} - -// DeleteBackups 批量删除指定备份 -// -// 根据 service_id 查找对应的 Cluster,然后删除请求中指定名称的备份 -// 返回成功删除的备份名称列表 -func (s *Service) DeleteBackups(ctx context.Context, rbd model.RBDService, backupNames []string) ([]string, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, rbd.ServiceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", rbd.ServiceID, err) - } - - backups, err := getBackupsByIndex(ctx, s.client, cluster.Name, cluster.Namespace) - if err != nil { - return nil, fmt.Errorf("get backups for cluster %s: %w", cluster.Name, err) - } - - backupMap := make(map[string]*datav1alpha1.Backup) - for i := range backups { - backupMap[backups[i].Name] = &backups[i] - } - - var deleted []string - - for _, name := range backupNames { - backup, exists := backupMap[name] - if !exists { - continue - } - - if canDelete, reason := s.canDeleteBackup(backup); !canDelete { - log.Info("备份无法删除", log.String("backup", name), log.String("reason", reason)) - continue - } - - if err := s.client.Delete(ctx, backup); err != nil { - if apierrors.IsNotFound(err) { - deleted = append(deleted, name) - continue - } - - log.Error("删除备份失败", log.String("backup", name), log.String("cluster", cluster.Name), log.Err(err)) - continue - } - - deleted = append(deleted, name) - } - - return deleted, nil -} - -// canDeleteBackup 检查备份是否可以安全删除 -func (s *Service) canDeleteBackup(backup *datav1alpha1.Backup) (bool, string) { - if backup.Status.Phase == datav1alpha1.BackupPhaseRunning { - return false, ReasonBackupRunning - } - - return true, "" -} - -// sortBackupsByTime 按时间倒序排列备份 -func sortBackupsByTime(backups []datav1alpha1.Backup) { - sort.Slice(backups, func(i, j int) bool { - a, b := backups[i], backups[j] - - timeA := a.CreationTimestamp.UTC() - if a.Status.StartTimestamp != nil { - timeA = a.Status.StartTimestamp.UTC() - } - - timeB := b.CreationTimestamp.UTC() - if b.Status.StartTimestamp != nil { - timeB = b.Status.StartTimestamp.UTC() - } - - return timeA.After(timeB) - }) -} diff --git a/plugins/kb-adapter-rbdplugin/service/backup/backup_test.go b/plugins/kb-adapter-rbdplugin/service/backup/backup_test.go deleted file mode 100644 index 47a0e5942..000000000 --- a/plugins/kb-adapter-rbdplugin/service/backup/backup_test.go +++ /dev/null @@ -1,850 +0,0 @@ -package backup - -import ( - "context" - "errors" - "testing" - "time" - - appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.backup-repo.list-all -func TestListAvailableBackupRepos(t *testing.T) { - tests := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - want []*model.BackupRepo - expectErr bool - errContains string - }{ - { - name: "multiple_repos_all_phases_returned", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewBackupRepoBuilder("repo1"). - WithStorageProvider("s3"). - WithAccessMethod(datav1alpha1.AccessMethodMount). - WithPhase(datav1alpha1.BackupRepoReady). - Build(), - testutil.NewBackupRepoBuilder("repo2"). - WithStorageProvider("s3"). - WithAccessMethod(datav1alpha1.AccessMethodTool). - WithPhase(datav1alpha1.BackupRepoFailed). - Build(), - testutil.NewBackupRepoBuilder("repo3"). - WithStorageProvider("minio"). - WithAccessMethod(datav1alpha1.AccessMethodTool). - WithPhase(datav1alpha1.BackupRepoPreChecking). - Build(), - }) - }, - want: []*model.BackupRepo{ - {Name: "repo1", Type: "s3", AccessMethod: datav1alpha1.AccessMethodMount, Phase: datav1alpha1.BackupRepoReady}, - {Name: "repo2", Type: "s3", AccessMethod: datav1alpha1.AccessMethodTool, Phase: datav1alpha1.BackupRepoFailed}, - {Name: "repo3", Type: "minio", AccessMethod: datav1alpha1.AccessMethodTool, Phase: datav1alpha1.BackupRepoPreChecking}, - }, - expectErr: false, - }, - { - name: "empty_list", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { return nil }, - want: []*model.BackupRepo{}, - expectErr: false, - }, - { - name: "list_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder().WithListError(errors.New("list failed")).Build() - }, - setup: func(c client.Client) error { return nil }, - want: nil, - expectErr: true, - errContains: "list failed", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := tt.clientSetup() - require.NoError(t, tt.setup(c)) - - svc := NewService(c) - got, err := svc.ListAvailableBackupRepos(context.Background()) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - assert.Nil(t, got) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.backup-repo.mutate -func TestCreateBackupRepo(t *testing.T) { - ctx := context.Background() - k8sClient := testutil.NewFakeClient() - svc := NewService(k8sClient) - - got, err := svc.CreateBackupRepo(ctx, model.BackupRepoInput{ - Name: "team-a-prod", - StorageProvider: "s3-compatible", - AccessMethod: datav1alpha1.AccessMethodTool, - PVReclaimPolicy: corev1.PersistentVolumeReclaimRetain, - VolumeCapacity: "100Gi", - Config: map[string]string{ - "bucket": "kubeblocks-backup", - "endpoint": "http://minio-service.rbd-system.svc.cluster.local:9000", - "region": "", - "forcePathStyle": "true", - }, - Credential: corev1.SecretReference{ - Name: "team-a-prod-secret", - Namespace: "rbd-plugins", - }, - Secrets: map[string]string{ - "accessKeyId": "ak", - "secretAccessKey": "sk", - }, - PathPrefix: "aaa", - }) - - require.NoError(t, err) - assert.Equal(t, "team-a-prod", got.Name) - assert.Equal(t, "s3-compatible", got.Type) - - var repo datav1alpha1.BackupRepo - require.NoError(t, k8sClient.Get(ctx, types.NamespacedName{Name: "team-a-prod"}, &repo)) - assert.Equal(t, "s3-compatible", repo.Spec.StorageProviderRef) - assert.Equal(t, datav1alpha1.AccessMethodTool, repo.Spec.AccessMethod) - assert.Equal(t, corev1.PersistentVolumeReclaimRetain, repo.Spec.PVReclaimPolicy) - assert.Equal(t, resource.MustParse("100Gi"), repo.Spec.VolumeCapacity) - assert.Equal(t, "true", repo.Spec.Config["forcePathStyle"]) - assert.Equal(t, "kubeblocks-backup", repo.Spec.Config["bucket"]) - assert.Equal(t, "aaa", repo.Spec.PathPrefix) - require.NotNil(t, repo.Spec.Credential) - assert.Equal(t, "team-a-prod-secret", repo.Spec.Credential.Name) - assert.Equal(t, "rbd-plugins", repo.Spec.Credential.Namespace) - - var secret corev1.Secret - require.NoError(t, k8sClient.Get(ctx, types.NamespacedName{Name: "team-a-prod-secret", Namespace: "rbd-plugins"}, &secret)) - assert.Equal(t, []byte("ak"), secret.Data["accessKeyId"]) - assert.Equal(t, []byte("sk"), secret.Data["secretAccessKey"]) -} - -// capability_id: rainbond.kb-adapter.backup-repo.mutate -func TestDeleteBackupRepoRejectsClusterInUse(t *testing.T) { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("mysql", testutil.TestNamespace). - WithBackup(&appsv1.ClusterBackup{RepoName: "team-a-prod"}). - Build() - repo := testutil.NewBackupRepoBuilder("team-a-prod"). - WithStorageProvider("s3-compatible"). - WithAccessMethod(datav1alpha1.AccessMethodTool). - Build() - k8sClient := testutil.NewFakeClient(cluster, repo) - svc := NewService(k8sClient) - - err := svc.DeleteBackupRepo(ctx, "team-a-prod") - - require.Error(t, err) - assert.Contains(t, err.Error(), "in use") - require.NoError(t, k8sClient.Get(ctx, types.NamespacedName{Name: "team-a-prod"}, &datav1alpha1.BackupRepo{})) -} - -// capability_id: rainbond.kb-adapter.cluster-backup.schedule-reconcile -func TestReScheduleBackup(t *testing.T) { - tests := []struct { - name string - request model.BackupScheduleInput - clientSetup func() client.Client - setup func(client.Client) error - expectErr bool - errContains string - validate func(t *testing.T, client client.Client) - }{ - { - name: "enable_backup_for_cluster_without_backup_config", - request: model.BackupScheduleInput{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: "backup-repo-1", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: "7d", - }, - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - validate: func(t *testing.T, client client.Client) { - cluster := getClusterByServiceID(t, client, testutil.TestServiceID) - require.NotNil(t, cluster.Spec.Backup) - backup := cluster.Spec.Backup - require.NotNil(t, backup.Enabled) - assert.True(t, *backup.Enabled) - assert.Equal(t, "backup-repo-1", backup.RepoName) - assert.Equal(t, "0 2 * * *", backup.CronExpression) - assert.Equal(t, "7d", string(backup.RetentionPeriod)) - assert.Equal(t, "xtrabackup", backup.Method) - }, - }, - { - name: "update_existing_backup_configuration", - request: model.BackupScheduleInput{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: "backup-repo-2", - Schedule: model.BackupSchedule{ - Frequency: model.Weekly, - Hour: 3, - Minute: 30, - DayOfWeek: 1, - }, - RetentionPeriod: "30d", - }, - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - enabled := true - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithBackup(&appsv1.ClusterBackup{ - RepoName: "backup-repo-1", - Enabled: &enabled, - CronExpression: "0 2 * * *", - RetentionPeriod: "7d", - }). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - validate: func(t *testing.T, client client.Client) { - cluster := getClusterByServiceID(t, client, testutil.TestServiceID) - require.NotNil(t, cluster.Spec.Backup) - backup := cluster.Spec.Backup - require.Equal(t, "backup-repo-2", backup.RepoName) - assert.Equal(t, "30 3 * * 1", backup.CronExpression) - assert.Equal(t, "30d", string(backup.RetentionPeriod)) - require.NotNil(t, backup.Enabled) - assert.True(t, *backup.Enabled) - assert.Equal(t, "xtrabackup", backup.Method) - }, - }, - { - name: "disable_backup_by_setting_empty_repo", - request: model.BackupScheduleInput{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: "", - }, - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - enabled := true - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithBackup(&appsv1.ClusterBackup{ - RepoName: "backup-repo-1", - Enabled: &enabled, - CronExpression: "0 2 * * *", - RetentionPeriod: "7d", - }). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - validate: func(t *testing.T, client client.Client) { - cluster := getClusterByServiceID(t, client, testutil.TestServiceID) - require.Nil(t, cluster.Spec.Backup) - }, - }, - { - name: "cluster_not_found_error", - request: model.BackupScheduleInput{ - RBDService: model.RBDService{ServiceID: "non-existent-service-id"}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { return nil }, - expectErr: true, - errContains: "get cluster by service_id: resource not found", - }, - { - name: "multiple_clusters_found_error", - request: model.BackupScheduleInput{ - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster1 := testutil.NewMySQLCluster("test-cluster-1", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - cluster2 := testutil.NewMySQLCluster("test-cluster-2", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster1, cluster2}) - }, - expectErr: true, - errContains: "get cluster by service_id: multiple resources found", - }, - { - name: "enable_backup_from_disabled_state", - request: model.BackupScheduleInput{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: "backup-repo-new", - Schedule: model.BackupSchedule{ - Frequency: model.Weekly, - Hour: 10, - Minute: 30, - DayOfWeek: 5, - }, - RetentionPeriod: "14d", - }, - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - enabled := false - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithBackup(&appsv1.ClusterBackup{ - RepoName: "backup-repo-old", - Enabled: &enabled, - CronExpression: "0 0 * * 0", - RetentionPeriod: "1d", - }). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - validate: func(t *testing.T, client client.Client) { - cluster := getClusterByServiceID(t, client, testutil.TestServiceID) - require.NotNil(t, cluster.Spec.Backup) - backup := cluster.Spec.Backup - require.NotNil(t, backup.Enabled) - assert.True(t, *backup.Enabled) - assert.Equal(t, "backup-repo-new", backup.RepoName) - assert.Equal(t, "30 10 * * 5", backup.CronExpression) - assert.Equal(t, "14d", string(backup.RetentionPeriod)) - assert.Equal(t, "xtrabackup", backup.Method) - }, - }, - { - name: "client_error_during_list_operation", - request: model.BackupScheduleInput{ - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder().WithListError(errors.New("list failed")).Build() - }, - setup: func(c client.Client) error { return nil }, - expectErr: true, - errContains: "list failed", - }, - { - name: "no_change_when_configuration_matches", - request: model.BackupScheduleInput{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: "backup-repo-1", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: "7d", - }, - RBDService: model.RBDService{ServiceID: testutil.TestServiceID}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - enabled := true - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithBackup(&appsv1.ClusterBackup{ - RepoName: "backup-repo-1", - Enabled: &enabled, - CronExpression: "0 2 * * *", - RetentionPeriod: "7d", - }). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - validate: func(t *testing.T, client client.Client) { - cluster := getClusterByServiceID(t, client, testutil.TestServiceID) - require.NotNil(t, cluster.Spec.Backup) - backup := cluster.Spec.Backup - require.NotNil(t, backup.Enabled) - assert.True(t, *backup.Enabled) - assert.Equal(t, "backup-repo-1", backup.RepoName) - assert.Equal(t, "0 2 * * *", backup.CronExpression) - assert.Equal(t, "7d", string(backup.RetentionPeriod)) - assert.Equal(t, "xtrabackup", backup.Method) - }, - }, - } - - for _, tc := range tests { - testCase := tc - t.Run(testCase.name, func(t *testing.T) { - c := testCase.clientSetup() - require.NoError(t, testCase.setup(c)) - - svc := NewService(c) - err := svc.ReScheduleBackup(context.Background(), testCase.request) - - if testCase.expectErr { - require.Error(t, err) - if testCase.errContains != "" { - assert.Contains(t, err.Error(), testCase.errContains) - } - } else { - require.NoError(t, err) - if testCase.validate != nil { - testCase.validate(t, c) - } - } - }) - } -} - -// getClusterByServiceID helper function -func getClusterByServiceID(t *testing.T, c client.Client, serviceID string) *appsv1.Cluster { - t.Helper() - - var clusters appsv1.ClusterList - require.NoError(t, c.List(context.Background(), &clusters, client.MatchingLabels{index.ServiceIDLabel: serviceID})) - require.Len(t, clusters.Items, 1) - cluster := clusters.Items[0] - return &cluster -} - -// capability_id: rainbond.kb-adapter.cluster-backup.list -func TestListBackups(t *testing.T) { - tests := []struct { - name string - req model.RBDService - clientSetup func() client.Client - setup func(client.Client) error - want []model.BackupItem - expectErr bool - }{ - { - name: "successful_backup_list", - req: model.RBDService{ServiceID: testutil.TestServiceID}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - - backup1 := testutil.NewBackupBuilder("backup-1", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseCompleted). - WithCreationTime(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)). - WithStartTime(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)). - Build() - - backup2 := testutil.NewBackupBuilder("backup-2", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseFailed). - WithCreationTime(time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)). - WithStartTime(time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC)). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backup1, backup2}) - }, - want: []model.BackupItem{ - { - Name: "backup-2", - Status: datav1alpha1.BackupPhaseFailed, - Time: time.Date(2024, 1, 2, 12, 0, 0, 0, time.UTC), - }, - { - Name: "backup-1", - Status: datav1alpha1.BackupPhaseCompleted, - Time: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), - }, - }, - expectErr: false, - }, - { - name: "no_cluster_found", - req: model.RBDService{ServiceID: "non-existent-service-id"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { return nil }, - want: nil, - expectErr: true, - }, - { - name: "multiple_clusters_found", - req: model.RBDService{ServiceID: testutil.TestServiceID}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster1 := testutil.NewMySQLCluster("cluster-1", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - cluster2 := testutil.NewMySQLCluster("cluster-2", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster1, cluster2}) - }, - want: nil, - expectErr: true, - }, - { - name: "no_backups_found", - req: model.RBDService{ServiceID: testutil.TestServiceID}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - want: nil, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := tt.clientSetup() - require.NoError(t, tt.setup(c)) - - svc := NewService(c) - got, err := svc.ListBackups(context.Background(), model.BackupListQuery{RBDService: tt.req}) - - if tt.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - if tt.want == nil { - assert.Nil(t, got.Items) - } else { - assert.Equal(t, tt.want, got.Items) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster-backup.delete -func TestDeleteBackups(t *testing.T) { - tests := []struct { - name string - serviceID string - backupNames []string - clientSetup func() client.Client - setup func(client.Client) error - want []string - expectErr bool - errContains string - }{ - { - name: "delete_single_backup_success", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-1"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - backup := testutil.NewBackupBuilder("backup-1", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseCompleted). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backup}) - }, - want: []string{"backup-1"}, - expectErr: false, - }, - { - name: "delete_multiple_backups_success", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-1", "backup-2"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - backup1 := testutil.NewBackupBuilder("backup-1", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseCompleted). - Build() - backup2 := testutil.NewBackupBuilder("backup-2", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseFailed). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backup1, backup2}) - }, - want: []string{"backup-1", "backup-2"}, - expectErr: false, - }, - { - name: "delete_non_existent_backup", - serviceID: testutil.TestServiceID, - backupNames: []string{"non-existent-backup"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - want: []string{}, - expectErr: false, - }, - { - name: "delete_running_backup_rejected", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-running"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - backup := testutil.NewBackupBuilder("backup-running", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseRunning). - WithStartTime(time.Now().Add(-10 * time.Minute)). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backup}) - }, - want: []string{}, - expectErr: false, - }, - { - name: "delete_backup_being_deleted", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-deleting"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - backup := testutil.NewBackupBuilder("backup-deleting", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseDeleting). - WithDeletionTimestamp(). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backup}) - }, - want: []string{"backup-deleting"}, - expectErr: false, - }, - { - name: "mixed_scenario_completed_running_nonexistent", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-completed", "backup-running", "non-existent"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - backupCompleted := testutil.NewBackupBuilder("backup-completed", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseCompleted). - Build() - backupRunning := testutil.NewBackupBuilder("backup-running", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseRunning). - WithStartTime(time.Now().Add(-10 * time.Minute)). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster, backupCompleted, backupRunning}) - }, - want: []string{"backup-completed"}, - expectErr: false, - }, - { - name: "cluster_not_found_error", - serviceID: "non-existent-service-id", - backupNames: []string{"backup-1"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { return nil }, - want: nil, - expectErr: true, - errContains: "get cluster by service_id", - }, - { - name: "multiple_clusters_error", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-1"}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster1 := testutil.NewMySQLCluster("cluster-1", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - cluster2 := testutil.NewMySQLCluster("cluster-2", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster1, cluster2}) - }, - want: nil, - expectErr: true, - errContains: "multiple resources found", - }, - { - name: "client_list_error", - serviceID: testutil.TestServiceID, - backupNames: []string{"backup-1"}, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder().WithListError(errors.New("list failed")).Build() - }, - setup: func(c client.Client) error { return nil }, - want: nil, - expectErr: true, - errContains: "list failed", - }, - { - name: "empty_backup_names_list", - serviceID: testutil.TestServiceID, - backupNames: []string{}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - setup: func(c client.Client) error { - ctx := context.Background() - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - want: []string{}, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := tt.clientSetup() - require.NoError(t, tt.setup(c)) - - svc := NewService(c) - rbd := model.RBDService{ServiceID: tt.serviceID} - - got, err := svc.DeleteBackups(context.Background(), rbd, tt.backupNames) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - assert.Nil(t, got) - } else { - require.NoError(t, err) - assert.ElementsMatch(t, tt.want, got) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster-backup.delete-guard -func TestCanDeleteBackup(t *testing.T) { - tests := []struct { - name string - backup *datav1alpha1.Backup - wantCanDelete bool - wantReason string - }{ - { - name: "can_delete_completed_backup", - backup: testutil.NewBackupBuilder("completed-backup", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseCompleted). - Build(), - wantCanDelete: true, - wantReason: "", - }, - { - name: "can_delete_failed_backup", - backup: testutil.NewBackupBuilder("failed-backup", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseFailed). - Build(), - wantCanDelete: true, - wantReason: "", - }, - { - name: "cannot_delete_running_backup", - backup: testutil.NewBackupBuilder("running-backup", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseRunning). - WithStartTime(time.Now().Add(-10 * time.Minute)). - Build(), - wantCanDelete: false, - wantReason: ReasonBackupRunning, - }, - { - name: "can_delete_deleting_backup", - backup: testutil.NewBackupBuilder("deleting-backup", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseDeleting). - WithDeletionTimestamp(). - Build(), - wantCanDelete: true, - wantReason: "", - }, - { - name: "can_delete_new_backup", - backup: testutil.NewBackupBuilder("new-backup", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithPhase(datav1alpha1.BackupPhaseNew). - Build(), - wantCanDelete: true, - wantReason: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := NewService(nil) - canDelete, reason := svc.canDeleteBackup(tt.backup) - - assert.Equal(t, tt.wantCanDelete, canDelete) - assert.Equal(t, tt.wantReason, reason) - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/builder/builder.go b/plugins/kb-adapter-rbdplugin/service/builder/builder.go deleted file mode 100644 index 74a6741a8..000000000 --- a/plugins/kb-adapter-rbdplugin/service/builder/builder.go +++ /dev/null @@ -1,103 +0,0 @@ -// Package builder 提供构建 adapter.ClusterBuilder 的 Builder 实现 -// -// ClusterBuilder 用于在 Rainbond 中 KubeBlocks Cluster 的创建 -package builder - -import ( - "crypto/md5" - "fmt" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" -) - -var _ adapter.ClusterBuilder = &Builder{} - -// Builder 实现 ClusterBuilder 接口,所有的 Builder 都应基于 Builder 实现 -type Builder struct{} - -// generateShortName 生成基于哈希的短名称,确保唯一性且长度可控 -// 格式:{originalName}-{hash4},其中 hash4 是 MD5 哈希的前4位十六进制字符 -func (b *Builder) generateShortName(originalName string) string { - timestamp := time.Now().UnixNano() - input := fmt.Sprintf("%s-%d", originalName, timestamp) - - hash := md5.Sum([]byte(input)) - - hashSuffix := fmt.Sprintf("%x", hash[:2]) - - return fmt.Sprintf("%s-%s", originalName, hashSuffix) -} - -// BuildCluster 用于构建最基础的 cluster, -// 其他 builder 只需要在此 Cluster 的基础上进行修改/补充 addon 特定的配置 -func (b *Builder) BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) { - resources, err := input.ParseResources() - if err != nil { - return nil, err - } - - // 生成短名称,避免同团队内重名 - clusterName := b.generateShortName(input.Name) - - cluster := &kbappsv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - Namespace: input.Namespace, - }, - Spec: kbappsv1.ClusterSpec{ - TerminationPolicy: input.TerminationPolicy, - ClusterDef: input.Type, - ComponentSpecs: []kbappsv1.ClusterComponentSpec{ - { - Name: input.Type, - ServiceVersion: input.Version, - Replicas: input.Replicas, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resources.CPU, - corev1.ResourceMemory: resources.Memory, - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resources.CPU, - corev1.ResourceMemory: resources.Memory, - }, - }, - VolumeClaimTemplates: []kbappsv1.ClusterComponentVolumeClaimTemplate{ - { - Name: "data", - Spec: kbappsv1.PersistentVolumeClaimSpec{ - StorageClassName: ptr.To(input.StorageClass), - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resources.Storage, - }, - }, - }, - }, - }, - }, - }, - }, - } - - if input.BackupRepo != "" { - cluster.Spec.Backup = &kbappsv1.ClusterBackup{ - RepoName: input.BackupRepo, - Enabled: ptr.To(true), - CronExpression: input.Schedule.Cron(), - RetentionPeriod: input.RetentionPeriod, - } - } - - return cluster, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/builder/mysql.go b/plugins/kb-adapter-rbdplugin/service/builder/mysql.go deleted file mode 100644 index 27d60c749..000000000 --- a/plugins/kb-adapter-rbdplugin/service/builder/mysql.go +++ /dev/null @@ -1,34 +0,0 @@ -package builder - -import ( - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" -) - -var _ adapter.ClusterBuilder = &MySQL{} - -// MySQL 实现 MySQL 的 Builder -type MySQL struct { - Builder -} - -func (b *MySQL) BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) { - cluster, err := b.Builder.BuildCluster(input) - if err != nil { - return nil, fmt.Errorf("build base cluster: %w", err) - } - - // Backup - if cluster.Spec.Backup != nil { - cluster.Spec.Backup.Method = "xtrabackup" - } - - log.Debug("Build mysql cluster", log.Any("cluster", cluster)) - - return cluster, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/builder/postgresql.go b/plugins/kb-adapter-rbdplugin/service/builder/postgresql.go deleted file mode 100644 index 4b4f277a3..000000000 --- a/plugins/kb-adapter-rbdplugin/service/builder/postgresql.go +++ /dev/null @@ -1,51 +0,0 @@ -package builder - -import ( - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" -) - -var _ adapter.ClusterBuilder = &PostgreSQL{} - -// PostgreSQL 实现 PostgreSQL 的 Builder -type PostgreSQL struct { - Builder -} - -func (b *PostgreSQL) BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) { - cluster, err := b.Builder.BuildCluster(input) - if err != nil { - return nil, fmt.Errorf("build base cluster: %w", err) - } - - // Postgresql 需要额外添加 lables - // - // PostgreSQL's CMPD specifies `KUBERNETES_SCOPE_LABEL=apps.kubeblocks.postgres.patroni/scope` through ENVs - // The KUBERNETES_SCOPE_LABEL is used to define the label key that Patroni will use to tag Kubernetes resources. - // This helps Patroni identify which resources belong to the specified scope (or cluster) used to define the label key - // that Patroni will use to tag Kubernetes resources. - // This helps Patroni identify which resources belong to the specified scope (or cluster). - // Note: DO NOT REMOVE THIS LABEL - // update the value w.r.t your cluster name - // the value must follow the format -postgresql - // which is pg-cluster-postgresql in this examples - // replace `pg-cluster` with your cluster name - if cluster.Spec.ComponentSpecs[0].Labels == nil { - cluster.Spec.ComponentSpecs[0].Labels = make(map[string]string) - } - cluster.Spec.ComponentSpecs[0].Labels["apps.kubeblocks.postgres.patroni/scope"] = fmt.Sprintf("%s-postgresql", cluster.Name) - - // Backup - if cluster.Spec.Backup != nil { - cluster.Spec.Backup.Method = "pg-basebackup" - } - - log.Debug("Build postgresql cluster", log.Any("cluster", cluster)) - - return cluster, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/builder/rabbitmq.go b/plugins/kb-adapter-rbdplugin/service/builder/rabbitmq.go deleted file mode 100644 index 88c411570..000000000 --- a/plugins/kb-adapter-rbdplugin/service/builder/rabbitmq.go +++ /dev/null @@ -1,30 +0,0 @@ -package builder - -import ( - "fmt" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" -) - -var _ adapter.ClusterBuilder = &RabbitMQ{} - -type RabbitMQ struct { - Builder -} - -func (r *RabbitMQ) BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) { - cluster, err := r.Builder.BuildCluster(input) - if err != nil { - return nil, fmt.Errorf("build base cluster: %w", err) - } - - // RabbitMQ 不支持备份 - cluster.Spec.Backup = nil - - log.Debug("Build rabbitmq cluster", log.Any("cluster", cluster)) - - return cluster, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/builder/redis.go b/plugins/kb-adapter-rbdplugin/service/builder/redis.go deleted file mode 100644 index ec57c3b44..000000000 --- a/plugins/kb-adapter-rbdplugin/service/builder/redis.go +++ /dev/null @@ -1,74 +0,0 @@ -package builder - -import ( - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/utils/ptr" -) - -var _ adapter.ClusterBuilder = &Redis{} - -// Redis 实现 replication Redis cluster -type Redis struct { - Builder -} - -func (r *Redis) BuildCluster(input model.ClusterInput) (*kbappsv1.Cluster, error) { - cluster, err := r.Builder.BuildCluster(input) - if err != nil { - return nil, err - } - - resource, err := input.ParseResources() - if err != nil { - return nil, err - } - - cluster.Spec.Topology = "replication" - - // Redis replication cluster 需要额外配置 sentinel,资源分配与 redis 一致 - sentinel := kbappsv1.ClusterComponentSpec{ - Name: "redis-sentinel", - Replicas: input.Replicas, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.CPU, - corev1.ResourceMemory: resource.Memory, - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.CPU, - corev1.ResourceMemory: resource.Memory, - }, - }, - VolumeClaimTemplates: []kbappsv1.ClusterComponentVolumeClaimTemplate{ - { - Name: "data", - Spec: kbappsv1.PersistentVolumeClaimSpec{ - StorageClassName: ptr.To(input.StorageClass), - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.Storage, - }, - }, - }, - }, - }, - } - - cluster.Spec.ComponentSpecs = append(cluster.Spec.ComponentSpecs, sentinel) - - // redis use datafile backup method - if cluster.Spec.Backup != nil { - cluster.Spec.Backup.Method = "datafile" - } - - log.Debug("Build redis replication cluster", log.Any("cluster", cluster)) - return cluster, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/cluster.go b/plugins/kb-adapter-rbdplugin/service/cluster/cluster.go deleted file mode 100644 index 09606b011..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/cluster.go +++ /dev/null @@ -1,285 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - "github.com/apecloud/kubeblocks/pkg/constant" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - MiB = 1024 * 1024 - GiB = 1024 * 1024 * 1024 -) - -// Service 提供针对 Cluster 相关操作 -type Service struct { - client client.Client -} - -func NewService(c client.Client) *Service { - return &Service{ - client: c, - } -} - -// formatToISO8601Time 将标准 time.Time 转为 ISO 8601(RFC3339,UTC)字符串 -func formatToISO8601Time(t time.Time) string { - return t.UTC().Format(time.RFC3339) -} - -// AssociateToKubeBlocksComponent 将 KubeBlocks 组件和 Cluster 通过 service_id 关联 -func (s *Service) associateToKubeBlocksComponent(ctx context.Context, cluster *kbappsv1.Cluster, serviceID string) error { - log.Debug("start associate cluster to rainbond component", - log.String("service_id", serviceID), - log.String("cluster", cluster.Name), - ) - - const labelServiceID = index.ServiceIDLabel - - err := wait.PollUntilContextCancel(ctx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) { - var latest kbappsv1.Cluster - if err := s.client.Get(ctx, client.ObjectKey{ - Name: cluster.Name, - Namespace: cluster.Namespace, - }, &latest); err != nil { - log.Debug("Cluster not found yet, waiting", - log.String("cluster", cluster.Name), - log.String("namespace", cluster.Namespace), - ) - return false, nil - } - - if latest.Labels != nil && latest.Labels[labelServiceID] == serviceID { - log.Debug("Cluster already has correct service_id label", - log.String("service_id", serviceID), - ) - return true, nil - } - - patchData := fmt.Sprintf(`{ - "metadata": { - "labels": { - "%s": "%s" - } - } - }`, labelServiceID, serviceID) - - if err := s.client.Patch(ctx, &kbappsv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: cluster.Name, - Namespace: cluster.Namespace, - }, - }, client.RawPatch(types.MergePatchType, []byte(patchData))); err != nil { - log.Debug("Patch operation failed, retrying", - log.String("cluster", cluster.Name), - log.Err(err), - ) - return false, nil - } - - log.Debug("Successfully added service_id label to cluster", - log.String("service_id", serviceID), - log.String("cluster", cluster.Name), - ) - return true, nil - }) - - if err != nil { - return fmt.Errorf("failed to associate cluster %s/%s with service_id label after retries: %w", cluster.Namespace, cluster.Name, err) - } - - log.Info("Associated KubeBlocks Cluster to Rainbond component", - log.String("service_id", serviceID), - log.String("cluster", cluster.Name), - ) - - return nil -} - -// getClusterPods 获取 Cluster 相关的 Pod 状态信息, -// 会将 Cluster 的多个组件的 Pod 状态信息合并返回 -func (s *Service) getClusterPods(ctx context.Context, cluster *kbappsv1.Cluster) ([]model.Status, error) { - if len(cluster.Spec.ComponentSpecs) == 0 { - return nil, fmt.Errorf("cluster %s/%s has no componentSpecs", cluster.Namespace, cluster.Name) - } - - var ( - namespace = cluster.Namespace - clusterName = cluster.Name - ) - - var ( - // podComponent 用于记录 Pod 所属的 Component 名称 - podComponent = make(map[string]string) - // podNames 用于记录 Pod 名称列表 - podNames = make([]string, 0) - ) - - for _, component := range cluster.Spec.ComponentSpecs { - componentName := component.Name - // 如果为空,则跳过,应该不会出现这种情况 - if componentName == "" { - log.Warn("Component name is empty, skip", - log.String("cluster", clusterName), - log.String("component", componentName), - ) - continue - } - - instanceSet, err := getInstanceSetByCluster(ctx, s.client, clusterName, namespace, componentName) - if err != nil { - if errors.Is(err, kbkit.ErrTargetNotFound) { - log.Info("InstanceSet not found, skip component", - log.String("cluster", clusterName), - log.String("component", componentName)) - continue - } - return nil, fmt.Errorf("get instanceset for component %s: %w", componentName, err) - } - - for _, instanceStatus := range instanceSet.Status.InstanceStatus { - if instanceStatus.PodName == "" { - continue - } - // 如果 Pod 名称已经存在,则跳过应该也不会出现这种情况 - if _, exists := podComponent[instanceStatus.PodName]; exists { - continue - } - - podComponent[instanceStatus.PodName] = componentName - podNames = append(podNames, instanceStatus.PodName) - } - } - - if len(podNames) == 0 { - return []model.Status{}, nil - } - - pods, err := getPodsByNames(ctx, s.client, podNames, namespace) - if err != nil { - return nil, fmt.Errorf("get pods by names: %w", err) - } - - result := make([]model.Status, 0, len(pods)) - for _, pod := range pods { - componentName := podComponent[pod.Name] - if componentName == "" { - componentName = pod.Labels["apps.kubeblocks.io/component-name"] - } - result = append(result, buildPodStatus(pod, componentName)) - } - - return result, nil -} - -// getPodsByNames 根据 Pod 名称列表查询 Pod -func getPodsByNames(ctx context.Context, c client.Client, podNames []string, namespace string) ([]corev1.Pod, error) { - var pods []corev1.Pod - - for _, podName := range podNames { - var pod corev1.Pod - if err := c.Get(ctx, client.ObjectKey{Name: podName, Namespace: namespace}, &pod); err != nil { - log.Warn("Failed to get pod", log.String("pod", podName), log.Err(err)) - continue - } - pods = append(pods, pod) - } - - return pods, nil -} - -// buildPodStatus 构建 Pod 状态信息 -func buildPodStatus(pod corev1.Pod, componentName string) model.Status { - ready := false - - for _, condition := range pod.Status.Conditions { - if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { - ready = true - break - } - } - - return model.Status{ - Name: pod.Name, - Component: componentName, - Status: pod.Status.Phase, - Ready: ready, - Containers: buildReplicaContainers(&pod), - } -} - -// buildReplicaContainers build []ReplicaContainer -func buildReplicaContainers(pod *corev1.Pod) []model.ReplicaContainer { - if len(pod.Spec.Containers) == 0 { - return nil - } - - containers := make([]model.ReplicaContainer, 0, len(pod.Spec.Containers)) - for _, container := range pod.Spec.Containers { - containers = append(containers, model.ReplicaContainer{ - Name: container.Name, - }) - } - - return containers -} - -// getInstanceSetByCluster 通过 cluster 和 component 获取 InstanceSet -func getInstanceSetByCluster( - ctx context.Context, - c client.Client, - clusterName, - namespace, - componentName string, -) (*workloadsv1.InstanceSet, error) { - var instanceSetList workloadsv1.InstanceSetList - - // 优先使用索引查询 - indexKey := fmt.Sprintf("%s/%s/%s", namespace, clusterName, componentName) - if err := c.List(ctx, &instanceSetList, client.MatchingFields{index.NamespaceClusterComponentField: indexKey}); err == nil { - switch len(instanceSetList.Items) { - case 0: - return nil, kbkit.ErrTargetNotFound - case 1: - return &instanceSetList.Items[0], nil - default: - return nil, kbkit.ErrMultipleFounded - } - } else { - log.Warn("Index query failed, falling back to label query", - log.String("indexKey", indexKey), log.Err(err)) - } - - // 回退到标签查询 - selector := client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterName, - "apps.kubeblocks.io/component-name": componentName, - } - if err := c.List(ctx, &instanceSetList, selector, client.InNamespace(namespace)); err != nil { - return nil, fmt.Errorf("list instanceset for cluster %s component %s: %w", clusterName, componentName, err) - } - - switch len(instanceSetList.Items) { - case 0: - return nil, kbkit.ErrTargetNotFound - case 1: - return &instanceSetList.Items[0], nil - default: - return nil, kbkit.ErrMultipleFounded - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go deleted file mode 100644 index 125f4d7e4..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go +++ /dev/null @@ -1,618 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.associate-service-id -func TestAssociateToKubeBlocksComponent(t *testing.T) { - tests := []struct { - name string - setup func() (client.Client, *kbappsv1.Cluster) - serviceID string - expectError bool - errorContains string - verify func(t *testing.T, client client.Client, clusterName, namespace string) - }{ - { - name: "successful_association_new_label", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewFakeClientWithIndexes(cluster) - return client, cluster - }, - serviceID: "test-service-123", - expectError: false, - verify: func(t *testing.T, c client.Client, clusterName, namespace string) { - cluster := &kbappsv1.Cluster{} - err := c.Get(context.Background(), types.NamespacedName{ - Name: clusterName, Namespace: namespace, - }, cluster) - require.NoError(t, err) - assert.Equal(t, "test-service-123", cluster.Labels[index.ServiceIDLabel]) - }, - }, - { - name: "label_already_exists_correct_value", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID("test-service-123"). // 已经有正确的标签 - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewFakeClientWithIndexes(cluster) - return client, cluster - }, - serviceID: "test-service-123", - expectError: false, - verify: func(t *testing.T, c client.Client, clusterName, namespace string) { - cluster := &kbappsv1.Cluster{} - err := c.Get(context.Background(), types.NamespacedName{ - Name: clusterName, Namespace: namespace, - }, cluster) - require.NoError(t, err) - assert.Equal(t, "test-service-123", cluster.Labels[index.ServiceIDLabel]) - }, - }, - { - name: "get_operation_fails", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewErrorClientBuilder(cluster). - WithGetError(errors.New("network error")). - Build() - - return client, cluster - }, - serviceID: "test-service-123", - expectError: true, - errorContains: "failed to associate cluster", - }, - { - name: "patch_operation_fails", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewErrorClientBuilder(cluster). - WithPatchError(errors.New("patch failed")). - Build() - - return client, cluster - }, - serviceID: "test-service-123", - expectError: true, - errorContains: "failed to associate cluster", - }, - { - name: "context_timeout", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewFakeClientWithIndexes() - return client, cluster - }, - serviceID: "test-service-123", - expectError: true, - errorContains: "failed to associate cluster", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, cluster := tt.setup() - service := NewService(client) - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - err := service.associateToKubeBlocksComponent(ctx, cluster, tt.serviceID) - - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - require.NoError(t, err) - if tt.verify != nil { - tt.verify(t, client, cluster.Name, cluster.Namespace) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.list-pods -func TestGetClusterPods(t *testing.T) { - tests := []struct { - name string - setup func() (client.Client, *kbappsv1.Cluster) - expectError bool - errorContains string - expectPods int - verifyPods func(t *testing.T, pods []model.Status) - }{ - { - name: "empty_component_specs", - setup: func() (client.Client, *kbappsv1.Cluster) { - // 创建没有 ComponentSpecs 的集群 - cluster := &kbappsv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "empty-cluster", - Namespace: testutil.TestNamespace, - }, - Spec: kbappsv1.ClusterSpec{ - ClusterDef: "mysql", - ComponentSpecs: []kbappsv1.ClusterComponentSpec{}, // 空的 - }, - } - - client := testutil.NewFakeClientWithIndexes(cluster) - return client, cluster - }, - expectError: true, - errorContains: "has no componentSpecs", - }, - { - name: "single_component_with_pods", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - // 创建 InstanceSet - instanceSet := testutil.NewInstanceSetBuilder("test-cluster-mysql", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithComponentName("mysql"). - WithInstanceStatus("test-cluster-mysql-0", "test-cluster-mysql-1"). - WithReplicas(2). - Build() - - // 创建 Pod - pod1 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-mysql-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": "mysql", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "mysql", Image: "mysql:8.0"}, - }, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - Conditions: []corev1.PodCondition{ - {Type: corev1.PodReady, Status: corev1.ConditionTrue}, - }, - }, - } - - pod2 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-mysql-1", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": "mysql", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "mysql", Image: "mysql:8.0"}, - }, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - Conditions: []corev1.PodCondition{ - {Type: corev1.PodReady, Status: corev1.ConditionFalse}, - }, - }, - } - - client := testutil.NewFakeClientWithIndexes(cluster, instanceSet, pod1, pod2) - return client, cluster - }, - expectError: false, - expectPods: 2, - verifyPods: func(t *testing.T, pods []model.Status) { - require.NotEmpty(t, pods) - assert.Equal(t, []model.ReplicaContainer{{Name: "mysql"}}, pods[0].Containers) - }, - }, - { - name: "multiple_components", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewClusterBuilder("multi-cluster", testutil.TestNamespace). - WithClusterDef("redis"). - WithComponent("redis", "redis-7.0"). - WithComponent("redis-sentinel", "redis-sentinel-7.0"). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - // Redis 组件的 InstanceSet 和 Pod - redisInstanceSet := testutil.NewInstanceSetBuilder("multi-cluster-redis", testutil.TestNamespace). - WithClusterInstance("multi-cluster"). - WithComponentName("redis"). - WithInstanceStatus("multi-cluster-redis-0"). - Build() - - redisPod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "multi-cluster-redis-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": "redis", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "redis", Image: "redis:7.0"}, - }, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - - // Sentinel 组件的 InstanceSet 和 Pod - sentinelInstanceSet := testutil.NewInstanceSetBuilder("multi-cluster-redis-sentinel", testutil.TestNamespace). - WithClusterInstance("multi-cluster"). - WithComponentName("redis-sentinel"). - WithInstanceStatus("multi-cluster-redis-sentinel-0"). - Build() - - sentinelPod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "multi-cluster-redis-sentinel-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": "redis-sentinel", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "sentinel", Image: "sentinel:7.0"}, - }, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - - client := testutil.NewFakeClientWithIndexes( - cluster, redisInstanceSet, sentinelInstanceSet, redisPod, sentinelPod) - return client, cluster - }, - expectError: false, - expectPods: 2, - verifyPods: func(t *testing.T, pods []model.Status) { - require.Len(t, pods, 2) - assert.Equal(t, []model.ReplicaContainer{{Name: "redis"}}, pods[0].Containers) - assert.Equal(t, []model.ReplicaContainer{{Name: "sentinel"}}, pods[1].Containers) - }, - }, - { - name: "instanceset_not_found", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - // 只创建集群,不创建 InstanceSet - client := testutil.NewFakeClientWithIndexes(cluster) - return client, cluster - }, - expectError: false, - expectPods: 0, // InstanceSet 不存在时返回空列表 - }, - { - name: "pod_not_found", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - // 创建 InstanceSet 但不创建 Pod - instanceSet := testutil.NewInstanceSetBuilder("test-cluster-mysql", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithComponentName("mysql"). - WithInstanceStatus("test-cluster-mysql-0", "test-cluster-mysql-1"). - Build() - - client := testutil.NewFakeClientWithIndexes(cluster, instanceSet) - return client, cluster - }, - expectError: false, - expectPods: 0, // Pod 不存在时返回空列表 - }, - { - name: "api_error_on_instanceset_query", - setup: func() (client.Client, *kbappsv1.Cluster) { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - client := testutil.NewErrorClientBuilder(cluster). - WithListError(errors.New("api server error")). - Build() - return client, cluster - }, - expectError: true, - errorContains: "get instanceset for component", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, cluster := tt.setup() - service := NewService(client) - - pods, err := service.getClusterPods(context.Background(), cluster) - - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - require.NoError(t, err) - assert.Len(t, pods, tt.expectPods) - - // 验证 Pod 状态结构 - for _, pod := range pods { - // 由于我们不能导入 model 包,这里只做基本的断言 - assert.NotNil(t, pod) - } - - if tt.verifyPods != nil { - tt.verifyPods(t, pods) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.list-pods -func TestGetInstanceSetByCluster(t *testing.T) { - tests := []struct { - name string - setup func() client.Client - clusterName string - namespace string - componentName string - expectError bool - errorType error // 预期的错误类型 - verify func(t *testing.T, instanceSet *workloadsv1.InstanceSet) - }{ - { - name: "index_query_success", - setup: func() client.Client { - instanceSet := testutil.NewInstanceSetBuilder("test-cluster-mysql", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithComponentName("mysql"). - Build() - return testutil.NewFakeClientWithIndexes(instanceSet) - }, - clusterName: "test-cluster", - namespace: testutil.TestNamespace, - componentName: "mysql", - expectError: false, - verify: func(t *testing.T, instanceSet *workloadsv1.InstanceSet) { - assert.Equal(t, "test-cluster-mysql", instanceSet.Name) - assert.Equal(t, "test-cluster", instanceSet.Labels["app.kubernetes.io/instance"]) - assert.Equal(t, "mysql", instanceSet.Labels["apps.kubeblocks.io/component-name"]) - }, - }, - { - name: "index_query_not_found", - setup: func() client.Client { - // 不创建任何 InstanceSet - return testutil.NewFakeClientWithIndexes() - }, - clusterName: "non-existent-cluster", - namespace: testutil.TestNamespace, - componentName: "mysql", - expectError: true, - errorType: kbkit.ErrTargetNotFound, - }, - { - name: "multiple_instancesets_found", - setup: func() client.Client { - // 创建两个相同标签的 InstanceSet(理论上不应该发生) - instanceSet1 := testutil.NewInstanceSetBuilder("test-cluster-mysql-1", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithComponentName("mysql"). - Build() - instanceSet2 := testutil.NewInstanceSetBuilder("test-cluster-mysql-2", testutil.TestNamespace). - WithClusterInstance("test-cluster"). - WithComponentName("mysql"). - Build() - - return testutil.NewFakeClientWithIndexes(instanceSet1, instanceSet2) - }, - clusterName: "test-cluster", - namespace: testutil.TestNamespace, - componentName: "mysql", - expectError: true, - errorType: kbkit.ErrMultipleFounded, - }, - { - name: "api_error_on_list", - setup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("api server error")). - Build() - }, - clusterName: "test-cluster", - namespace: testutil.TestNamespace, - componentName: "mysql", - expectError: true, - errorType: nil, // API 错误是包装后的错误,不检查特定类型 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.setup() - - instanceSet, err := getInstanceSetByCluster( - context.Background(), client, tt.clusterName, tt.namespace, tt.componentName) - - if tt.expectError { - require.Error(t, err) - // 如果指定了错误类型,验证错误类型 - if tt.errorType != nil { - assert.ErrorIs(t, err, tt.errorType) - } - } else { - require.NoError(t, err) - require.NotNil(t, instanceSet) - if tt.verify != nil { - tt.verify(t, instanceSet) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.list-pods -func TestGetPodsByNames(t *testing.T) { - tests := []struct { - name string - setup func() client.Client - podNames []string - namespace string - expectPods int - expectError bool - errorContains string - }{ - { - name: "all_pods_exist", - setup: func() client.Client { - pod1 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-1", - Namespace: testutil.TestNamespace, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - pod2 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-2", - Namespace: testutil.TestNamespace, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - return testutil.NewFakeClient(pod1, pod2) - }, - podNames: []string{"pod-1", "pod-2"}, - namespace: testutil.TestNamespace, - expectPods: 2, - expectError: false, - }, - { - name: "partial_pods_exist", - setup: func() client.Client { - pod1 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-1", - Namespace: testutil.TestNamespace, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - return testutil.NewFakeClient(pod1) - }, - podNames: []string{"pod-1", "pod-2", "pod-3"}, - namespace: testutil.TestNamespace, - expectPods: 1, // 只有 pod-1 存在 - expectError: false, - }, - { - name: "no_pods_exist", - setup: func() client.Client { - return testutil.NewFakeClient() - }, - podNames: []string{"pod-1", "pod-2"}, - namespace: testutil.TestNamespace, - expectPods: 0, - expectError: false, - }, - { - name: "empty_pod_names", - setup: func() client.Client { - return testutil.NewFakeClient() - }, - podNames: []string{}, - namespace: testutil.TestNamespace, - expectPods: 0, - expectError: false, - }, - { - name: "some_pods_not_found_continues", - setup: func() client.Client { - pod1 := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-1", - Namespace: testutil.TestNamespace, - }, - Status: corev1.PodStatus{Phase: corev1.PodRunning}, - } - // 只创建 pod-1,pod-2 不存在,测试容错能力 - return testutil.NewFakeClient(pod1) - }, - podNames: []string{"pod-1", "pod-2"}, - namespace: testutil.TestNamespace, - expectPods: 1, // pod-1 可以获取,pod-2 不存在但继续处理 - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.setup() - - pods, err := getPodsByNames(context.Background(), client, tt.podNames, tt.namespace) - - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - require.NoError(t, err) - assert.Len(t, pods, tt.expectPods) - - // 验证返回的 Pod 都是有效的 - for _, pod := range pods { - assert.NotEmpty(t, pod.Name) - assert.Equal(t, tt.namespace, pod.Namespace) - } - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/event.go b/plugins/kb-adapter-rbdplugin/service/cluster/event.go deleted file mode 100644 index d14f16728..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/event.go +++ /dev/null @@ -1,266 +0,0 @@ -package cluster - -import ( - "context" - "fmt" - "sort" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// GetClusterEvents 获取指定 KubeBlocks Cluster 的运维事件列表 -// -// 事件数据来源于与 Cluster 关联的 OpsRequest 资源,按创建时间降序排序 -func (s *Service) GetClusterEvents(ctx context.Context, serviceID string, pagination model.Pagination) (*model.PaginatedResult[model.EventItem], error) { - pagination.Validate() - - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, serviceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", serviceID, err) - } - - allOps, err := kbkit.GetAllOpsRequestsByCluster(ctx, s.client, cluster.Namespace, cluster.Name) - if err != nil { - return nil, fmt.Errorf("get all opsrequests for cluster %s: %w", cluster.Name, err) - } - - // 转换所有 OpsRequest 为 EventItem - events := make([]model.EventItem, 0, len(allOps)) - for _, ops := range allOps { - event := s.convertOpsRequestToEventItem(&ops) - // 只保留 block mechanica 支持的 OpsType - if event.OpsType == "" { - continue - } - log.Debug("convert opsrequest to eventItem", log.Any("eventItem", event)) - events = append(events, event) - } - - podEvents, err := s.getClusterPodEventItems(ctx, cluster) - if err != nil { - log.Warn("Failed to get cluster pod events", - log.String("cluster", cluster.Name), - log.String("namespace", cluster.Namespace), - log.Err(err)) - } else { - events = append(events, podEvents...) - } - - // 按创建时间降序 - sort.Slice(events, func(i, j int) bool { - return events[i].CreateTime > events[j].CreateTime - }) - - result := kbkit.Paginate(events, pagination.Page, pagination.PageSize) - - log.Debug("get paginated events", - log.String("cluster", cluster.Name), - log.Any("events", events), - log.Int("page", pagination.Page), - log.Int("pageSize", pagination.PageSize), - log.Any("result", result), - ) - - return &model.PaginatedResult[model.EventItem]{ - Items: result, - Total: len(events), - }, nil -} - -// getClusterPodEventItems 将数据库实例 Pod 的 Warning 事件纳入组件事件列表 -func (s *Service) getClusterPodEventItems(ctx context.Context, cluster *kbappsv1.Cluster) ([]model.EventItem, error) { - pods, err := s.getClusterPods(ctx, cluster) - if err != nil { - return nil, err - } - - eventItems := make([]model.EventItem, 0) - for _, pod := range pods { - events, err := getRawPodEventsByIndex(ctx, s.client, pod.Name, cluster.Namespace) - if err != nil { - return nil, err - } - for i := range events { - eventItem, ok := convertPodEventToEventItem(pod.Name, &events[i]) - if !ok { - continue - } - eventItems = append(eventItems, eventItem) - } - } - - return eventItems, nil -} - -func getRawPodEventsByIndex(ctx context.Context, c client.Client, podName, namespace string) ([]corev1.Event, error) { - var eventList corev1.EventList - - indexKey := fmt.Sprintf("%s/%s", namespace, podName) - if err := c.List(ctx, &eventList, client.MatchingFields{index.NamespacePodNameField: indexKey}); err != nil { - log.Warn("Index query for pod events failed", - log.String("indexKey", indexKey), - log.String("pod", podName), - log.String("namespace", namespace), - log.Err(err)) - return []corev1.Event{}, nil - } - - return eventList.Items, nil -} - -func convertPodEventToEventItem(podName string, event *corev1.Event) (model.EventItem, bool) { - if event.Type != corev1.EventTypeWarning { - return model.EventItem{}, false - } - - eventTime := podEventTimestamp(event) - return model.EventItem{ - OpsName: fmt.Sprintf("%s/%s", podName, event.Name), - OpsType: podEventType(event), - UserName: "system", - Status: "failure", - FinalStatus: "complete", - Message: event.Message, - Reason: event.Reason, - CreateTime: formatTimeWithOffset(eventTime), - EndTime: formatTimeWithOffset(eventTime), - }, true -} - -func podEventTimestamp(event *corev1.Event) time.Time { - if !event.LastTimestamp.IsZero() { - return event.LastTimestamp.Time - } - if !event.FirstTimestamp.IsZero() { - return event.FirstTimestamp.Time - } - if !event.EventTime.IsZero() { - return event.EventTime.Time - } - return event.CreationTimestamp.Time -} - -func podEventType(event *corev1.Event) string { - if event.Reason != "" { - return event.Reason - } - return event.Type -} - -// convertOpsRequestToEventItem 将 OpsRequest 转换为 EventItem -func (s *Service) convertOpsRequestToEventItem(opsRequest *opsv1alpha1.OpsRequest) model.EventItem { - var message, reason, status, finalStatus, endTime string - - if !opsRequest.Status.CompletionTimestamp.IsZero() { - endTime = formatTimeWithOffset(opsRequest.Status.CompletionTimestamp.Time) - } - - switch opsRequest.Status.Phase { - case opsv1alpha1.OpsSucceedPhase: - status = "success" - finalStatus = "complete" - message = "Operation completed successfully" - case opsv1alpha1.OpsFailedPhase: - status = "failure" - finalStatus = "complete" - // 优先从 condition 中获取详细失败信息 - if cond := findFailedCondition(opsRequest.Status.Conditions); cond != nil { - message = cond.Message - reason = cond.Reason - } else { - message = "Operation failed with unknown reason" - } - case opsv1alpha1.OpsCancelledPhase: - status = "failure" - finalStatus = "complete" - message = "Operation was cancelled" - case opsv1alpha1.OpsAbortedPhase: - status = "failure" - finalStatus = "complete" - message = "Operation was aborted" - case opsv1alpha1.OpsPendingPhase: - status = "pending" - finalStatus = "running" - message = "Operation is pending" - case opsv1alpha1.OpsCreatingPhase: - status = "running" - finalStatus = "running" - message = "Operation is being created" - case opsv1alpha1.OpsRunningPhase: - status = "running" - finalStatus = "running" - message = "Operation is running" - case opsv1alpha1.OpsCancellingPhase: - status = "cancelling" - finalStatus = "running" - message = "Operation is being cancelled" - default: - status = "unknown" - finalStatus = "running" - message = "Operation status unknown" - } - - return model.EventItem{ - OpsName: opsRequest.Name, - OpsType: toRainbondOptType(opsRequest.Spec.Type), - UserName: "system", - Status: status, - FinalStatus: finalStatus, - Message: message, - Reason: reason, - CreateTime: formatTimeWithOffset(opsRequest.CreationTimestamp.Time), - EndTime: endTime, - } -} - -// toRainbondOptType 将 OpsType 转换为 Rainbond 支持的 OpsType 的 string 值 -// -// 忽略会与 Rainbond event 重复的 OpsType,只保留 KubeBlocks 特有的事件类型 -func toRainbondOptType(opsType opsv1alpha1.OpsType) string { - switch opsType { - case opsv1alpha1.VerticalScalingType: - // Vertical Scaling - return "vertical-service" - case opsv1alpha1.HorizontalScalingType: - // Horizontal Scaling - return "horizontal-service" - case opsv1alpha1.VolumeExpansionType: - // Storage Expansion - return "update-service-volume" - case opsv1alpha1.BackupType: - return "backup-database" - case opsv1alpha1.ReconfiguringType: - return "reconfiguring-cluster" - case opsv1alpha1.RestoreType: - return "restore-database" - default: - return "" - } -} - -// findFailedCondition 查找失败状态的 Condition -func findFailedCondition(conditions []metav1.Condition) *metav1.Condition { - for _, cond := range conditions { - if cond.Status == metav1.ConditionFalse { - return &cond - } - } - return nil -} - -// formatTimeWithOffset 将时间格式化为带数字时区偏移的 RFC3339 格式 -// 形如: 2025-09-09T16:51:59+08:00 -func formatTimeWithOffset(t time.Time) string { - localTime := t.In(time.Local) - return localTime.Format(time.RFC3339) -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/event_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/event_test.go deleted file mode 100644 index fce9d3451..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/event_test.go +++ /dev/null @@ -1,1057 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.event-timeline -func TestFindFailedCondition(t *testing.T) { - tests := []struct { - name string - conditions []metav1.Condition - expected *metav1.Condition - }{ - { - name: "has failed condition - any type with false status", - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionTrue}, - {Type: "Available", Status: metav1.ConditionFalse, Reason: "TestFailure", Message: "Service unavailable"}, - }, - expected: &metav1.Condition{Type: "Available", Status: metav1.ConditionFalse, Reason: "TestFailure", Message: "Service unavailable"}, - }, - { - name: "multiple failed conditions - returns first one", - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionFalse, Reason: "FirstFailure", Message: "First failed condition"}, - {Type: "Available", Status: metav1.ConditionFalse, Reason: "SecondFailure", Message: "Second failed condition"}, - }, - expected: &metav1.Condition{Type: "Ready", Status: metav1.ConditionFalse, Reason: "FirstFailure", Message: "First failed condition"}, - }, - { - name: "no failed condition", - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionTrue}, - {Type: "Available", Status: metav1.ConditionTrue}, - }, - expected: nil, - }, - { - name: "empty conditions", - conditions: []metav1.Condition{}, - expected: nil, - }, - { - name: "nil conditions", - conditions: nil, - expected: nil, - }, - { - name: "only true conditions", - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionTrue}, - {Type: "Available", Status: metav1.ConditionTrue}, - {Type: "Progressing", Status: metav1.ConditionTrue}, - }, - expected: nil, - }, - { - name: "mixed with unknown status - only false matters", - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionTrue}, - {Type: "Unknown", Status: metav1.ConditionUnknown}, - {Type: "Failed", Status: metav1.ConditionFalse, Reason: "ActualFailure"}, - }, - expected: &metav1.Condition{Type: "Failed", Status: metav1.ConditionFalse, Reason: "ActualFailure"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := findFailedCondition(tt.conditions) - if tt.expected == nil { - assert.Nil(t, result) - } else { - require.NotNil(t, result) - assert.Equal(t, tt.expected.Type, result.Type) - assert.Equal(t, tt.expected.Status, result.Status) - assert.Equal(t, tt.expected.Reason, result.Reason) - } - }) - } -} - -// TestGetClusterEvents 测试获取集群事件列表功能 -// capability_id: rainbond.kb-adapter.cluster.event-timeline -func TestGetClusterEvents(t *testing.T) { - tests := []struct { - name string - serviceID string - pagination model.Pagination - setupCluster func(client.Client) error - setupOpsReqs func(client.Client) error - clientSetup func() client.Client - expectError bool - errorContains string - expectCount int - verifyResult func(*testing.T, *model.PaginatedResult[model.EventItem]) - }{ - { - name: "multiple_ops_mixed_states_with_sorting", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - now := time.Now() - - // 创建不同时间和状态的 OpsRequest,用于测试排序和过滤 - ops := []client.Object{ - // 支持的操作类型 - 垂直伸缩(成功)- 最早 - testutil.NewOpsRequestBuilder("vertical-success", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - - // 支持的操作类型 - 水平伸缩(运行中)- 最新 - testutil.NewOpsRequestBuilder("horizontal-running", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - - // 支持的操作类型 - 存储扩容(失败)- 中间 - testutil.NewOpsRequestBuilder("volume-failed", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - - // 不支持的操作类型 - 应该被过滤掉 - testutil.NewOpsRequestBuilder("restart-unsupported", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.RestartType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - - // 设置创建时间以确保排序测试 - for i, obj := range ops { - opsReq := obj.(*opsv1alpha1.OpsRequest) - opsReq.CreationTimestamp = metav1.NewTime(now.Add(time.Duration(i) * time.Minute)) - } - - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 3, // 应该只返回3个支持的操作类型 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - require.Len(t, result.Items, 3) - - // 验证排序:按创建时间降序(最新的在前) - // 根据时间设置:vertical-success(i=0, +0min), horizontal-running(i=1, +1min), volume-failed(i=2, +2min) - // 排序后:volume-failed(最新), horizontal-running(中间), vertical-success(最早) - assert.Equal(t, "volume-failed", result.Items[0].OpsName) - assert.Equal(t, "horizontal-running", result.Items[1].OpsName) - assert.Equal(t, "vertical-success", result.Items[2].OpsName) - - // 验证操作类型映射 - assert.Equal(t, "update-service-volume", result.Items[0].OpsType) - assert.Equal(t, "horizontal-service", result.Items[1].OpsType) - assert.Equal(t, "vertical-service", result.Items[2].OpsType) - - // 验证状态映射 - assert.Equal(t, "failure", result.Items[0].Status) - assert.Equal(t, "running", result.Items[1].Status) - assert.Equal(t, "success", result.Items[2].Status) - - }, - }, - { - name: "pagination_first_page", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 2, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - ops := []client.Object{ - testutil.NewOpsRequestBuilder("ops-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("ops-2", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("ops-3", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 3, // 总共3个事件 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - // 第一页应该返回2个项目 - assert.Len(t, result.Items, 2) - assert.Equal(t, 3, result.Total) // 总数应该是3 - }, - }, - { - name: "pagination_second_page", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 2, - PageSize: 2, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - ops := []client.Object{ - testutil.NewOpsRequestBuilder("ops-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("ops-2", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("ops-3", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 3, // 总共3个事件 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - // 第二页应该返回1个项目 - assert.Len(t, result.Items, 1) - assert.Equal(t, 3, result.Total) // 总数应该是3 - }, - }, - { - name: "service_id_not_found", - serviceID: "non-existent-service-id", - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - // 创建一个不同serviceID的集群 - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID("different-service-id"). - Build() - return c.Create(context.Background(), cluster) - }, - expectError: true, - errorContains: "get cluster by service_id", - }, - { - name: "empty_ops_history", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - // 不设置setupOpsReqs,表示没有任何OpsRequest - expectCount: 0, - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - assert.Empty(t, result.Items) - assert.Equal(t, 0, result.Total) - }, - }, - { - name: "business_contract_operation_filtering", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - // 测试业务契约:只保留 Block Mechanica 支持的操作类型 - ops := []client.Object{ - // 支持的操作类型 - testutil.NewOpsRequestBuilder("supported-backup", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.BackupType). // 支持 -> "backup-database" - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("supported-vertical", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). // 支持 -> "vertical-service" - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - // 不支持的操作类型 - 应该被过滤掉 - testutil.NewOpsRequestBuilder("unsupported-restart", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.RestartType). // 不支持 -> "" - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("unsupported-start", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.StartType). // 不支持 -> "" - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 2, // 只保留2个支持的操作类型 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - require.Len(t, result.Items, 2) - assert.Equal(t, 2, result.Total) - - // 业务契约验证:确保只包含支持的操作类型 - opsNames := make([]string, len(result.Items)) - opsTypes := make([]string, len(result.Items)) - for i, item := range result.Items { - opsNames[i] = item.OpsName - opsTypes[i] = item.OpsType - } - - // 应该包含支持的操作 - assert.Contains(t, opsNames, "supported-backup") - assert.Contains(t, opsNames, "supported-vertical") - - // 不应该包含不支持的操作 - assert.NotContains(t, opsNames, "unsupported-restart") - assert.NotContains(t, opsNames, "unsupported-start") - - // 验证操作类型映射正确 - assert.Contains(t, opsTypes, "backup-database") - assert.Contains(t, opsTypes, "vertical-service") - assert.NotContains(t, opsTypes, "") // 不应该有空的操作类型 - }, - }, - { - name: "all_ops_filtered_out", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - // 创建只有不支持类型的OpsRequest - ops := []client.Object{ - testutil.NewOpsRequestBuilder("restart-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.RestartType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("start-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.StartType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 0, // 所有操作都被过滤掉 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - assert.Empty(t, result.Items) - assert.Equal(t, 0, result.Total) - }, - }, - { - name: "pagination_out_of_range", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 10, // 超出范围的页码 - PageSize: 2, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - ops := []client.Object{ - testutil.NewOpsRequestBuilder("ops-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 1, // 总数应该是1 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - // 超出范围的页码应该返回空结果 - assert.Empty(t, result.Items) - assert.Equal(t, 1, result.Total) // 但总数应该正确 - }, - }, - { - name: "invalid_pagination_params", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 0, // 无效的页码 - PageSize: 0, // 无效的页大小 - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - ops := []client.Object{ - testutil.NewOpsRequestBuilder("ops-1", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 1, - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - // pagination.Validate()会处理无效的分页参数 - // 通常会设置默认值而不是返回空结果 - assert.NotNil(t, result.Items) - assert.Equal(t, 1, result.Total) - }, - }, - { - name: "get_cluster_by_service_id_fails", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("mock k8s list error")). - Build() - }, - expectError: true, - errorContains: "get cluster by service_id", - }, - { - name: "k8s_list_operation_fails", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - clientSetup: func() client.Client { - // FailingClientBuilder的ListError会影响所有List操作 - // 包括GetClusterByServiceID中的集群查询 - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("mock k8s list error")). - Build() - }, - expectError: true, - errorContains: "get cluster by service_id", - }, - { - name: "business_contract_time_sorting", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - baseTime := time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC) - - // 测试业务契约:事件必须按创建时间降序排序,无论什么操作类型 - ops := []client.Object{ - // 最早的操作 - testutil.NewOpsRequestBuilder("early-backup", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - // 中间的操作 - testutil.NewOpsRequestBuilder("middle-scaling", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - // 最新的操作 - testutil.NewOpsRequestBuilder("latest-volume", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - } - - // 明确设置创建时间以确保业务契约测试 - for i, obj := range ops { - opsReq := obj.(*opsv1alpha1.OpsRequest) - // 时间间隔确保排序稳定性 - opsReq.CreationTimestamp = metav1.NewTime(baseTime.Add(time.Duration(i) * time.Hour)) - } - - return testutil.CreateObjects(ctx, c, ops) - }, - expectCount: 3, - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - require.Len(t, result.Items, 3) - - // 业务契约验证:必须按创建时间降序排序 - // latest-volume (最新) -> middle-scaling (中间) -> early-backup (最早) - assert.Equal(t, "latest-volume", result.Items[0].OpsName, "Latest operation should be first") - assert.Equal(t, "middle-scaling", result.Items[1].OpsName, "Middle operation should be second") - assert.Equal(t, "early-backup", result.Items[2].OpsName, "Earliest operation should be last") - - // 验证创建时间确实是降序的 - for i := 0; i < len(result.Items)-1; i++ { - currentTime, _ := time.Parse(time.RFC3339, result.Items[i].CreateTime) - nextTime, _ := time.Parse(time.RFC3339, result.Items[i+1].CreateTime) - assert.True(t, currentTime.After(nextTime), "Events should be sorted by creation time in descending order") - } - }, - }, - { - name: "data_correctness_verification", - serviceID: testutil.TestServiceID, - pagination: model.Pagination{ - Page: 1, - PageSize: 10, - }, - setupCluster: func(c client.Client) error { - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - return c.Create(context.Background(), cluster) - }, - setupOpsReqs: func(c client.Client) error { - ctx := context.Background() - now := time.Now() - - // 创建一个失败的OpsRequest,包含Condition信息用于测试错误信息提取 - failedOps := testutil.NewOpsRequestBuilder("failed-with-condition", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build() - - // 设置失败条件 - failedOps.Status.Conditions = []metav1.Condition{ - { - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "Progressing", - Message: "Operation is progressing", - }, - { - Type: "Failed", - Status: metav1.ConditionFalse, - Reason: "BackupFailed", - Message: "Backup operation failed due to insufficient storage", - }, - } - - // 设置创建时间和完成时间 - failedOps.CreationTimestamp = metav1.NewTime(now.Add(-10 * time.Minute)) - failedOps.Status.CompletionTimestamp = metav1.NewTime(now.Add(-5 * time.Minute)) - - // 创建一个成功的OpsRequest用于验证其他操作类型 - successOps := testutil.NewOpsRequestBuilder("restore-success", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.RestoreType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build() - - successOps.CreationTimestamp = metav1.NewTime(now.Add(-15 * time.Minute)) - successOps.Status.CompletionTimestamp = metav1.NewTime(now.Add(-12 * time.Minute)) - - // 创建一个重配置操作 - reconfOps := testutil.NewOpsRequestBuilder("reconfig-pending", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.ReconfiguringType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build() - - reconfOps.CreationTimestamp = metav1.NewTime(now.Add(-8 * time.Minute)) - - return testutil.CreateObjects(ctx, c, []client.Object{failedOps, successOps, reconfOps}) - }, - expectCount: 3, // 3个支持的操作类型 - verifyResult: func(t *testing.T, result *model.PaginatedResult[model.EventItem]) { - require.Len(t, result.Items, 3) - - // 找到特定的事件进行验证 - var failedEvent, restoreEvent, reconfigEvent *model.EventItem - for i := range result.Items { - switch result.Items[i].OpsName { - case "failed-with-condition": - failedEvent = &result.Items[i] - case "restore-success": - restoreEvent = &result.Items[i] - case "reconfig-pending": - reconfigEvent = &result.Items[i] - } - } - - // 验证失败事件的错误信息提取 - require.NotNil(t, failedEvent) - assert.Equal(t, "backup-database", failedEvent.OpsType) - assert.Equal(t, "failure", failedEvent.Status) - assert.Equal(t, "complete", failedEvent.FinalStatus) - assert.Equal(t, "Backup operation failed due to insufficient storage", failedEvent.Message) - assert.Equal(t, "BackupFailed", failedEvent.Reason) - assert.NotEmpty(t, failedEvent.CreateTime) - assert.NotEmpty(t, failedEvent.EndTime) - - // 验证时间格式是否符合RFC3339 - _, err := time.Parse(time.RFC3339, failedEvent.CreateTime) - assert.NoError(t, err, "CreateTime should be in RFC3339 format") - _, err = time.Parse(time.RFC3339, failedEvent.EndTime) - assert.NoError(t, err, "EndTime should be in RFC3339 format") - - // 验证恢复操作 - require.NotNil(t, restoreEvent) - assert.Equal(t, "restore-database", restoreEvent.OpsType) - assert.Equal(t, "success", restoreEvent.Status) - assert.Equal(t, "complete", restoreEvent.FinalStatus) - assert.Equal(t, "Operation completed successfully", restoreEvent.Message) - assert.Empty(t, restoreEvent.Reason) // 成功的操作不应该有reason - assert.NotEmpty(t, restoreEvent.EndTime) - - // 验证重配置操作 - require.NotNil(t, reconfigEvent) - assert.Equal(t, "reconfiguring-cluster", reconfigEvent.OpsType) - assert.Equal(t, "pending", reconfigEvent.Status) - assert.Equal(t, "running", reconfigEvent.FinalStatus) - assert.Equal(t, "Operation is pending", reconfigEvent.Message) - assert.Empty(t, reconfigEvent.EndTime) // 进行中的操作没有结束时间 - - // 验证所有事件的公共字段 - for _, item := range result.Items { - assert.Equal(t, "system", item.UserName) - assert.NotEmpty(t, item.CreateTime) - - // 验证CreateTime格式 - _, err := time.Parse(time.RFC3339, item.CreateTime) - assert.NoError(t, err, "CreateTime should be in RFC3339 format for %s", item.OpsName) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var k8sClient client.Client - if tt.clientSetup != nil { - k8sClient = tt.clientSetup() - } else { - k8sClient = testutil.NewFakeClientWithIndexes() - } - - ctx := context.Background() - - // 设置集群数据 - if tt.setupCluster != nil { - require.NoError(t, tt.setupCluster(k8sClient)) - } - - // 设置OpsRequest数据 - if tt.setupOpsReqs != nil { - require.NoError(t, tt.setupOpsReqs(k8sClient)) - } - - // 创建服务并执行测试 - service := &Service{client: k8sClient} - result, err := service.GetClusterEvents(ctx, tt.serviceID, tt.pagination) - - // 验证错误情况 - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - return - } - - // 验证正常情况 - require.NoError(t, err) - require.NotNil(t, result) - - // 验证基本计数 - if tt.expectCount >= 0 { - assert.Equal(t, tt.expectCount, result.Total) - } - - // 执行自定义验证 - if tt.verifyResult != nil { - tt.verifyResult(t, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.event-timeline -func TestGetClusterEventsIncludesPodWarningEvents(t *testing.T) { - ctx := context.Background() - baseTime := time.Date(2026, 6, 11, 10, 0, 0, 0, time.UTC) - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - instanceSet := testutil.NewInstanceSetBuilder("test-cluster-mysql", testutil.TestNamespace). - WithClusterInstance(cluster.Name). - WithComponentName("mysql"). - WithInstanceStatus("test-cluster-mysql-0"). - Build() - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-mysql-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - index.InstanceLabel: cluster.Name, - "apps.kubeblocks.io/component-name": "mysql", - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Status: corev1.PodStatus{Phase: corev1.PodPending}, - } - podEvent := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-mysql-0.17d2", - Namespace: testutil.TestNamespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Name: pod.Name, - Namespace: pod.Namespace, - }, - Type: corev1.EventTypeWarning, - Reason: "FailedScheduling", - Message: "0/3 nodes are available: 3 Insufficient memory.", - FirstTimestamp: metav1.NewTime(baseTime), - LastTimestamp: metav1.NewTime(baseTime.Add(2 * time.Minute)), - } - normalEvent := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-mysql-0.17d1", - Namespace: testutil.TestNamespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Name: pod.Name, - Namespace: pod.Namespace, - }, - Type: corev1.EventTypeNormal, - Reason: "Pulled", - Message: "Successfully pulled image", - FirstTimestamp: metav1.NewTime(baseTime.Add(-time.Minute)), - } - unrelatedPod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "other-pod-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - index.InstanceLabel: "other-cluster", - }, - }, - } - unrelatedEvent := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "other-pod-0.17d3", - Namespace: testutil.TestNamespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Name: unrelatedPod.Name, - Namespace: unrelatedPod.Namespace, - }, - Type: corev1.EventTypeWarning, - Reason: "FailedScheduling", - Message: "unrelated", - FirstTimestamp: metav1.NewTime(baseTime.Add(time.Minute)), - } - - k8sClient := testutil.NewFakeClientWithIndexes( - cluster, - instanceSet, - pod, - podEvent, - normalEvent, - unrelatedPod, - unrelatedEvent, - ) - - service := &Service{client: k8sClient} - result, err := service.GetClusterEvents(ctx, testutil.TestServiceID, model.Pagination{Page: 1, PageSize: 10}) - require.NoError(t, err) - require.NotNil(t, result) - require.Len(t, result.Items, 1) - assert.Equal(t, 1, result.Total) - - event := result.Items[0] - assert.Equal(t, fmt.Sprintf("%s/%s", pod.Name, podEvent.Name), event.OpsName) - assert.Equal(t, "FailedScheduling", event.OpsType) - assert.Equal(t, "failure", event.Status) - assert.Equal(t, "complete", event.FinalStatus) - assert.Equal(t, podEvent.Message, event.Message) - assert.Equal(t, podEvent.Reason, event.Reason) - assert.Equal(t, "system", event.UserName) - assert.NotEmpty(t, event.CreateTime) - _, err = time.Parse(time.RFC3339, event.CreateTime) - assert.NoError(t, err) -} - -// TestConvertOpsRequestToEventItem 测试 OpsRequest 转换为 EventItem -// capability_id: rainbond.kb-adapter.cluster.event-timeline -func TestConvertOpsRequestToEventItem(t *testing.T) { - service := &Service{} - baseTime := time.Now() - - tests := []struct { - name string - phase opsv1alpha1.OpsPhase - expectedStatus string - expectedFinalStatus string - expectedMessage string - completionTime *metav1.Time - conditions []metav1.Condition - }{ - { - name: "OpsSucceedPhase", - phase: opsv1alpha1.OpsSucceedPhase, - expectedStatus: "success", - expectedFinalStatus: "complete", - expectedMessage: "Operation completed successfully", - completionTime: &metav1.Time{Time: baseTime}, - }, - { - name: "OpsFailedPhase with condition", - phase: opsv1alpha1.OpsFailedPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "Test failure message", - completionTime: &metav1.Time{Time: baseTime}, - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionFalse, Message: "Test failure message", Reason: "TestFailure"}, - }, - }, - { - name: "OpsFailedPhase without condition", - phase: opsv1alpha1.OpsFailedPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "Operation failed with unknown reason", - completionTime: &metav1.Time{Time: baseTime}, - }, - { - name: "OpsCancelledPhase", - phase: opsv1alpha1.OpsCancelledPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "Operation was cancelled", - completionTime: &metav1.Time{Time: baseTime}, - }, - { - name: "OpsAbortedPhase", - phase: opsv1alpha1.OpsAbortedPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "Operation was aborted", - completionTime: &metav1.Time{Time: baseTime}, - }, - { - name: "OpsPendingPhase", - phase: opsv1alpha1.OpsPendingPhase, - expectedStatus: "pending", - expectedFinalStatus: "running", - expectedMessage: "Operation is pending", - }, - { - name: "OpsCreatingPhase", - phase: opsv1alpha1.OpsCreatingPhase, - expectedStatus: "running", - expectedFinalStatus: "running", - expectedMessage: "Operation is being created", - }, - { - name: "OpsRunningPhase", - phase: opsv1alpha1.OpsRunningPhase, - expectedStatus: "running", - expectedFinalStatus: "running", - expectedMessage: "Operation is running", - }, - { - name: "OpsCancellingPhase", - phase: opsv1alpha1.OpsCancellingPhase, - expectedStatus: "cancelling", - expectedFinalStatus: "running", - expectedMessage: "Operation is being cancelled", - }, - { - name: "UnknownPhase", - phase: opsv1alpha1.OpsPhase("UnknownPhase"), - expectedStatus: "unknown", - expectedFinalStatus: "running", - expectedMessage: "Operation status unknown", - }, - { - name: "OpsFailedPhase with multiple conditions", - phase: opsv1alpha1.OpsFailedPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "First failure message", - completionTime: &metav1.Time{Time: baseTime}, - conditions: []metav1.Condition{ - {Type: "Ready", Status: metav1.ConditionTrue, Message: "All good"}, - {Type: "Available", Status: metav1.ConditionFalse, Message: "First failure message", Reason: "FirstFailure"}, - {Type: "Progressing", Status: metav1.ConditionFalse, Message: "Second failure message", Reason: "SecondFailure"}, - }, - }, - { - name: "OpsFailedPhase with empty message in condition", - phase: opsv1alpha1.OpsFailedPhase, - expectedStatus: "failure", - expectedFinalStatus: "complete", - expectedMessage: "", - completionTime: &metav1.Time{Time: baseTime}, - conditions: []metav1.Condition{ - {Type: "Failed", Status: metav1.ConditionFalse, Message: "", Reason: "EmptyMessage"}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opsRequest := &opsv1alpha1.OpsRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ops", - CreationTimestamp: metav1.Time{Time: baseTime}, - }, - Spec: opsv1alpha1.OpsRequestSpec{ - Type: opsv1alpha1.VerticalScalingType, - }, - Status: opsv1alpha1.OpsRequestStatus{ - Phase: tt.phase, - Conditions: tt.conditions, - }, - } - - if tt.completionTime != nil { - opsRequest.Status.CompletionTimestamp = *tt.completionTime - } - - result := service.convertOpsRequestToEventItem(opsRequest) - - assert.Equal(t, "test-ops", result.OpsName) - assert.Equal(t, "vertical-service", result.OpsType) // VerticalScalingType -> vertical-service - assert.Equal(t, "system", result.UserName) - assert.Equal(t, tt.expectedStatus, result.Status) - assert.Equal(t, tt.expectedFinalStatus, result.FinalStatus) - assert.Equal(t, tt.expectedMessage, result.Message) - - // 验证时间格式 - assert.NotEmpty(t, result.CreateTime) - if tt.completionTime != nil { - assert.NotEmpty(t, result.EndTime) - } else { - assert.Empty(t, result.EndTime) - } - - // 验证失败条件的 Reason - if tt.phase == opsv1alpha1.OpsFailedPhase && len(tt.conditions) > 0 { - for _, cond := range tt.conditions { - if cond.Status == metav1.ConditionFalse { - assert.Equal(t, cond.Reason, result.Reason) - break - } - } - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/info.go b/plugins/kb-adapter-rbdplugin/service/cluster/info.go deleted file mode 100644 index 72a7f3c1c..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/info.go +++ /dev/null @@ -1,248 +0,0 @@ -package cluster - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/mono" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// GetConnectInfo 获取指定 Cluster 的连接账户信息, -// 从 Kubernetes Secret 中获取数据库账户的用户名和密码 -// -// Secret 名称由对应数据库类型的 Coordinator 适配器生成 -func (s *Service) GetConnectInfo(ctx context.Context, rbd model.RBDService) ([]model.ConnectInfo, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, rbd.ServiceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", rbd.ServiceID, err) - } - - dbType := kbkit.ClusterType(cluster) - clusterAdapter, ok := registry.Cluster[dbType] - if !ok { - return nil, fmt.Errorf("unsupported cluster type: %s", dbType) - } - secretName := clusterAdapter.Coordinator.GetSecretName(cluster.Name) - - secret := &corev1.Secret{} - timeoutCtx, cancel := context.WithTimeout(ctx, 80*time.Second) - defer cancel() - err = wait.PollUntilContextCancel(timeoutCtx, 2*time.Second, true, func(ctx context.Context) (bool, error) { - err := s.client.Get(ctx, client.ObjectKey{ - Name: secretName, - Namespace: cluster.Namespace, - }, secret) - - if err != nil { - log.Debug("Failed to get secret or not exist, skipping", - log.String("cluster", cluster.Name), - log.Err(err), - ) - return false, nil - } - - if _, exists := secret.Data["username"]; !exists { - return false, nil - } - - if _, exists := secret.Data["password"]; !exists { - return false, nil - } - - log.Debug("Secret exists and contains necessary fields", - log.String("secret_name", secretName), - log.String("namespace", cluster.Namespace), - ) - - return true, nil - }) - - if err != nil { - return nil, fmt.Errorf("wait for secret %s/%s to be ready: %w", cluster.Namespace, secretName, err) - } - - user, err := mono.GetSecretField(secret, "username") - if err != nil { - return nil, fmt.Errorf("get username: %w", err) - } - - pwd, err := mono.GetSecretField(secret, "password") - if err != nil { - return nil, fmt.Errorf("get password: %w", err) - } - - connectInfo := model.ConnectInfo{ - User: user, - Password: pwd, - } - - log.Debug("get connect info", - log.Any("connect_info", connectInfo), - ) - - return []model.ConnectInfo{connectInfo}, nil -} - -// GetClusterDetail 通过 ServiceIdentifier.ID 获取 Cluster 的详细信息 -func (s *Service) GetClusterDetail(ctx context.Context, rbd model.RBDService) (*model.ClusterDetail, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, rbd.ServiceID) - if err != nil { - return nil, err - } - - podList, err := s.getClusterPods(ctx, cluster) - if err != nil { - return nil, fmt.Errorf("get cluster pods: %w", err) - } - - component := cluster.Spec.ComponentSpecs[0] - resourceInfo := s.extractResourceInfo(component) - basicInfo := s.buildBasicInfo(cluster, component, rbd, podList) - - detail := &model.ClusterDetail{ - Basic: basicInfo, - Resource: resourceInfo, - } - - if cluster.Spec.Backup == nil || (cluster.Spec.Backup.Enabled != nil && !*cluster.Spec.Backup.Enabled) { - log.Debug("get cluster detail", - log.Any("detail", detail), - ) - return detail, nil - } - - backupInfo, err := s.buildBackupInfo(cluster.Spec.Backup) - if err != nil { - return nil, fmt.Errorf("build backup info: %w", err) - } - detail.Backup = *backupInfo - - log.Debug("get cluster detail", - log.Any("detail", detail), - ) - - return detail, nil -} - -// extractResourceInfo 提取集群资源信息 -func (s *Service) extractResourceInfo(component kbappsv1.ClusterComponentSpec) model.ClusterResourceStatus { - cpuMilli := component.Resources.Limits.Cpu().MilliValue() - memoryBytes := component.Resources.Limits.Memory().Value() - memoryMiB := memoryBytes / MiB - - var storageGiB int64 - if len(component.VolumeClaimTemplates) > 0 { - storageQty := component.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] - storageGiB = storageQty.Value() / GiB - } - - return model.ClusterResourceStatus{ - CPUMilli: cpuMilli, - MemoryMi: memoryMiB, - StorageGi: storageGiB, - Replicas: component.Replicas, - } -} - -func (s *Service) buildBasicInfo( - cluster *kbappsv1.Cluster, - component kbappsv1.ClusterComponentSpec, - rbdService model.RBDService, - podList []model.Status, -) model.BasicInfo { - startTime := getStartTimeISO(cluster.Status.Conditions) - status := strings.ToLower(string(cluster.Status.Phase)) - - var storageClass string - if len(component.VolumeClaimTemplates) > 0 && - component.VolumeClaimTemplates[0].Spec.StorageClassName != nil { - storageClass = *component.VolumeClaimTemplates[0].Spec.StorageClassName - } - - return model.BasicInfo{ - ClusterInfo: model.ClusterInfo{ - Name: cluster.Name, - Namespace: cluster.Namespace, - Type: cluster.Spec.ClusterDef, - Version: component.ServiceVersion, - StorageClass: storageClass, - TerminationPolicy: cluster.Spec.TerminationPolicy, - }, - RBDService: model.RBDService{ServiceID: rbdService.ServiceID}, - Status: model.ClusterStatus{ - Status: status, - StatusCN: transStatus(status), - StartTime: startTime, - }, - Replicas: podList, - IsSupportBackup: kbkit.IsSupportBackup(kbkit.ClusterType(cluster)), - IsSupportParameter: kbkit.IsSupportParameter(kbkit.ClusterType(cluster)), - } -} - -func (s *Service) buildBackupInfo(backup *kbappsv1.ClusterBackup) (*model.BackupInfo, error) { - backupSchedule := &model.BackupSchedule{} - if err := backupSchedule.Uncron(backup.CronExpression); err != nil { - return nil, fmt.Errorf("parse backup schedule, cron: %s, err: %w", backup.CronExpression, err) - } - - return &model.BackupInfo{ - ClusterBackup: model.ClusterBackup{ - BackupRepo: backup.RepoName, - Schedule: *backupSchedule, - RetentionPeriod: backup.RetentionPeriod, - }, - }, nil -} - -// getStartTimeISO 提取 Cluster 最近一次达到 Ready 且 Status 为 True 的时间点(metav1.Time) -func getStartTimeISO(conditions []metav1.Condition) string { - var last *metav1.Time - for _, cond := range conditions { - if cond.Type == "Ready" && cond.Status == "True" { - if last == nil || cond.LastTransitionTime.After(last.Time) { - t := cond.LastTransitionTime - last = &t - } - } - } - if last == nil { - return "" - } - return formatToISO8601Time(last.Time) -} - -func transStatus(status string) string { - switch status { - case "creating": - return "创建中" - case "running": - return "运行中" - case "updating": - return "更新中" - case "stopping": - return "停止中" - case "stopped": - return "已停止" - case "deleting": - return "删除中" - case "failed": - return "失败" - case "abnormal": - return "异常" - default: - return status - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/info_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/info_test.go deleted file mode 100644 index 9cf76b6a6..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/info_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// capability_id: rainbond.kb-adapter.cluster.connection-info -func TestGetConnectInfo(t *testing.T) { - tests := []struct { - name string - setupClient func() client.Client - setupCtx func() context.Context // 添加 context 设置 - rbdService model.RBDService - expectInfo []model.ConnectInfo - expectError bool - errorMsg string - }{ - { - name: "successful_connection_info", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-test", testutil.TestNamespace). - WithServiceID("test-service"). - Build() - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mysql-test-mysql-account-root", - Namespace: testutil.TestNamespace, - }, - Data: map[string][]byte{ - "username": []byte("root"), - "password": []byte("secretpass"), - }, - } - - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, testutil.CreateObjects(ctx, c, []client.Object{cluster, secret})) - return c - }, - setupCtx: func() context.Context { return context.Background() }, - rbdService: model.RBDService{ServiceID: "test-service"}, - expectInfo: []model.ConnectInfo{ - { - User: "root", - Password: "secretpass", - }, - }, - expectError: false, - }, - { - name: "timeout_while_waiting_for_secret", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-test", testutil.TestNamespace). - WithServiceID("test-service"). - Build() - - // 不创建 Secret,模拟 Secret 永远不会出现的情况 - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, c.Create(ctx, cluster)) - return c - }, - setupCtx: func() context.Context { - // 创建一个已经超时的 context - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) - time.Sleep(2 * time.Millisecond) // 确保 context 已经超时 - defer cancel() - return ctx - }, - rbdService: model.RBDService{ServiceID: "test-service"}, - expectError: true, - errorMsg: "wait for secret", - }, - { - name: "cluster_not_found", - setupClient: func() client.Client { - return testutil.NewFakeClient() - }, - setupCtx: func() context.Context { return context.Background() }, - rbdService: model.RBDService{ServiceID: "nonexistent-service"}, - expectError: true, - errorMsg: "get cluster by service_id", - }, - { - name: "secret_empty_username", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-test", testutil.TestNamespace). - WithServiceID("test-service"). - Build() - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mysql-test-mysql-account-root", - Namespace: testutil.TestNamespace, - }, - Data: map[string][]byte{ - "username": []byte(""), // 空字符串 - "password": []byte("secretpass"), - }, - } - - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, testutil.CreateObjects(ctx, c, []client.Object{cluster, secret})) - return c - }, - setupCtx: func() context.Context { return context.Background() }, - rbdService: model.RBDService{ServiceID: "test-service"}, - expectError: true, - errorMsg: "get username", - }, - { - name: "secret_empty_password", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-test", testutil.TestNamespace). - WithServiceID("test-service"). - Build() - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mysql-test-mysql-account-root", - Namespace: testutil.TestNamespace, - }, - Data: map[string][]byte{ - "username": []byte("root"), - "password": []byte(""), // 空字符串 - }, - } - - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, testutil.CreateObjects(ctx, c, []client.Object{cluster, secret})) - return c - }, - setupCtx: func() context.Context { return context.Background() }, - rbdService: model.RBDService{ServiceID: "test-service"}, - expectError: true, - errorMsg: "get password", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := tt.setupClient() - service := NewService(c) - - ctx := context.Background() - if tt.setupCtx != nil { - ctx = tt.setupCtx() - } - - result, err := service.GetConnectInfo(ctx, tt.rbdService) - - if tt.expectError { - assert.Error(t, err) - if tt.errorMsg != "" { - assert.Contains(t, err.Error(), tt.errorMsg) - } - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectInfo, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.detail-summary -func TestGetClusterDetail(t *testing.T) { - tests := []struct { - name string - setupClient func() client.Client - rbdService model.RBDService - expectDetail *model.ClusterDetail - expectError bool - errorMsg string - }{ - { - name: "cluster_detail_without_backup", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-no-backup", testutil.TestNamespace). - WithServiceID("no-backup-service"). - WithComponentResources("mysql", - testutil.Resources("250m", "512Mi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("mysql", "data", "", "5Gi"). - WithPhase(kbappsv1.RunningClusterPhase). - Build() - - // 简化的 fake c,InstanceSet 不存在时会跳过组件 - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, testutil.CreateObjects(ctx, c, []client.Object{cluster})) - return c - }, - rbdService: model.RBDService{ServiceID: "no-backup-service"}, - expectDetail: &model.ClusterDetail{ - Basic: model.BasicInfo{ - ClusterInfo: model.ClusterInfo{ - Name: "mysql-no-backup", - Namespace: testutil.TestNamespace, - Type: "mysql", - Version: "", // NewMySQLCluster 没有设置 serviceVersion - StorageClass: "", - TerminationPolicy: "", // NewMySQLCluster 没有设置 terminationPolicy - }, - RBDService: model.RBDService{ServiceID: "no-backup-service"}, - Status: model.ClusterStatus{ - Status: "running", - StatusCN: "运行中", - StartTime: "", - }, - Replicas: []model.Status{}, // 空的副本列表,因为没有 InstanceSet - IsSupportBackup: true, - }, - Resource: model.ClusterResourceStatus{ - CPUMilli: 500, // 500m - MemoryMi: 1024, // 1Gi - StorageGi: 5, // 5Gi,通过 WithComponentVolumeClaimTemplate 设置 - Replicas: 1, - }, - Backup: model.BackupInfo{}, // 空的备份信息,因为集群没有备份配置 - }, - expectError: false, - }, - { - name: "cluster_not_found", - setupClient: func() client.Client { - return testutil.NewFakeClient() - }, - rbdService: model.RBDService{ServiceID: "nonexistent-service"}, - expectError: true, - }, - { - name: "get_cluster_pods_error", - setupClient: func() client.Client { - cluster := testutil.NewMySQLCluster("mysql-pods-error", testutil.TestNamespace). - WithServiceID("pods-error-service"). - Build() - - c := testutil.NewErrorClientBuilder(cluster). - WithListError(errors.New("pods list failed")). - Build() - return c - }, - rbdService: model.RBDService{ServiceID: "pods-error-service"}, - expectError: true, - errorMsg: "pods list failed", - }, - { - name: "invalid_backup_cron_expression", - setupClient: func() client.Client { - backup := &kbappsv1.ClusterBackup{ - CronExpression: "invalid-cron", - RetentionPeriod: "7d", - RepoName: "test-repo", - } - - cluster := testutil.NewMySQLCluster("mysql-invalid-cron", testutil.TestNamespace). - WithServiceID("invalid-cron-service"). - WithBackup(backup). - Build() - - c := testutil.NewFakeClient() - ctx := context.Background() - require.NoError(t, testutil.CreateObjects(ctx, c, []client.Object{cluster})) - return c - }, - rbdService: model.RBDService{ServiceID: "invalid-cron-service"}, - expectError: true, - errorMsg: "build backup info", // 应该在解析 cron 表达式时失败 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := tt.setupClient() - service := NewService(c) - ctx := context.Background() - - result, err := service.GetClusterDetail(ctx, tt.rbdService) - - if tt.expectError { - assert.Error(t, err) - if tt.errorMsg != "" && err != nil { - assert.Contains(t, err.Error(), tt.errorMsg) - } - } else { - assert.NoError(t, err) - if tt.expectDetail != nil && result != nil { - assert.Equal(t, tt.expectDetail.Basic.ClusterInfo, result.Basic.ClusterInfo) - assert.Equal(t, tt.expectDetail.Basic.RBDService, result.Basic.RBDService) - assert.Equal(t, tt.expectDetail.Basic.Status, result.Basic.Status) - assert.Equal(t, tt.expectDetail.Basic.IsSupportBackup, result.Basic.IsSupportBackup) - assert.Equal(t, tt.expectDetail.Resource, result.Resource) - if tt.expectDetail.Backup.BackupRepo != "" { - assert.Equal(t, tt.expectDetail.Backup, result.Backup) - } - } - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go b/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go deleted file mode 100644 index 744c11398..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go +++ /dev/null @@ -1,485 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/mono" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "golang.org/x/sync/errgroup" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// CreateCluster 依据 req 创建 KubeBlocks Cluster -// -// 通过将 service_id 添加至 Cluster 的 labels 中以关联 KubeBlocks Component 与 Cluster, -// 同时,Rainbond 也通过这层关系来判断 Rainbond 组件是否为 KubeBlocks Component -// -// 返回成功创建的 KubeBlocks Cluster 实例 -func (s *Service) CreateCluster(ctx context.Context, input model.ClusterInput) (*kbappsv1.Cluster, error) { - if input.Name == "" { - return nil, fmt.Errorf("name is required") - } - - clusterAdapter, ok := registry.Cluster[input.Type] - if !ok { - return nil, fmt.Errorf("unsupported cluster type: %s", input.Type) - } - - cluster, err := clusterAdapter.Builder.BuildCluster(input) - if err != nil { - return nil, fmt.Errorf("build %s cluster: %w", input.Type, err) - } - - var ( - // 为启用了 custom secret 的 Addon 设置 systemAccount - systemAccount *string - // custom secret name,不为空说明创建了 custom secret,需要回滚 - customSecretName string - ) - - systemAccount = clusterAdapter.Coordinator.SystemAccount() - if systemAccount != nil { - s.configureSystemAccount(cluster, clusterAdapter.Coordinator, *systemAccount) - customSecretName = clusterAdapter.Coordinator.GetSecretName(cluster.Name) - - if err := s.createSystemAccountSecret( - ctx, - customSecretName, - cluster.Namespace, - *systemAccount, - input.RBDService.ServiceID, - ); err != nil { - log.Debug("failed to create system account secret", - log.String("secret_name", customSecretName), - log.Err(err)) - return nil, fmt.Errorf("create system account secret: %w", err) - } - log.Debug("created system account secret", log.String("secret", customSecretName)) - } - - if err := s.client.Create(ctx, cluster); err != nil { - // rollback - if customSecretName != "" { - s.deleteSecretByName(ctx, customSecretName, cluster.Namespace) - } - return nil, fmt.Errorf("create cluster: %w", err) - } - - log.Debug("created cluster", log.String("cluster", cluster.Name)) - - input.Name = cluster.Name - if err := s.associateToKubeBlocksComponent(ctx, cluster, input.RBDService.ServiceID); err != nil { - // rollbacl - if delErr := s.deleteCluster(ctx, cluster, true); delErr != nil { - log.Error("failed to cleanup cluster after association failure", - log.String("cluster", cluster.Name), - log.Err(delErr)) - } - return nil, fmt.Errorf("associate to rainbond component: %w", err) - } - - log.Info("Successfully created cluster", - log.String("cluster", cluster.Name), - log.String("namespace", cluster.Namespace), - log.String("service_id", input.RBDService.ServiceID)) - - return cluster, nil -} - -// configureSystemAccount 为 设置 systemAccount -func (s *Service) configureSystemAccount( - cluster *kbappsv1.Cluster, - coordinator adapter.Coordinator, - systemAccountName string) { - secretName := coordinator.GetSecretName(cluster.Name) - - for i := range cluster.Spec.ComponentSpecs { - if cluster.Spec.ComponentSpecs[i].Name != kbkit.ClusterType(cluster) { - continue - } - - cluster.Spec.ComponentSpecs[i].SystemAccounts = []kbappsv1.ComponentSystemAccount{ - { - Name: systemAccountName, - SecretRef: &kbappsv1.ProvisionSecretRef{ - Name: secretName, - Namespace: cluster.Namespace, - }, - }, - } - - } - -} - -// createSystemAccountSecret 创建 cluster 使用的 custom secret -func (s *Service) createSystemAccountSecret( - ctx context.Context, - secretName string, - namespace string, - accountName string, - serviceID string, -) error { - password := mono.GeneratePWD(16) - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: namespace, - Labels: map[string]string{ - index.ServiceIDLabel: serviceID, - }, - }, - Immutable: ptr.To(true), - Data: map[string][]byte{ - "username": []byte(accountName), - "password": []byte(password), - }, - } - - return s.client.Create(ctx, secret) -} - -// DeleteClusters 删除 KubeBlocks 数据库集群 -// -// 批量删除指定 serviceIDs 对应的 Cluster,忽略找不到的 service_id -func (s *Service) DeleteClusters(ctx context.Context, serviceIDs []string) error { - for _, serviceID := range serviceIDs { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, serviceID) - if err != nil { - if errors.Is(err, kbkit.ErrTargetNotFound) { - continue - } - return fmt.Errorf("get cluster by service_id %s: %w", serviceID, err) - } - - if err := s.deleteCluster(ctx, cluster, false); err != nil { - return fmt.Errorf("delete cluster for service_id %s: %w", serviceID, err) - } - } - return nil -} - -// CancelClusterCreate 取消集群创建 -// -// 在删除前将 TerminationPolicy 调整为 WipeOut,确保 PVC/PV 等存储资源一并清理,避免脏数据残留 -func (s *Service) CancelClusterCreate(ctx context.Context, rbd model.RBDService) error { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, rbd.ServiceID) - if err != nil { - return fmt.Errorf("get cluster by service_id %s: %w", rbd.ServiceID, err) - } - return s.deleteCluster(ctx, cluster, true) -} - -// deleteCluster 内部删除方法,提供是否将 TerminationPolicy 设置为 WipeOut 的选项 -func (s *Service) deleteCluster(ctx context.Context, cluster *kbappsv1.Cluster, isCancel bool) error { - log.Info("Found cluster for deletion", - log.String("cluster_name", cluster.Name), - log.String("namespace", cluster.Namespace), - log.String("current_termination_policy", string(cluster.Spec.TerminationPolicy)), - log.Bool("wipe_out", isCancel)) - - // 清理 Cluster 的 OpsRequest - if err := s.cleanupClusterOpsRequests(ctx, cluster); err != nil { - log.Warn("Failed to cleanup OpsRequests, proceeding with cluster deletion", - log.String("cluster_name", cluster.Name), - log.Err(err)) - } - - if isCancel && cluster.Spec.TerminationPolicy != kbappsv1.WipeOut { - log.Info("Updating TerminationPolicy to WipeOut before deletion", - log.String("cluster_name", cluster.Name), - log.String("namespace", cluster.Namespace)) - - patch := client.MergeFrom(cluster.DeepCopy()) - cluster.Spec.TerminationPolicy = kbappsv1.WipeOut - - if err := s.client.Patch(ctx, cluster, patch); err != nil { - return fmt.Errorf("patch cluster %s/%s terminationPolicy to WipeOut: %w", - cluster.Namespace, cluster.Name, err) - } - - log.Info("Successfully updated TerminationPolicy to WipeOut", - log.String("cluster_name", cluster.Name), - log.String("namespace", cluster.Namespace)) - } - - policy := metav1.DeletePropagationForeground - deleteOptions := &client.DeleteOptions{ - PropagationPolicy: &policy, - } - - if err := s.client.Delete(ctx, cluster, deleteOptions); err != nil { - return fmt.Errorf("delete cluster %s/%s: %w", cluster.Namespace, cluster.Name, err) - } - - log.Info("Successfully initiated cluster deletion", - log.String("cluster_name", cluster.Name), - log.String("namespace", cluster.Namespace), - log.Bool("wipe_out", isCancel)) - - if err := s.deleteSecretsByCluster(ctx, cluster); err != nil { - log.Warn("Failed to cleanup secrets, but cluster deletion succeeded", - log.String("cluster", cluster.Name), - log.Err(err)) - } - - return nil -} - -// ManageClustersLifecycle 通过创建 OpsRequest 批量管理多个 Cluster 的生命周期 -func (s *Service) ManageClustersLifecycle(ctx context.Context, operation opsv1alpha1.OpsType, serviceIDs []string) *model.BatchOperationResult { - manageResult := model.NewBatchOperationResult() - for _, serviceID := range serviceIDs { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, serviceID) - if errors.Is(err, kbkit.ErrTargetNotFound) { - continue - } - if err != nil { - manageResult.AddFailed(serviceID, err) - continue - } - - if err = kbkit.CreateLifecycleOpsRequest(ctx, s.client, cluster, operation); err == nil { - manageResult.AddSucceeded(serviceID) - } else { - manageResult.AddFailed(serviceID, err) - } - } - return manageResult -} - -// cleanupClusterOpsRequests 清理指定 Cluster 的所有 OpsRequest -func (s *Service) cleanupClusterOpsRequests(ctx context.Context, cluster *kbappsv1.Cluster) error { - // 获取并清理所有非终态 OpsRequest,使其进入终态 - blockingOps, err := kbkit.GetAllNonFinalOpsRequests(ctx, s.client, cluster.Namespace, cluster.Name) - if err != nil { - return fmt.Errorf("get existing opsrequests: %w", err) - } - - if len(blockingOps) > 0 { - log.Debug("Found blocking OpsRequests, initiating cleanup", - log.String("cluster", cluster.Name), - log.Int("blocking_count", len(blockingOps))) - - if err := kbkit.CleanupBlockingOps(ctx, s.client, blockingOps); err != nil { - return fmt.Errorf("cleanup blocking ops: %w", err) - } - } - - // 获取并删除所有 OpsRequest - allOps, err := kbkit.GetAllOpsRequestsByCluster(ctx, s.client, cluster.Namespace, cluster.Name) - if err != nil { - return fmt.Errorf("get all opsrequests: %w", err) - } - - if len(allOps) == 0 { - log.Debug("No OpsRequests found for cluster", - log.String("cluster", cluster.Name)) - return nil - } - - log.Info("Deleting all OpsRequests for complete cleanup", - log.String("cluster", cluster.Name), - log.Int("total_count", len(allOps))) - - // 并发删除所有 OpsRequest,避免孤儿资源 - if err := s.deleteAllOpsRequestsConcurrently(ctx, allOps); err != nil { - return fmt.Errorf("delete all ops: %w", err) - } - - log.Info("Successfully cleaned up all OpsRequests", - log.String("cluster", cluster.Name), - log.Int("deleted_count", len(allOps))) - - return nil -} - -// deleteAllOpsRequestsConcurrently 并发删除所有 OpsRequest -func (s *Service) deleteAllOpsRequestsConcurrently(ctx context.Context, allOps []opsv1alpha1.OpsRequest) error { - if len(allOps) == 0 { - return nil - } - - group, gctx := errgroup.WithContext(ctx) - for i := range allOps { - op := &allOps[i] - group.Go(func() error { - if err := s.client.Delete(gctx, op); err != nil { - if apierrors.IsNotFound(err) { - return nil - } - return fmt.Errorf("failed to delete opsrequest %s: %w", op.Name, err) - } - return nil - }) - } - - return group.Wait() -} - -// deleteSecretsByCluster 删除 Cluster 引用的 SystemAccount secrets -// -// 只有当 secret 仅被当前 cluster 引用时才会删除,避免误删 restored cluster 共享的 secret -func (s *Service) deleteSecretsByCluster(ctx context.Context, cluster *kbappsv1.Cluster) error { - secretNames := extractSecretRefs(cluster) - if len(secretNames) == 0 { - log.Debug("no systemAccount secrets setted, skip", - log.String("cluster", cluster.Name), - ) - return nil - } - - var clusterList kbappsv1.ClusterList - if err := s.client.List(ctx, &clusterList, client.InNamespace(cluster.Namespace)); err != nil { - return fmt.Errorf("list clusters in namespace %s: %w", cluster.Namespace, err) - } - - var deletionErrors []error - - for _, secretName := range secretNames { - refCount := countSecretReferences(clusterList.Items, secretName) - - if refCount > 1 { - log.Debug("skipping secret deletion, still in use by other clusters", - log.String("secret", secretName), - log.Int("reference_count", refCount)) - continue - } - - if err := s.deleteSecret(ctx, secretName, cluster.Namespace); err != nil { - log.Error("failed to delete secret", - log.String("secret", secretName), - log.Err(err)) - deletionErrors = append(deletionErrors, - fmt.Errorf("delete secret %s: %w", secretName, err)) - continue - } - - log.Debug("deleted systemAccount secret", - log.String("cluster", cluster.Name), - log.String("secret", secretName), - log.Int("final_reference_count(should be 0)", refCount)) - } - - if len(deletionErrors) > 0 { - return fmt.Errorf("failed to delete %d secret(s)", len(deletionErrors)) - } - - return nil -} - -// deleteSecretByName 按名字删除 Secret -// 用于 Cluster 创建失败的场景 -// 不返回错误,只记录日志 -func (s *Service) deleteSecretByName(ctx context.Context, secretName, namespace string) { - log.Warn("cleaning up secret after operation failure", - log.String("secret", secretName), - log.String("namespace", namespace)) - - if err := s.deleteSecret(ctx, secretName, namespace); err != nil { - log.Error("failed to cleanup secret", - log.String("secret", secretName), - log.Err(err)) - return - } - - log.Info("successfully cleaned up secret", log.String("secret", secretName)) -} - -// deleteSecret 删除指定的 Secret,IsNotFound 不视为错误 -func (s *Service) deleteSecret(ctx context.Context, name, namespace string) error { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - - if err := s.client.Delete(ctx, secret); err != nil { - if apierrors.IsNotFound(err) { - log.Debug("secret already deleted or not found", - log.String("secret", name), - log.String("namespace", namespace)) - return nil - } - return fmt.Errorf("delete secret %s/%s: %w", namespace, name, err) - } - - log.Debug("successfully deleted secret", - log.String("secret", name), - log.String("namespace", namespace)) - return nil -} - -// extractSecretRefs 从 cluster 中提取所有 SystemAccount 的 secretRef 名称 -func extractSecretRefs(cluster *kbappsv1.Cluster) []string { - if cluster == nil { - return nil - } - - // 用于储存已出现的 secret 名称,避免重复 - seen := make(map[string]struct{}) - var secretNames []string - - for _, component := range cluster.Spec.ComponentSpecs { - for _, systemAccount := range component.SystemAccounts { - if systemAccount.SecretRef == nil || systemAccount.SecretRef.Name == "" { - continue - } - - secretName := systemAccount.SecretRef.Name - if _, exists := seen[secretName]; !exists { - seen[secretName] = struct{}{} - secretNames = append(secretNames, secretName) - } - } - } - - return secretNames -} - -// countSecretReferences 计算 secret 的被引用次数 -func countSecretReferences(clusters []kbappsv1.Cluster, secretName string) int { - if secretName == "" { - return 0 - } - - count := 0 - for i := range clusters { - if clusterReferencesSecret(&clusters[i], secretName) { - count++ - } - } - return count -} - -func clusterReferencesSecret(cluster *kbappsv1.Cluster, secretName string) bool { - if cluster == nil || secretName == "" { - return false - } - - for _, comp := range cluster.Spec.ComponentSpecs { - for _, sysAcct := range comp.SystemAccounts { - if sysAcct.SecretRef != nil && sysAcct.SecretRef.Name == secretName { - return true - } - } - } - - return false -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go deleted file mode 100644 index 10f26cb4e..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go +++ /dev/null @@ -1,1105 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - datav1alpha "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestCleanupClusterOpsRequests(t *testing.T) { - ctx := context.Background() - clusterName := "test-cluster" - cluster := testutil.NewMySQLCluster(clusterName, testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - - tests := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - expectErr bool - errContains string - verify func(*testing.T, client.Client) - }{ - { - name: "non_final_list_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list failed")). - Build() - }, - expectErr: true, - errContains: "get existing opsrequests", - }, - { - name: "blocking_cleanup_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithPatchError(errors.New("patch failed")). - Build() - }, - setup: func(c client.Client) error { - blockingCancel := testutil.NewOpsRequestBuilder("cancel-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - blockingExpire := testutil.NewOpsRequestBuilder("expire-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build() - timeout := int32(300) - blockingExpire.Spec.TimeoutSeconds = &timeout - return testutil.CreateObjects(ctx, c, []client.Object{blockingCancel, blockingExpire}) - }, - expectErr: true, - errContains: "cleanup blocking ops", - }, - { - name: "all_ops_list_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list all failed")). - Build() - }, - expectErr: true, - errContains: "get existing opsrequests", - }, - { - name: "no_ops_present", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - verify: func(t *testing.T, c client.Client) { - var list opsv1alpha1.OpsRequestList - err := c.List(ctx, &list, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, list.Items, 0) - }, - }, - { - name: "cleanup_and_delete_all", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) error { - blockingCancel := testutil.NewOpsRequestBuilder("cancel-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - blockingExpire := testutil.NewOpsRequestBuilder("expire-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - timeout := int32(600) - blockingExpire.Spec.TimeoutSeconds = &timeout - finalOps := testutil.NewOpsRequestBuilder("final", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{blockingCancel, blockingExpire, finalOps}) - }, - verify: func(t *testing.T, c client.Client) { - var list opsv1alpha1.OpsRequestList - err := c.List(ctx, &list, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, list.Items, 0) - }, - }, - { - name: "only_final_ops", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) error { - finalOps := []client.Object{ - testutil.NewOpsRequestBuilder("succeeded", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("failed", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestoreType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, finalOps) - }, - verify: func(t *testing.T, c client.Client) { - var list opsv1alpha1.OpsRequestList - err := c.List(ctx, &list, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, list.Items, 0) - }, - }, - { - name: "delete_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithDeleteError(errors.New("delete failed")). - Build() - }, - setup: func(c client.Client) error { - op := testutil.NewOpsRequestBuilder("delete-me", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - return testutil.CreateObjects(ctx, c, []client.Object{op}) - }, - expectErr: true, - errContains: "delete all ops", - }, - { - name: "ignore_not_found", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) error { - // 不创建任何对象,模拟对象已被其他进程删除的场景 - return nil - }, - verify: func(t *testing.T, c client.Client) { - // 验证没有创建任何OpsRequest对象 - var list opsv1alpha1.OpsRequestList - err := c.List(ctx, &list, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, list.Items, 0) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - svc := &Service{client: k8sClient} - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - err := svc.cleanupClusterOpsRequests(ctx, cluster.DeepCopy()) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestDeleteAllOpsRequestsConcurrently(t *testing.T) { - ctx := context.Background() - clusterName := "test-cluster" - - tests := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - ops []opsv1alpha1.OpsRequest - expectErr bool - errContains string - verify func(*testing.T, client.Client) - }{ - { - name: "empty_list", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - ops: nil, - }, - { - name: "delete_success", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - ops := []client.Object{ - testutil.NewOpsRequestBuilder("success-1", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("success-2", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - ops: []opsv1alpha1.OpsRequest{ - *testutil.NewOpsRequestBuilder("success-1", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - *testutil.NewOpsRequestBuilder("success-2", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.VerticalScalingType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build(), - }, - verify: func(t *testing.T, c client.Client) { - for _, name := range []string{"success-1", "success-2"} { - op := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: name}, op) - assert.Error(t, err) - assert.True(t, apierrors.IsNotFound(err)) - } - }, - }, - { - name: "delete_not_found", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) error { - // 不创建任何对象,模拟要删除的对象已被其他进程删除 - return nil - }, - ops: []opsv1alpha1.OpsRequest{ - *testutil.NewOpsRequestBuilder("gone", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestoreType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }, - }, - { - name: "delete_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithDeleteError(errors.New("delete failed")). - Build() - }, - setup: func(c client.Client) error { - ops := []client.Object{ - testutil.NewOpsRequestBuilder("bad", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - } - return testutil.CreateObjects(ctx, c, ops) - }, - ops: []opsv1alpha1.OpsRequest{ - *testutil.NewOpsRequestBuilder("bad", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.BackupType). - WithInstanceLabel(clusterName). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }, - expectErr: true, - errContains: "failed to delete opsrequest bad", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - svc := &Service{client: k8sClient} - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - err := svc.deleteAllOpsRequestsConcurrently(ctx, tt.ops) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestExtractSecretRefs(t *testing.T) { - tests := []struct { - name string - cluster *kbappsv1.Cluster - expected []string - }{ - { - name: "single_secret", - cluster: testutil.NewRedisCluster("test", testutil.TestNamespace). - WithSystemAccountSecret("default", "secret-1"). - Build(), - expected: []string{"secret-1"}, - }, - { - name: "multiple_unique_secrets", - cluster: func() *kbappsv1.Cluster { - c := testutil.NewRedisCluster("test", testutil.TestNamespace).Build() - c.Spec.ComponentSpecs = []kbappsv1.ClusterComponentSpec{ - { - Name: "comp1", - SystemAccounts: []kbappsv1.ComponentSystemAccount{ - {Name: "acc1", SecretRef: &kbappsv1.ProvisionSecretRef{Name: "secret-1"}}, - }, - }, - { - Name: "comp2", - SystemAccounts: []kbappsv1.ComponentSystemAccount{ - {Name: "acc2", SecretRef: &kbappsv1.ProvisionSecretRef{Name: "secret-2"}}, - }, - }, - } - return c - }(), - expected: []string{"secret-1", "secret-2"}, - }, - { - name: "duplicate_secrets_should_dedup", - cluster: func() *kbappsv1.Cluster { - c := testutil.NewRedisCluster("test", testutil.TestNamespace).Build() - c.Spec.ComponentSpecs = []kbappsv1.ClusterComponentSpec{ - { - Name: "comp1", - SystemAccounts: []kbappsv1.ComponentSystemAccount{ - {Name: "acc1", SecretRef: &kbappsv1.ProvisionSecretRef{Name: "secret-1"}}, - {Name: "acc2", SecretRef: &kbappsv1.ProvisionSecretRef{Name: "secret-1"}}, - }, - }, - } - return c - }(), - expected: []string{"secret-1"}, - }, - { - name: "nil_cluster", - cluster: nil, - expected: nil, - }, - { - name: "no_systemaccounts", - cluster: testutil.NewRedisCluster("test", testutil.TestNamespace).Build(), - expected: []string{}, - }, - { - name: "secretref_nil", - cluster: func() *kbappsv1.Cluster { - c := testutil.NewRedisCluster("test", testutil.TestNamespace).Build() - c.Spec.ComponentSpecs[0].SystemAccounts = []kbappsv1.ComponentSystemAccount{ - {Name: "acc1", SecretRef: nil}, - } - return c - }(), - expected: []string{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := extractSecretRefs(tt.cluster) - assert.ElementsMatch(t, tt.expected, result) - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestClusterReferencesSecret(t *testing.T) { - tests := []struct { - name string - cluster *kbappsv1.Cluster - secretName string - expected bool - }{ - { - name: "references_secret", - cluster: testutil.NewRedisCluster("test", testutil.TestNamespace). - WithSystemAccountSecret("default", "my-secret"). - Build(), - secretName: "my-secret", - expected: true, - }, - { - name: "does_not_reference_secret", - cluster: testutil.NewRedisCluster("test", testutil.TestNamespace). - WithSystemAccountSecret("default", "other-secret"). - Build(), - secretName: "my-secret", - expected: false, - }, - { - name: "nil_cluster", - cluster: nil, - secretName: "my-secret", - expected: false, - }, - { - name: "empty_secret_name", - cluster: testutil.NewRedisCluster("test", testutil.TestNamespace).Build(), - secretName: "", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := clusterReferencesSecret(tt.cluster, tt.secretName) - assert.Equal(t, tt.expected, result) - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestCountSecretReferences(t *testing.T) { - tests := []struct { - name string - clusters []kbappsv1.Cluster - secretName string - expected int - }{ - { - name: "single_reference", - clusters: []kbappsv1.Cluster{ - *testutil.NewRedisCluster("c1", testutil.TestNamespace). - WithSystemAccountSecret("default", "secret-1"). - Build(), - }, - secretName: "secret-1", - expected: 1, - }, - { - name: "multiple_references", - clusters: []kbappsv1.Cluster{ - *testutil.NewRedisCluster("c1", testutil.TestNamespace). - WithSystemAccountSecret("default", "secret-1"). - Build(), - *testutil.NewRedisCluster("c2", testutil.TestNamespace). - WithSystemAccountSecret("default", "secret-1"). - Build(), - *testutil.NewRedisCluster("c3", testutil.TestNamespace). - WithSystemAccountSecret("default", "secret-2"). - Build(), - }, - secretName: "secret-1", - expected: 2, - }, - { - name: "no_references", - clusters: []kbappsv1.Cluster{ - *testutil.NewRedisCluster("c1", testutil.TestNamespace). - WithSystemAccountSecret("default", "other-secret"). - Build(), - }, - secretName: "secret-1", - expected: 0, - }, - { - name: "empty_clusters", - clusters: []kbappsv1.Cluster{}, - secretName: "secret-1", - expected: 0, - }, - { - name: "empty_secret_name", - clusters: []kbappsv1.Cluster{ - *testutil.NewRedisCluster("c1", testutil.TestNamespace).Build(), - }, - secretName: "", - expected: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := countSecretReferences(tt.clusters, tt.secretName) - assert.Equal(t, tt.expected, result) - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.delete-cleanup -func TestDeleteSecretsByCluster(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - setup func(client.Client) (*kbappsv1.Cluster, error) - clientSetup func() client.Client - expectErr bool - errContains string - verify func(*testing.T, client.Client) - }{ - { - name: "single_cluster_single_secret_should_delete", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - cluster := testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID("svc-001"). - WithSystemAccountSecret("default", "test-secret"). - Build() - - secret := testutil.NewSecretBuilder("test-secret", testutil.TestNamespace). - WithServiceID("svc-001"). - Build() - - return cluster, testutil.CreateObjects(ctx, c, []client.Object{cluster, secret}) - }, - verify: func(t *testing.T, c client.Client) { - var secret corev1.Secret - err := c.Get(ctx, types.NamespacedName{ - Name: "test-secret", - Namespace: testutil.TestNamespace, - }, &secret) - assert.True(t, apierrors.IsNotFound(err), "secret should be deleted") - }, - }, - { - name: "two_clusters_share_secret_delete_one_should_preserve", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - sharedSecret := testutil.NewSecretBuilder("shared-secret", testutil.TestNamespace). - WithServiceID("svc-001"). - Build() - - clusterA := testutil.NewRedisCluster("cluster-a", testutil.TestNamespace). - WithServiceID("svc-001"). - WithSystemAccountSecret("default", "shared-secret"). - Build() - - clusterB := testutil.NewRedisCluster("cluster-b", testutil.TestNamespace). - WithServiceID("svc-002"). - WithSystemAccountSecret("default", "shared-secret"). - Build() - - err := testutil.CreateObjects(ctx, c, []client.Object{sharedSecret, clusterA, clusterB}) - return clusterA, err - }, - verify: func(t *testing.T, c client.Client) { - var secret corev1.Secret - err := c.Get(ctx, types.NamespacedName{ - Name: "shared-secret", - Namespace: testutil.TestNamespace, - }, &secret) - assert.NoError(t, err, "secret should still exist because cluster-b references it") - }, - }, - { - name: "restored_cluster_scenario", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - originalSecret := testutil.NewSecretBuilder("cluster-a-redis-account", testutil.TestNamespace). - WithServiceID("svc-original"). - Build() - - originalCluster := testutil.NewRedisCluster("cluster-a", testutil.TestNamespace). - WithServiceID("svc-original"). - WithSystemAccountSecret("default", "cluster-a-redis-account"). - Build() - - restoredCluster := testutil.NewRedisCluster("cluster-a-restore-abcd", testutil.TestNamespace). - WithServiceID("svc-restored"). - WithSystemAccountSecret("default", "cluster-a-redis-account"). - Build() - - err := testutil.CreateObjects(ctx, c, []client.Object{originalSecret, originalCluster, restoredCluster}) - return originalCluster, err - }, - verify: func(t *testing.T, c client.Client) { - var secret corev1.Secret - err := c.Get(ctx, types.NamespacedName{ - Name: "cluster-a-redis-account", - Namespace: testutil.TestNamespace, - }, &secret) - assert.NoError(t, err, "secret should be preserved for restored cluster") - }, - }, - { - name: "cluster_without_systemaccounts_should_skip", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - cluster := testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID("svc-001"). - Build() - cluster.Spec.ComponentSpecs[0].SystemAccounts = nil - - return cluster, testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - }, - { - name: "secret_not_found_should_not_fail", - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - cluster := testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithSystemAccountSecret("default", "non-existent-secret"). - Build() - - return cluster, testutil.CreateObjects(ctx, c, []client.Object{cluster}) - }, - expectErr: false, - }, - { - name: "list_clusters_error_should_fail", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(fmt.Errorf("list failed")). - Build() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - cluster := testutil.NewRedisCluster("test", testutil.TestNamespace). - WithSystemAccountSecret("default", "test-secret"). - Build() - return cluster, nil - }, - expectErr: true, - errContains: "list clusters in namespace", - }, - { - name: "delete_secret_error_should_record", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithDeleteError(fmt.Errorf("delete failed")). - Build() - }, - setup: func(c client.Client) (*kbappsv1.Cluster, error) { - cluster := testutil.NewRedisCluster("test", testutil.TestNamespace). - WithSystemAccountSecret("default", "test-secret"). - Build() - - secret := testutil.NewSecretBuilder("test-secret", testutil.TestNamespace).Build() - - return cluster, testutil.CreateObjects(ctx, c, []client.Object{cluster, secret}) - }, - expectErr: true, - errContains: "failed to delete", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - svc := &Service{client: k8sClient} - - cluster, err := tt.setup(k8sClient) - require.NoError(t, err, "setup should not fail") - - err = svc.deleteSecretsByCluster(ctx, cluster) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.create -func TestCreateCluster(t *testing.T) { - tests := []struct { - name string - input model.ClusterInput - clientSetup func() client.Client - setup func(client.Client) error - useTimeout bool - expectErr bool - errContains string - verify func(*testing.T, client.Client) - }{ - { - name: "empty_name", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "", - Type: "mysql", - }, - }, - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - expectErr: true, - errContains: "name is required", - }, - { - name: "unsupported_cluster_type", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-unsupported", - Namespace: testutil.TestNamespace, - Type: "unsupported-db", - }, - }, - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - expectErr: true, - errContains: "unsupported cluster type", - }, - { - name: "build_cluster_fails_invalid_resources", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-invalid-resources", - Namespace: testutil.TestNamespace, - Type: "mysql", - Version: "8.0.30", - StorageClass: "standard", - TerminationPolicy: kbappsv1.Delete, - }, - ClusterResource: model.ClusterResource{ - CPU: "invalid-cpu", - Memory: "2Gi", - Storage: "10Gi", - Replicas: 1, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: "default-repo", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: datav1alpha.RetentionPeriod("7d"), - }, - RBDService: model.RBDService{ - ServiceID: testutil.TestServiceID, - }, - }, - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - expectErr: true, - errContains: "build mysql cluster", - }, - { - name: "success_without_custom_secret", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-mysql", - Namespace: testutil.TestNamespace, - Type: "mysql", - Version: "8.0.30", - StorageClass: "standard", - TerminationPolicy: kbappsv1.Delete, - }, - ClusterResource: model.ClusterResource{ - CPU: "1", - Memory: "2Gi", - Storage: "10Gi", - Replicas: 1, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: "default-repo", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: datav1alpha.RetentionPeriod("7d"), - }, - RBDService: model.RBDService{ - ServiceID: testutil.TestServiceID, - }, - }, - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - verify: func(t *testing.T, c client.Client) { - ctx := context.Background() - var clusterList kbappsv1.ClusterList - err := c.List(ctx, &clusterList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - require.Len(t, clusterList.Items, 1) - - cluster := clusterList.Items[0] - assert.Equal(t, testutil.TestServiceID, cluster.Labels[index.ServiceIDLabel]) - - var secretList corev1.SecretList - err = c.List(ctx, &secretList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, secretList.Items, 0) - }, - }, - { - name: "success_with_system_account", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-redis", - Namespace: testutil.TestNamespace, - Type: "redis", - Version: "7.0.6", - StorageClass: "standard", - TerminationPolicy: kbappsv1.Delete, - }, - ClusterResource: model.ClusterResource{ - CPU: "1", - Memory: "2Gi", - Storage: "10Gi", - Replicas: 1, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: "default-repo", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: datav1alpha.RetentionPeriod("7d"), - }, - RBDService: model.RBDService{ - ServiceID: testutil.TestServiceID, - }, - }, - clientSetup: func() client.Client { - return testutil.NewFakeClientWithIndexes() - }, - verify: func(t *testing.T, c client.Client) { - ctx := context.Background() - // 验证 Cluster 被创建 - var clusterList kbappsv1.ClusterList - err := c.List(ctx, &clusterList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - require.Len(t, clusterList.Items, 1) - - cluster := clusterList.Items[0] - assert.Equal(t, testutil.TestServiceID, cluster.Labels[index.ServiceIDLabel]) - - // 验证 SystemAccounts 配置正确 - found := false - for _, comp := range cluster.Spec.ComponentSpecs { - if comp.Name == "redis" { - require.NotEmpty(t, comp.SystemAccounts) - assert.Equal(t, "default", comp.SystemAccounts[0].Name) - assert.NotNil(t, comp.SystemAccounts[0].SecretRef) - found = true - break - } - } - assert.True(t, found, "redis component not found") - - // 验证 Secret 被创建且内容正确 - var secretList corev1.SecretList - err = c.List(ctx, &secretList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - require.Len(t, secretList.Items, 1) - - secret := secretList.Items[0] - assert.Equal(t, testutil.TestServiceID, secret.Labels[index.ServiceIDLabel]) - assert.Contains(t, secret.Data, "username") - assert.Contains(t, secret.Data, "password") - assert.Equal(t, "default", string(secret.Data["username"])) - assert.NotEmpty(t, secret.Data["password"]) - }, - }, - { - name: "secret_create_fails_no_resources_left", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-redis-fail", - Namespace: testutil.TestNamespace, - Type: "redis", - Version: "7.0.6", - StorageClass: "standard", - TerminationPolicy: kbappsv1.Delete, - }, - ClusterResource: model.ClusterResource{ - CPU: "1", - Memory: "2Gi", - Storage: "10Gi", - Replicas: 1, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: "default-repo", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: datav1alpha.RetentionPeriod("7d"), - }, - RBDService: model.RBDService{ - ServiceID: testutil.TestServiceID, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("secret create failed")). - Build() - }, - expectErr: true, - errContains: "create system account secret", - verify: func(t *testing.T, c client.Client) { - ctx := context.Background() - // 验证没有 Cluster 被创建 - var clusterList kbappsv1.ClusterList - err := c.List(ctx, &clusterList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, clusterList.Items, 0) - - // 验证没有 Secret 被创建 - var secretList corev1.SecretList - err = c.List(ctx, &secretList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, secretList.Items, 0) - }, - }, - { - name: "cluster_create_fails_secret_cleaned", - input: model.ClusterInput{ - ClusterInfo: model.ClusterInfo{ - Name: "test-redis-conflict", - Namespace: testutil.TestNamespace, - Type: "redis", - Version: "7.0.6", - StorageClass: "standard", - TerminationPolicy: kbappsv1.Delete, - }, - ClusterResource: model.ClusterResource{ - CPU: "1", - Memory: "2Gi", - Storage: "10Gi", - Replicas: 1, - }, - ClusterBackup: model.ClusterBackup{ - BackupRepo: "default-repo", - Schedule: model.BackupSchedule{ - Frequency: model.Daily, - Hour: 2, - Minute: 0, - }, - RetentionPeriod: datav1alpha.RetentionPeriod("7d"), - }, - RBDService: model.RBDService{ - ServiceID: testutil.TestServiceID, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateErrorForType(&kbappsv1.Cluster{}, errors.New("cluster create failed")). - Build() - }, - expectErr: true, - errContains: "create cluster", - verify: func(t *testing.T, c client.Client) { - ctx := context.Background() - // 验证没有 Cluster 被创建 - var clusterList kbappsv1.ClusterList - err := c.List(ctx, &clusterList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, clusterList.Items, 0) - - // 验证 Secret 被清理(不存在) - var secretList corev1.SecretList - err = c.List(ctx, &secretList, client.InNamespace(testutil.TestNamespace)) - require.NoError(t, err) - assert.Len(t, secretList.Items, 0) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - svc := &Service{client: k8sClient} - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - ctx := context.Background() - if tt.useTimeout { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - } - - cluster, err := svc.CreateCluster(ctx, tt.input) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - assert.Nil(t, cluster) - if tt.verify != nil { - tt.verify(t, k8sClient) - } - return - } - - require.NoError(t, err) - require.NotNil(t, cluster) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/parameter.go b/plugins/kb-adapter-rbdplugin/service/cluster/parameter.go deleted file mode 100644 index 19bfb8c6a..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/parameter.go +++ /dev/null @@ -1,692 +0,0 @@ -package cluster - -import ( - "cmp" - "context" - "encoding/json" - "errors" - "fmt" - "slices" - "strconv" - "strings" - - "github.com/apecloud/kubeblocks/apis/parameters/v1alpha1" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - "github.com/sahilm/fuzzy" - "golang.org/x/sync/errgroup" - corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// GetClusterParameter 获取 KubeBlocks Cluster 的 Parameter -func (s *Service) GetClusterParameter(ctx context.Context, query model.ClusterParametersQuery) (*model.PaginatedResult[model.Parameter], error) { - // 先通过 ComponentDefinition 获取 Parameters(value 为 definition 中的默认值), - // 再通过 configmap 从数据库配置文件构造 ParameterEntry, - // 最后将获取到的 Parameters 与 ParameterEntry 取交集,确保只返回数据库配置文件中的 Parameter - - query.Validate() - - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, query.ServiceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", query.ServiceID, err) - } - - var ( - constraints map[string]model.Parameter - parameterEntries []model.ParameterEntry - ) - - g, gctx := errgroup.WithContext(ctx) - - g.Go(func() error { - c, err := s.getParameterConstraints(gctx, cluster) - if err != nil { - return fmt.Errorf("get parameter constraints: %w", err) - } - constraints = c - return nil - }) - - g.Go(func() error { - pe, err := s.getParametersFromConfigmap(gctx, cluster) - if err != nil { - return fmt.Errorf("get parameters from ConfigMap: %w", err) - } - parameterEntries = pe - return nil - }) - - if err := g.Wait(); errors.Is(err, kbkit.ErrTargetNotFound) { - // 不支持 parameter 的 cluster 返回空列表,而不是报错 - log.Info( - "cluster does not support parameter", - log.String("serviceID", cluster.Name), - log.String("clusterType", cluster.Spec.ClusterDef), - log.String("serviceID", query.ServiceID), - ) - return &model.PaginatedResult[model.Parameter]{ - Items: []model.Parameter{}, - Total: 0, - }, nil - } else if err != nil { - return nil, err - } - - parameters := mergeEntriesAndConstraints(parameterEntries, constraints) - - // Rainbond 隐藏 immutable 参数 - parameters = filterOutImmutableParameters(parameters) - - // 对参数名称进行搜索 - if keyword := strings.TrimSpace(query.Keyword); keyword != "" { - parameters = filterParametersByKeyword(parameters, keyword) - } - - slices.SortStableFunc(parameters, func(a, b model.Parameter) int { - return cmp.Compare(a.Name, b.Name) - }) - - totalCount := len(parameters) - result := kbkit.Paginate(parameters, query.Page, query.PageSize) - - log.Debug("get paginated parameters", log.Any("parameters", parameters)) - return &model.PaginatedResult[model.Parameter]{ - Items: result, - Total: totalCount, - }, nil -} - -// ChangeClusterParameter 变更给定 service_id 对应的 Cluster 的参数设置 -func (s *Service) ChangeClusterParameter( - ctx context.Context, - req model.ClusterParametersChange, -) (*model.ParameterChangeResult, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, req.ServiceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", req.ServiceID, err) - } - - constraints, err := s.getParameterConstraints(ctx, cluster) - if err != nil { - return nil, fmt.Errorf("get parameter constraints: %w", err) - } - - // 将约束转换为参数列表以创建验证器 - constraintList := make([]model.Parameter, 0, len(constraints)) - for _, constraint := range constraints { - constraintList = append(constraintList, constraint) - } - - // 创建参数验证器 - validator := kbkit.NewParameterValidator(constraintList) - - paramCount := len(req.Parameters) - applied := make([]string, 0, paramCount) // 成功应用的参数名称列表 - invalids := make([]model.ParameterChangeError, 0, paramCount/4) // 校验失败的参数,预期25%失败率 - validParameters := make([]model.ParameterEntry, 0, paramCount) // 符合约束用于创建 Ops 的参数 - - // 验证所有参数变更 - for _, parameterToChange := range req.Parameters { - if validationErr := validator.Validate(parameterToChange); validationErr != nil { - // 验证失败,添加到 invalid - invalids = append(invalids, model.ParameterChangeError{ - Name: validationErr.ParameterName, - Code: string(validationErr.ErrorCode), - }) - continue - } - - // 验证成功 - applied = append(applied, parameterToChange.Name) - - // 构建用于创建 OpsRequest 的参数 - validParam := model.ParameterEntry{ - Name: parameterToChange.Name, - Value: validator.ConvertToStringValue(parameterToChange.Value), - } - validParameters = append(validParameters, validParam) - } - - // 创建 OpsRequest(仅当存在有效参数变更时) - if len(validParameters) > 0 { - if err := kbkit.CreateParameterChangeOpsRequest(ctx, s.client, cluster, validParameters); err != nil { - return nil, fmt.Errorf("create parameter change OpsRequest: %w", err) - } - - log.Debug("created parameter change OpsRequest", - log.String("clusterName", cluster.Name), - log.Int("parameterCount", len(validParameters))) - } - - result := &model.ParameterChangeResult{ - Applied: applied, - Invalids: invalids, - } - - log.Debug("parameter change operation completed", - log.String("clusterName", cluster.Name), - log.Int("appliedCount", len(applied)), - log.Int("invalidCount", len(invalids))) - - return result, nil -} - -// getParameterConstraints 从 KubeBlocks 的参数定义中提取参数约束为 map[string]model.Parameter -// 返回 dynamic、static 与 immutable;componentName 可选,未提供则回退第一个普通组件 -// 返回 map[string]model.Parameter,便于后续合并参数约束 -func (s *Service) getParameterConstraints( - ctx context.Context, - cluster *kbappsv1.Cluster, - componentName ...string, -) (map[string]model.Parameter, error) { - compName, err := s.determineComponentName(cluster, componentName...) - if err != nil { - return nil, fmt.Errorf("determine component name: %w", err) - } - - compDef, err := s.resolveComponentDefinition(ctx, cluster, compName) - if err != nil { - return nil, fmt.Errorf("resolve component definition: %w", err) - } - - renderer, err := s.findParamConfigRenderer(ctx, compDef) - if err != nil { - return nil, fmt.Errorf("find param config renderer: %w", err) - } - - paramDefs, err := s.getParameterDefinitions(ctx, renderer) - if err != nil { - return nil, fmt.Errorf("get parameter definitions: %w", err) - } - - parameters := make(map[string]model.Parameter) - allParamSets := &model.ParameterSets{ - Dynamic: make(map[string]bool), - Static: make(map[string]bool), - Immutable: make(map[string]bool), - } - - // 从 schema 提取带完整定义的参数 - for _, pd := range paramDefs { - if pd == nil { - continue - } - log.Debug("processing parameter definition", log.String("pd", pd.Name)) - schema, err := s.processParameterSchema(&pd.Spec) - if err != nil { - return nil, fmt.Errorf("process parameter schema: %w", err) - } - - paramSets := createParameterSets(&pd.Spec) - - // 合并所有 ParametersDefinition 的参数集合 - for param := range paramSets.Dynamic { - allParamSets.Dynamic[param] = true - } - for param := range paramSets.Static { - allParamSets.Static[param] = true - } - for param := range paramSets.Immutable { - allParamSets.Immutable[param] = true - } - - // 仅处理有 schema 定义的参数 - if schema == nil { - continue - } - - properties := s.extractSchemaProperties(schema) - if len(properties) == 0 { - continue - } - - for paramName, property := range properties { - param := s.buildParameterConstraint(paramName, property, paramSets) - if _, exists := parameters[paramName]; exists { - log.Debug("duplicate parameter name detected; overriding previous entry", log.String("param", paramName)) - } - parameters[paramName] = param - } - } - - // 补充只在参数列表中声明但没有 schema 定义的参数 - // 收集所有出現在 parametersdefinitions 中的参数 - allDeclaredParams := make(map[string]bool) - for param := range allParamSets.Dynamic { - allDeclaredParams[param] = true - } - for param := range allParamSets.Static { - allDeclaredParams[param] = true - } - for param := range allParamSets.Immutable { - allDeclaredParams[param] = true - } - // 补充参数 - for paramName := range allDeclaredParams { - if _, exists := parameters[paramName]; !exists { - // 创建基础约束:只包含名称和可变性标记,Type 为空表示无详细约束 - param := model.Parameter{ - ParameterEntry: model.ParameterEntry{ - Name: paramName, - Value: nil, - }, - Type: "", // 无 schema 约束 - IsDynamic: allParamSets.Dynamic[paramName], - IsImmutable: allParamSets.Immutable[paramName], - IsRequired: false, - } - parameters[paramName] = param - log.Debug("parameter declared in list but missing schema definition", - log.String("param", paramName), - log.Bool("isDynamic", param.IsDynamic), - log.Bool("isImmutable", param.IsImmutable)) - } - } - - return parameters, nil -} - -// getParametersFromConfigmap 从 configmap 中获取实际设置的 Parameter 并覆盖默认值 -func (s *Service) getParametersFromConfigmap( - ctx context.Context, - cluster *kbappsv1.Cluster, -) (parameters []model.ParameterEntry, err error) { - - // 获取对应数据库类型的适配器 - a, exists := registry.Cluster[cluster.Spec.ClusterDef] - if !exists { - return nil, fmt.Errorf("unsupported cluster type: %s", cluster.Spec.ClusterDef) - } - - // 获取存有参数配置的 ConfigMap 名称 - cmName := a.Coordinator.GetParametersConfigMap(cluster.Name) - if cmName == nil { - log.Debug("cluster type does not support parameter configuration", log.String("clusterType", cluster.Spec.ClusterDef)) - return []model.ParameterEntry{}, nil - } - - // 获取 ConfigMap - var configMap corev1.ConfigMap - cmKey := client.ObjectKey{ - Name: *cmName, - Namespace: cluster.Namespace, - } - - if err := s.client.Get(ctx, cmKey, &configMap); err != nil { - if client.IgnoreNotFound(err) == nil { - log.Debug("parameters ConfigMap not found", log.String("configMap", *cmName), log.String("namespace", cluster.Namespace)) - return []model.ParameterEntry{}, nil - } - return nil, fmt.Errorf("get parameters ConfigMap %s: %w", *cmName, err) - } - - // 使用对应的 Coordinator 解析配置 - parameters, err = a.Coordinator.ParseParameters(configMap.Data) - if err != nil { - return nil, fmt.Errorf("parse parameters from ConfigMap %s: %w", *cmName, err) - } - - log.Debug("successfully loaded parameters from ConfigMap", - log.String("configMap", *cmName), - log.String("clusterType", cluster.Spec.ClusterDef), - log.Int("parameterCount", len(parameters))) - - return parameters, nil -} - -// determineComponentName 返回要解析的组件名: -// 优先使用显式传入的 componentName,否则回退到第一个组件;未找到时报错 -func (s *Service) determineComponentName(cluster *kbappsv1.Cluster, componentName ...string) (string, error) { - if len(componentName) > 0 && componentName[0] != "" { - return componentName[0], nil - } - - if len(cluster.Spec.ComponentSpecs) == 0 { - return "", kbkit.ErrTargetNotFound - } - - firstCompSpec := cluster.Spec.ComponentSpecs[0] - if firstCompSpec.Name == "" { - return "", fmt.Errorf("first component spec has empty name") - } - - return firstCompSpec.Name, nil -} - -// resolveComponentDefinition 根据组件名读取 ComponentDefinition: -func (s *Service) resolveComponentDefinition(ctx context.Context, cluster *kbappsv1.Cluster, componentName string) (*kbappsv1.ComponentDefinition, error) { - var compSpec *kbappsv1.ClusterComponentSpec - for i := range cluster.Spec.ComponentSpecs { - if cluster.Spec.ComponentSpecs[i].Name == componentName { - compSpec = &cluster.Spec.ComponentSpecs[i] - break - } - } - - if compSpec == nil { - return nil, fmt.Errorf("component %s not found in cluster: %w", componentName, kbkit.ErrTargetNotFound) - } - - if compSpec.ComponentDef == "" { - return nil, fmt.Errorf("component %s has empty ComponentDef: %w", componentName, kbkit.ErrTargetNotFound) - } - - var compDef kbappsv1.ComponentDefinition - key := client.ObjectKey{Name: compSpec.ComponentDef} - if err := s.client.Get(ctx, key, &compDef); err != nil { - return nil, fmt.Errorf("get component definition %s: %w", compSpec.ComponentDef, err) - } - - return &compDef, nil -} - -// findParamConfigRenderer 查找唯一匹配的 ParamConfigRenderer: -// 组件名需匹配,ServiceVersion 为空或等于 compDef 的版本; -// 数量为 0 返回 ErrTargetNotFound,>1 报错 -func (s *Service) findParamConfigRenderer( - ctx context.Context, - compDef *kbappsv1.ComponentDefinition, -) (*v1alpha1.ParamConfigRenderer, error) { - var rendererList v1alpha1.ParamConfigRendererList - if err := s.client.List(ctx, &rendererList); err != nil { - return nil, fmt.Errorf("list ParamConfigRenderer: %w", err) - } - - var matchedRenderers []*v1alpha1.ParamConfigRenderer - for i := range rendererList.Items { - renderer := &rendererList.Items[i] - - if renderer.Spec.ComponentDef != compDef.Name { - continue - } - - rendererServiceVersion := renderer.Spec.ServiceVersion - compDefServiceVersion := compDef.Spec.ServiceVersion - - if rendererServiceVersion != "" && rendererServiceVersion != compDefServiceVersion { - continue - } - - matchedRenderers = append(matchedRenderers, renderer) - } - - switch len(matchedRenderers) { - case 0: - return nil, kbkit.ErrTargetNotFound - case 1: - return matchedRenderers[0], nil - default: - return nil, kbkit.ErrMultipleFounded - } -} - -// getParameterDefinitions 按 renderer.Spec.ParametersDefs 批量获取 ParametersDefinition。 -func (s *Service) getParameterDefinitions( - ctx context.Context, - renderer *v1alpha1.ParamConfigRenderer, -) ([]*v1alpha1.ParametersDefinition, error) { - if renderer == nil { - return nil, nil - } - - // 在现行体系下,ParametersDefinition 与 ParamConfigRenderer 是一对一的 - paramDefs := make([]*v1alpha1.ParametersDefinition, 0, len(renderer.Spec.ParametersDefs)) - for _, paramDefName := range renderer.Spec.ParametersDefs { - var paramDef v1alpha1.ParametersDefinition - key := client.ObjectKey{ - Name: paramDefName, - } - - if err := s.client.Get(ctx, key, ¶mDef); err != nil { - return nil, fmt.Errorf("get ParametersDefinition %s: %w", paramDefName, err) - } - - paramDefs = append(paramDefs, ¶mDef) - } - - return paramDefs, nil -} - -// processParameterSchema 返回 ParametersDefinition 中的 JSON Schema: -// 仅处理 schemaInJSON,忽略 CUE, 目前的需求下 ParametersDefinition 都支持 spec.parametersSchema.schemaInJSON。 -func (s *Service) processParameterSchema( - spec *v1alpha1.ParametersDefinitionSpec, -) (*apiextensionsv1.JSONSchemaProps, error) { - if spec.ParametersSchema == nil { - return nil, nil - } - - schema := spec.ParametersSchema - if schema.SchemaInJSON == nil { - return nil, nil - } - - return schema.SchemaInJSON, nil -} - -// extractSchemaProperties 从 schema.Properties["spec"] 提取一层参数属性; -// 跳过 type==object 的容器字段,返回 name->property 映射。 -func (s *Service) extractSchemaProperties( - schema *apiextensionsv1.JSONSchemaProps, -) map[string]apiextensionsv1.JSONSchemaProps { - if schema == nil { - return nil - } - - if schema.Properties == nil { - return nil - } - - specProperty, exists := schema.Properties["spec"] - if !exists { - return nil - } - - if specProperty.Properties == nil { - return nil - } - - result := make(map[string]apiextensionsv1.JSONSchemaProps) - for name, property := range specProperty.Properties { - if property.Type == "object" { - continue - } - - result[name] = property - } - - return result -} - -// createParameterSets 将 ParametersDefinition 中的参数列表转换为集合。 -func createParameterSets(spec *v1alpha1.ParametersDefinitionSpec) *model.ParameterSets { - if spec == nil { - return &model.ParameterSets{} - } - - return &model.ParameterSets{ - Static: sliceToSet(spec.StaticParameters), - Dynamic: sliceToSet(spec.DynamicParameters), - Immutable: sliceToSet(spec.ImmutableParameters), - } -} - -// mergeEntriesAndConstraints 合并 ParameterEntry 与 Parameter -// 仅返回 entries 与 constraints 的交集: -// - 如果某个 entry 未在 constraints 中出现,则跳过 -func mergeEntriesAndConstraints( - entries []model.ParameterEntry, - constraints map[string]model.Parameter, -) []model.Parameter { - parameters := make([]model.Parameter, 0, len(entries)) - for _, e := range entries { - constraint, ok := constraints[e.Name] - if !ok { - continue - } - param := model.Parameter{ - ParameterEntry: e, - Type: constraint.Type, - MinValue: constraint.MinValue, - MaxValue: constraint.MaxValue, - EnumValues: constraint.EnumValues, - Description: constraint.Description, - IsDynamic: constraint.IsDynamic, - IsRequired: constraint.IsRequired, - IsImmutable: constraint.IsImmutable, - } - - // 如果约束中没有类型信息,尝试从参数值推断类型 - if param.Type == "" { - param.Type = inferParameterType(e.Value) - } - - parameters = append(parameters, param) - } - - return parameters -} - -// isDynamicParameter 判定参数是否为动态: -func (s *Service) isDynamicParameter(name string, sets *model.ParameterSets) bool { - return sets.Dynamic[name] -} - -// buildParameterConstraint 构造参数约束: -// Type 优先使用 format;填充描述、动态标记、默认值、数值范围与枚举。 -func (s *Service) buildParameterConstraint(name string, property apiextensionsv1.JSONSchemaProps, sets *model.ParameterSets) model.Parameter { - pType := property.Type - if strings.TrimSpace(property.Format) != "" { - pType = property.Format - } - - parameter := model.Parameter{ - ParameterEntry: model.ParameterEntry{ - Name: name, - Value: nil, - }, - Type: model.ParameterType(pType), - Description: strings.TrimSpace(property.Description), - IsDynamic: s.isDynamicParameter(name, sets), - IsRequired: false, - IsImmutable: sets.Immutable[name], - } - - if property.Default != nil && len(property.Default.Raw) > 0 { - var val any - if err := json.Unmarshal(property.Default.Raw, &val); err != nil { - log.Warn("decode default value failed", log.String("param", name), log.Err(err)) - } else { - parameter.Value = val - } - } - - if property.Minimum != nil { - parameter.MinValue = property.Minimum - } - if property.Maximum != nil { - parameter.MaxValue = property.Maximum - } - - if len(property.Enum) > 0 { - enums := make([]string, 0, len(property.Enum)) - for i := range property.Enum { - if len(property.Enum[i].Raw) == 0 { - continue - } - // 与 kbcli 保持一致:枚举项以 JSON 字符串形式存储(字符串包含引号,布尔/数字为原样 JSON) - enums = append(enums, string(property.Enum[i].Raw)) - } - if len(enums) > 0 { - parameter.EnumValues = enums - } - } - - return parameter -} - -// filterParametersByKeyword 对参数列表进行关键词搜索过滤, 匹配参数名称和描述 -func filterParametersByKeyword(parameters []model.Parameter, keyword string) []model.Parameter { - if strings.TrimSpace(keyword) == "" { - return parameters - } - - keyword = strings.TrimSpace(keyword) - var result []model.Parameter - - for _, param := range parameters { - // 使用 fuzzy 搜索检查参数名称是否匹配 - nameMatches := fuzzy.Find(keyword, []string{param.Name}) - if len(nameMatches) > 0 { - result = append(result, param) - continue - } - } - - return result -} - -// filterOutImmutableParameters 过滤掉不可变参数 -func filterOutImmutableParameters(parameters []model.Parameter) []model.Parameter { - if len(parameters) == 0 { - return parameters - } - result := make([]model.Parameter, 0, len(parameters)) - for _, p := range parameters { - if p.IsImmutable { - continue - } - result = append(result, p) - } - return result -} - -// sliceToSet 将字符串切片转换为集合。 -func sliceToSet(slice []string) map[string]bool { - set := make(map[string]bool, len(slice)) - for _, item := range slice { - set[item] = true - } - return set -} - -// inferParameterType 从参数值推断参数类型 -func inferParameterType(value any) model.ParameterType { - if value == nil { - return "" - } - - switch v := value.(type) { - case int, int32, int64, float32, float64: - return "integer" - case bool: - return "boolean" - case string: - // 尝试解析为数字 - if strings.Contains(v, ".") { - if _, err := strconv.ParseFloat(v, 64); err == nil { - return "number" - } - } else { - if _, err := strconv.ParseInt(v, 10, 64); err == nil { - return "integer" - } - } - - // 尝试解析为布尔值 - if strings.ToUpper(v) == "ON" || strings.ToUpper(v) == "OFF" || - strings.ToLower(v) == "true" || strings.ToLower(v) == "false" { - return "boolean" - } - - return "string" - default: - return "string" - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go deleted file mode 100644 index 5a865e082..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package cluster - -import ( - "testing" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - "github.com/stretchr/testify/assert" - "k8s.io/utils/ptr" -) - -// capability_id: rainbond.kb-adapter.cluster.parameter-constraint-merge -func TestMergeEntriesAndConstraints(t *testing.T) { - tests := []struct { - name string - entries []model.ParameterEntry - constraints map[string]model.Parameter - expected []model.Parameter - description string - }{ - { - name: "empty_entries_empty_constraints", - entries: []model.ParameterEntry{}, - constraints: map[string]model.Parameter{}, - expected: []model.Parameter{}, - description: "两个输入都为空应返回空结果", - }, - { - name: "empty_entries_has_constraints", - entries: []model.ParameterEntry{}, - constraints: map[string]model.Parameter{ - "max_connections": testutil.NewParameterConstraint("max_connections").WithType(model.ParameterTypeInteger).Build(), - }, - expected: []model.Parameter{}, - description: "entries为空时应返回空结果,即使有约束定义", - }, - { - name: "has_entries_empty_constraints", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("max_connections", 100), - }, - constraints: map[string]model.Parameter{}, - expected: []model.Parameter{}, - description: "constraints为空时应过滤掉所有entries", - }, - { - name: "entries_filtered_by_constraints", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("max_connections", 100), - testutil.NewParameterEntry("unknown_param", "should_be_filtered"), - testutil.NewParameterEntry("another_unknown", 123), - }, - constraints: map[string]model.Parameter{ - "max_connections": testutil.NewParameterConstraint("max_connections").WithType(model.ParameterTypeInteger).Build(), - }, - expected: []model.Parameter{ - { - ParameterEntry: testutil.NewParameterEntry("max_connections", 100), - Type: model.ParameterTypeInteger, - }, - }, - description: "只保留在constraints中定义的参数,过滤未知参数", - }, - { - name: "no_type_inference_when_constraint_has_type", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("max_connections", "100"), - }, - constraints: map[string]model.Parameter{ - "max_connections": testutil.NewParameterConstraint("max_connections").WithType(model.ParameterTypeInteger).Build(), - }, - expected: []model.Parameter{ - { - ParameterEntry: testutil.NewParameterEntry("max_connections", "100"), - Type: model.ParameterTypeInteger, // 直接使用constraint中的类型 - }, - }, - description: "constraints有类型信息时,直接使用不进行推断", - }, - { - name: "complete_constraint_information_transfer", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("max_connections", 100), - }, - constraints: map[string]model.Parameter{ - "max_connections": testutil.NewParameterConstraint("max_connections"). - WithType(model.ParameterTypeInteger). - WithRange(ptr.To(1.0), ptr.To(100000.0)). - WithDynamic(true). - Build(), - }, - expected: []model.Parameter{ - { - ParameterEntry: testutil.NewParameterEntry("max_connections", 100), - Type: model.ParameterTypeInteger, - MinValue: ptr.To(1.0), - MaxValue: ptr.To(100000.0), - IsDynamic: true, - }, - }, - description: "应完整传递约束信息:类型、描述、范围、动态标记", - }, - { - name: "nil_value_handling", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("empty_param", nil), - }, - constraints: map[string]model.Parameter{ - "empty_param": testutil.NewParameterConstraint("empty_param").WithType("").Build(), - }, - expected: []model.Parameter{ - { - ParameterEntry: testutil.NewParameterEntry("empty_param", nil), - Type: "", // nil值无法推断类型 - }, - }, - description: "nil值无法进行类型推断,Type应保持为空", - }, - { - name: "realistic_mysql_scenario", - entries: testutil.CreateTypicalMySQLParameterEntries(), - constraints: testutil.CreateTypicalMySQLParameterConstraints(), - expected: []model.Parameter{ - { - ParameterEntry: testutil.NewParameterEntry("max_connections", 100), - Type: model.ParameterTypeInteger, - MinValue: ptr.To(1.0), - MaxValue: ptr.To(100000.0), - IsDynamic: true, - }, - { - ParameterEntry: testutil.NewParameterEntry("innodb_buffer_pool_size", "128M"), - Type: model.ParameterTypeString, - IsImmutable: true, - }, - { - ParameterEntry: testutil.NewParameterEntry("sql_mode", "STRICT_TRANS_TABLES"), - Type: model.ParameterTypeString, - EnumValues: []string{`"STRICT_TRANS_TABLES"`, `"NO_ZERO_DATE"`}, - IsDynamic: true, - }, - { - ParameterEntry: testutil.NewParameterEntry("autocommit", "ON"), - Type: model.ParameterTypeBoolean, - IsDynamic: true, - }, - // query_cache_size 在 entries 中存在但不在 constraints 中,应被过滤 - }, - description: "真实MySQL参数场景:包含各种类型、约束和过滤逻辑", - }, - { - name: "type_inference_boundary_cases", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("bool_on", "ON"), - testutil.NewParameterEntry("bool_off", "OFF"), - testutil.NewParameterEntry("bool_false", "false"), - testutil.NewParameterEntry("negative_int", "-123"), - testutil.NewParameterEntry("negative_float", "-45.67"), - testutil.NewParameterEntry("zero", "0"), - testutil.NewParameterEntry("empty_string", ""), - testutil.NewParameterEntry("mixed_string", "abc123"), - }, - constraints: func() map[string]model.Parameter { - result := make(map[string]model.Parameter) - for _, name := range []string{"bool_on", "bool_off", "bool_false", "negative_int", "negative_float", "zero", "empty_string", "mixed_string"} { - result[name] = testutil.NewParameterConstraint(name).WithType("").Build() - } - return result - }(), - expected: []model.Parameter{ - {ParameterEntry: testutil.NewParameterEntry("bool_on", "ON"), Type: model.ParameterTypeBoolean}, - {ParameterEntry: testutil.NewParameterEntry("bool_off", "OFF"), Type: model.ParameterTypeBoolean}, - {ParameterEntry: testutil.NewParameterEntry("bool_false", "false"), Type: model.ParameterTypeBoolean}, - {ParameterEntry: testutil.NewParameterEntry("negative_int", "-123"), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("negative_float", "-45.67"), Type: model.ParameterTypeNumber}, - {ParameterEntry: testutil.NewParameterEntry("zero", "0"), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("empty_string", ""), Type: model.ParameterTypeString}, - {ParameterEntry: testutil.NewParameterEntry("mixed_string", "abc123"), Type: model.ParameterTypeString}, - }, - description: "类型推断的各种边界情况:布尔值变体、负数、零值、空字符串", - }, - { - name: "native_go_type_inference", - entries: []model.ParameterEntry{ - testutil.NewParameterEntry("native_int", int(42)), - testutil.NewParameterEntry("native_int32", int32(100)), - testutil.NewParameterEntry("native_int64", int64(999)), - testutil.NewParameterEntry("native_float32", float32(3.14)), - testutil.NewParameterEntry("native_float64", float64(2.718)), - testutil.NewParameterEntry("native_bool_true", true), - testutil.NewParameterEntry("native_bool_false", false), - testutil.NewParameterEntry("invalid_float", "1.2.3"), // 包含多个小数点 - testutil.NewParameterEntry("slice_type", []string{"a"}), // 非基础类型 - testutil.NewParameterEntry("map_type", map[string]int{"key": 1}), // 非基础类型 - }, - constraints: func() map[string]model.Parameter { - result := make(map[string]model.Parameter) - for _, name := range []string{"native_int", "native_int32", "native_int64", "native_float32", "native_float64", "native_bool_true", "native_bool_false", "invalid_float", "slice_type", "map_type"} { - result[name] = testutil.NewParameterConstraint(name).WithType("").Build() - } - return result - }(), - expected: []model.Parameter{ - {ParameterEntry: testutil.NewParameterEntry("native_int", int(42)), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("native_int32", int32(100)), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("native_int64", int64(999)), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("native_float32", float32(3.14)), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("native_float64", float64(2.718)), Type: model.ParameterTypeInteger}, - {ParameterEntry: testutil.NewParameterEntry("native_bool_true", true), Type: model.ParameterTypeBoolean}, - {ParameterEntry: testutil.NewParameterEntry("native_bool_false", false), Type: model.ParameterTypeBoolean}, - {ParameterEntry: testutil.NewParameterEntry("invalid_float", "1.2.3"), Type: model.ParameterTypeString}, // 无效浮点数回退为string - {ParameterEntry: testutil.NewParameterEntry("slice_type", []string{"a"}), Type: model.ParameterTypeString}, // 其他类型回退为string - {ParameterEntry: testutil.NewParameterEntry("map_type", map[string]int{"key": 1}), Type: model.ParameterTypeString}, // 其他类型回退为string - }, - description: "原生Go类型推断和边界情况:int、float、bool原生类型,以及无效值的处理", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := mergeEntriesAndConstraints(tt.entries, tt.constraints) - - assert.ElementsMatch(t, tt.expected, result, tt.description) - - assert.Len(t, result, len(tt.expected), "结果数量应匹配期望") - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/pod.go b/plugins/kb-adapter-rbdplugin/service/cluster/pod.go deleted file mode 100644 index 03012cbce..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/pod.go +++ /dev/null @@ -1,338 +0,0 @@ -package cluster - -import ( - "context" - "fmt" - "sort" - "strings" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// GetPodDetail 获取指定 Cluster 的 Pod detail -// 获取指定 service_id 的 Cluster 管理的指定 Pod 的详细信息 -func (s *Service) GetPodDetail(ctx context.Context, serviceID string, podName string) (*model.PodDetail, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, serviceID) - if err != nil { - return nil, fmt.Errorf("get cluster by service_id %s: %w", serviceID, err) - } - - pods, err := s.getClusterPods(ctx, cluster) - if err != nil { - return nil, fmt.Errorf("get cluster pods: %w", err) - } - - targetPod := findPodByName(pods, podName) - if targetPod == nil { - return nil, kbkit.ErrTargetNotFound - } - - pod := &corev1.Pod{} - if err := s.client.Get(ctx, client.ObjectKey{Name: podName, Namespace: cluster.Namespace}, pod); err != nil { - return nil, fmt.Errorf("get pod %s: %w", podName, err) - } - - var ( - componentName = pod.Labels["apps.kubeblocks.io/component-name"] - instanceSetName = pod.Labels["workloads.kubeblocks.io/instance"] - componentDef = "" - version = "" - ) - - // 同 instanceSet 获取 componentDef - if instanceSetName != "" { - var instanceSet workloadsv1.InstanceSet - if err := s.client.Get( - ctx, client.ObjectKey{ - Name: instanceSetName, - Namespace: cluster.Namespace, - }, &instanceSet); err != nil { - log.Warn("Failed to get instanceset for pod", - log.String("pod", podName), - log.String("instanceset", instanceSetName), - log.Err(err)) - } else { - if componentName == "" { - componentName = instanceSet.Labels["apps.kubeblocks.io/component-name"] - } - if v := instanceSet.Annotations["app.kubernetes.io/component"]; v != "" { - componentDef = v - } - if v := instanceSet.Annotations["apps.kubeblocks.io/service-version"]; v != "" { - version = v - } - } - } - - if componentName == "" { - return nil, fmt.Errorf("pod %s has no component name", podName) - } - - var spec *kbappsv1.ClusterComponentSpec - if componentName != "" { - spec = findComponentSpec(cluster, componentName) - } - if spec == nil { - return nil, fmt.Errorf("component spec %s not found in cluster %s", componentName, cluster.Name) - } - - if componentDef == "" { - componentDef = spec.ComponentDef - } - if version == "" { - if spec.ComponentDef != "" { - version = spec.ComponentDef - } else if spec.ServiceVersion != "" { - version = spec.ServiceVersion - } - } - - if componentDef == "" { - return nil, fmt.Errorf("component definition missing for component %s", componentName) - } - - status := buildPodDetailStatus(*pod) - containers := buildContainerDetails(pod.Spec.Containers, pod.Status.ContainerStatuses, componentDef, componentName) - events, err := getPodEventsByIndex(ctx, s.client, podName, pod.Namespace) - if err != nil { - log.Warn("Failed to get pod events", - log.String("pod", podName), - log.String("namespace", pod.Namespace), - log.Err(err)) - events = []model.PodEvent{} - } - - startTime := "" - if pod.Status.StartTime != nil { - startTime = formatToISO8601Time(pod.Status.StartTime.Time) - } - - podDetail := &model.PodDetail{ - Name: pod.Name, - NodeIP: pod.Status.HostIP, - StartTime: startTime, - IP: pod.Status.PodIP, - Version: version, - Namespace: pod.Namespace, - Status: status, - Containers: containers, - Events: events, - } - - log.Debug("get pod detail", - log.String("service_id", serviceID), - log.String("pod", podName), - log.Any("detail", podDetail)) - - return podDetail, nil -} - -// findPodByName 在 Pod 状态列表中查找指定名称的 Pod -func findPodByName(pods []model.Status, podName string) *model.Status { - for _, pod := range pods { - if pod.Name == podName { - return &pod - } - } - return nil -} - -func findComponentSpec(cluster *kbappsv1.Cluster, componentName string) *kbappsv1.ClusterComponentSpec { - if cluster == nil || componentName == "" { - return nil - } - for i := range cluster.Spec.ComponentSpecs { - if cluster.Spec.ComponentSpecs[i].Name == componentName { - return &cluster.Spec.ComponentSpecs[i] - } - } - return nil -} - -// buildPodDetailStatus 构建符合注释约定的 PodStatus(包含 type_str/reason/message/advice) -func buildPodDetailStatus(pod corev1.Pod) model.PodStatus { - typeStr := strings.ToLower(string(pod.Status.Phase)) - reason := "" - message := "" - advice := "" - - // 优先取 Waiting 的容器状态 - for _, cs := range pod.Status.ContainerStatuses { - if cs.State.Waiting != nil { - reason = cs.State.Waiting.Reason - message = cs.State.Waiting.Message - advice = deriveAdvice(reason, message) - break - } - } - // 其次取 Terminated 的容器状态 - if reason == "" { - for _, cs := range pod.Status.ContainerStatuses { - if cs.State.Terminated != nil { - reason = cs.State.Terminated.Reason - message = cs.State.Terminated.Message - advice = deriveAdvice(reason, message) - break - } - } - } - - return model.PodStatus{ - TypeStr: typeStr, - Reason: reason, - Message: message, - Advice: advice, - } -} - -// buildContainerDetails 构建容器详情列表,基于组件名称识别并返回主要工作容器 -func buildContainerDetails(containers []corev1.Container, containerStatuses []corev1.ContainerStatus, componentDef string, componentName string) []model.Container { - var details []model.Container - - statusMap := make(map[string]corev1.ContainerStatus) - for _, status := range containerStatuses { - statusMap[status.Name] = status - } - - for _, container := range containers { - if !isPrimaryContainer(container.Name, componentName) { - continue - } - - status, exists := statusMap[container.Name] - if !exists { - continue - } - - startedTime := "" - state := "Unknown" - reason := "" - - if status.State.Running != nil { - startedTime = formatToISO8601Time(status.State.Running.StartedAt.Time) - state = "Running" - } else if status.State.Waiting != nil { - state = "Waiting" - reason = status.State.Waiting.Reason - } else if status.State.Terminated != nil { - state = "Terminated" - reason = status.State.Terminated.Reason - } - - limitCPU := "" - if cpu := container.Resources.Limits.Cpu(); cpu != nil { - limitCPU = cpu.String() - } - - limitMemory := "" - if memory := container.Resources.Limits.Memory(); memory != nil { - limitMemory = memory.String() - } - - containerDetail := model.Container{ - ComponentDef: componentDef, - LimitMemory: limitMemory, - LimitCPU: limitCPU, - Started: startedTime, - State: state, - Reason: reason, - } - - details = append(details, containerDetail) - } - - return details -} - -// deriveAdvice 将常见的 reason 映射为建议性结论 -func deriveAdvice(reason, message string) string { - switch reason { - case "OOMKilled": - return "OutOfMemory" - case "ImagePullBackOff", "ErrImagePull": - return "ImagePullError" - default: - _ = message - return "" - } -} - -// getPodEventsByIndex 使用索引查询 Pod 相关的 Event -func getPodEventsByIndex(ctx context.Context, c client.Client, podName, namespace string) ([]model.PodEvent, error) { - var eventList corev1.EventList - - indexKey := fmt.Sprintf("%s/%s", namespace, podName) - if err := c.List(ctx, &eventList, client.MatchingFields{index.NamespacePodNameField: indexKey}); err != nil { - log.Warn("Index query for pod events failed", - log.String("indexKey", indexKey), - log.String("pod", podName), - log.String("namespace", namespace), - log.Err(err)) - return []model.PodEvent{}, nil - } - - return processEvents(eventList.Items), nil -} - -// processEvents 处理 Event 列表 -func processEvents(events []corev1.Event) []model.PodEvent { - // 按时间排序 - sort.Slice(events, func(i, j int) bool { - return events[i].FirstTimestamp.After(events[j].FirstTimestamp.Time) - }) - - // 限制返回数量 - const maxEvents = 10 - endIndex := len(events) - if endIndex > maxEvents { - endIndex = maxEvents - } - - result := make([]model.PodEvent, 0, endIndex) - for i := 0; i < endIndex; i++ { - event := events[i] - result = append(result, model.PodEvent{ - Type: event.Type, - Reason: event.Reason, - Age: formatAge(event.FirstTimestamp), - Message: event.Message, - }) - } - - return result -} - -// isPrimaryContainer 判断容器是否为主要业务容器 -// 基于 KubeBlocks component-name 标准进行判断 -func isPrimaryContainer(containerName, componentName string) bool { - return containerName == componentName -} - -// formatAge 将时间差格式化为人类可读的格式 (如 "5m", "2h", "3d") -func formatAge(eventTime metav1.Time) string { - if eventTime.IsZero() { - return "" - } - - duration := time.Since(eventTime.Time) - - if duration < time.Minute { - return fmt.Sprintf("%.0fs", duration.Seconds()) - } else if duration < time.Hour { - return fmt.Sprintf("%.0fm", duration.Minutes()) - } else if duration < 24*time.Hour { - return fmt.Sprintf("%.0fh", duration.Hours()) - } else { - return fmt.Sprintf("%.0fd", duration.Hours()/24) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go deleted file mode 100644 index ffa81c292..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go +++ /dev/null @@ -1,449 +0,0 @@ -package cluster - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - workloadsv1 "github.com/apecloud/kubeblocks/apis/workloads/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.pod-detail -func TestGetPodDetail(t *testing.T) { - type expectation struct { - wantErr error - errorMsg string - check func(t *testing.T, detail *model.PodDetail) - } - - tests := []struct { - name string - serviceID string - podName string - objects func() []client.Object - expectation expectation - }{ - { - name: "success_with_instanceset_metadata", - serviceID: "svc-success", - podName: "redis-sentinel-0", - objects: func() []client.Object { - componentName := "redis-sentinel" - componentDef := "redis-sentinel-7-1.0.0" - cluster := testutil.NewClusterBuilder("redis", testutil.TestNamespace). - WithServiceID("svc-success"). - WithComponent(componentName, componentDef). - WithComponentServiceVersion(componentName, "7.2.7"). - Build() - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-sentinel", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": componentName, - }, - Annotations: map[string]string{ - "app.kubernetes.io/component": componentDef, - "apps.kubeblocks.io/service-version": "7.2.7", - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "redis-sentinel-0"}}, - }, - } - - start := metav1.NewTime(time.Unix(1700000000, 0)) - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-sentinel-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": componentName, - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: componentName, - Resources: corev1.ResourceRequirements{ - Limits: testutil.Resources("1", "1Gi"), - }, - }, - { - Name: "sidecar", - }, - }, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - HostIP: "10.0.0.1", - PodIP: "10.0.0.2", - StartTime: &start, - ContainerStatuses: []corev1.ContainerStatus{ - { - Name: componentName, - State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: start}}, - }, - { - Name: "sidecar", - State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: start}}, - }, - }, - }, - } - - var events []client.Object - now := time.Now() - for i := 0; i < 12; i++ { - ts := metav1.NewTime(now.Add(time.Duration(-i) * time.Minute)) - events = append(events, &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "event" + fmt.Sprint(i), - Namespace: testutil.TestNamespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Name: pod.Name, - Namespace: pod.Namespace, - }, - FirstTimestamp: ts, - Type: "Normal", - Reason: fmt.Sprintf("reason-%d", i), - Message: fmt.Sprintf("message-%d", i), - }) - } - - objects := []client.Object{cluster, instanceSet, pod} - objects = append(objects, events...) - return objects - }, - expectation: expectation{ - check: func(t *testing.T, detail *model.PodDetail) { - require.NotNil(t, detail) - assert.Equal(t, "redis-sentinel-0", detail.Name) - assert.Equal(t, "10.0.0.1", detail.NodeIP) - assert.Equal(t, "10.0.0.2", detail.IP) - assert.Equal(t, "7.2.7", detail.Version) - require.Len(t, detail.Containers, 1) - container := detail.Containers[0] - assert.Equal(t, "redis-sentinel-7-1.0.0", container.ComponentDef) - assert.Equal(t, "1Gi", container.LimitMemory) - assert.Equal(t, "1", container.LimitCPU) - assert.Equal(t, "Running", container.State) - require.Len(t, detail.Events, 10) - assert.Equal(t, "reason-0", detail.Events[0].Reason) - assert.Equal(t, "message-0", detail.Events[0].Message) - assert.Equal(t, "reason-9", detail.Events[9].Reason) - assert.NotEmpty(t, detail.Events[0].Age) - }, - }, - }, - { - name: "success_fallback_to_spec_service_version", - serviceID: "svc-fallback-version", - podName: "pg-0", - objects: func() []client.Object { - componentName := "postgresql" - cluster := testutil.NewClusterBuilder("pg", testutil.TestNamespace). - WithServiceID("svc-fallback-version"). - WithComponent(componentName, ""). - WithComponentServiceVersion(componentName, "14.6"). - Build() - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pg", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": componentName, - }, - Annotations: map[string]string{ - "app.kubernetes.io/component": "postgresql-14-def", - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "pg-0"}}, - }, - } - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pg-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": componentName, - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Name: componentName}}, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodPending, - ContainerStatuses: []corev1.ContainerStatus{{ - Name: componentName, - State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{ - Reason: "ImagePullBackOff", - Message: "pulling image", - }}, - }}, - }, - } - - event := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pg-event", - Namespace: testutil.TestNamespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Name: pod.Name, - Namespace: pod.Namespace, - }, - Message: "pod waiting", - } - - return []client.Object{cluster, instanceSet, pod, event} - }, - expectation: expectation{ - check: func(t *testing.T, detail *model.PodDetail) { - require.NotNil(t, detail) - assert.Equal(t, "14.6", detail.Version) - assert.Equal(t, "pending", detail.Status.TypeStr) - assert.Equal(t, "ImagePullBackOff", detail.Status.Reason) - assert.Equal(t, "pulling image", detail.Status.Message) - assert.Equal(t, "ImagePullError", detail.Status.Advice) - require.Len(t, detail.Events, 1) - assert.Empty(t, detail.Events[0].Age) - }, - }, - }, - { - name: "success_component_name_from_instanceset", - serviceID: "svc-component-name", - podName: "mysql-0", - objects: func() []client.Object { - componentName := "mysql" - cluster := testutil.NewClusterBuilder("mysql", testutil.TestNamespace). - WithServiceID("svc-component-name"). - WithComponent(componentName, "mysql-8.0"). - Build() - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mysql", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": componentName, - }, - Annotations: map[string]string{ - "app.kubernetes.io/component": componentName + "-def", - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "mysql-0"}}, - }, - } - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "mysql-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Name: componentName}}, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - ContainerStatuses: []corev1.ContainerStatus{{ - Name: componentName, - State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.Now()}}, - }}, - }, - } - - return []client.Object{cluster, instanceSet, pod} - }, - expectation: expectation{ - check: func(t *testing.T, detail *model.PodDetail) { - require.NotNil(t, detail) - assert.Equal(t, "mysql-8.0", detail.Version) - require.Len(t, detail.Containers, 1) - assert.Equal(t, "mysql-def", detail.Containers[0].ComponentDef) - }, - }, - }, - { - name: "target_pod_not_found", - serviceID: "svc-missing-pod", - podName: "missing", - objects: func() []client.Object { - componentName := "redis" - cluster := testutil.NewClusterBuilder("rediscluster", testutil.TestNamespace). - WithServiceID("svc-missing-pod"). - WithComponent(componentName, "redis-def"). - Build() - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": componentName, - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "redis-0"}}, - }, - } - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-0", - Namespace: testutil.TestNamespace, - }, - } - - return []client.Object{cluster, instanceSet, pod} - }, - expectation: expectation{wantErr: kbkit.ErrTargetNotFound}, - }, - { - name: "missing_component_spec_error", - serviceID: "svc-no-spec", - podName: "rogue-0", - objects: func() []client.Object { - cluster := testutil.NewClusterBuilder("rogue", testutil.TestNamespace). - WithServiceID("svc-no-spec"). - WithComponent("valid", "valid-def"). - Build() - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "valid", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": "valid", - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "rogue-0"}}, - }, - } - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "rogue-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": "rogue", - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "rogue"}}}, - Status: corev1.PodStatus{ - ContainerStatuses: []corev1.ContainerStatus{{Name: "rogue"}}, - }, - } - - return []client.Object{cluster, instanceSet, pod} - }, - expectation: expectation{errorMsg: "component spec rogue not found"}, - }, - { - name: "missing_component_definition_error", - serviceID: "svc-no-def", - podName: "node-0", - objects: func() []client.Object { - componentName := "node" - cluster := testutil.NewClusterBuilder("node", testutil.TestNamespace). - WithServiceID("svc-no-def"). - WithComponent(componentName, ""). - Build() - - // 清空 serviceVersion - for i := range cluster.Spec.ComponentSpecs { - cluster.Spec.ComponentSpecs[i].ServiceVersion = "" - } - - instanceSet := &workloadsv1.InstanceSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "node", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "app.kubernetes.io/instance": cluster.Name, - "apps.kubeblocks.io/component-name": componentName, - }, - }, - Status: workloadsv1.InstanceSetStatus{ - InstanceStatus: []workloadsv1.InstanceStatus{{PodName: "node-0"}}, - }, - } - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "node-0", - Namespace: testutil.TestNamespace, - Labels: map[string]string{ - "apps.kubeblocks.io/component-name": componentName, - "workloads.kubeblocks.io/instance": instanceSet.Name, - }, - }, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: componentName}}}, - Status: corev1.PodStatus{ - ContainerStatuses: []corev1.ContainerStatus{{Name: componentName}}, - }, - } - - return []client.Object{cluster, instanceSet, pod} - }, - expectation: expectation{errorMsg: "component definition missing"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - objects := tt.objects() - client := testutil.NewFakeClientWithIndexes(objects...) - - svc := &Service{client: client} - detail, err := svc.GetPodDetail(context.Background(), tt.serviceID, tt.podName) - - if tt.expectation.wantErr != nil || tt.expectation.errorMsg != "" { - require.Error(t, err) - if tt.expectation.wantErr != nil { - assert.ErrorIs(t, err, tt.expectation.wantErr) - } - if tt.expectation.errorMsg != "" { - assert.Contains(t, err.Error(), tt.expectation.errorMsg) - } - return - } - - require.NoError(t, err) - if tt.expectation.check != nil { - tt.expectation.check(t, detail) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/restore.go b/plugins/kb-adapter-rbdplugin/service/cluster/restore.go deleted file mode 100644 index 3fbe0fb39..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/restore.go +++ /dev/null @@ -1,218 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// RestoreFromBackup 从用户通过 backupName 指定的备份中 restore cluster, -// 返回 restored cluster 的名称 + clusterDef, 用于 Rainbond 更新 KubeBlocks Component 信息 -// -// 该方法将为恢复的 cluster 通过 newServiceID 绑定到一个新的 KubeBlocks Component 中 -func (s *Service) RestoreFromBackup(ctx context.Context, oldServiceID, newServiceID, backupName string) (string, error) { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, oldServiceID) - if err != nil { - return "", fmt.Errorf("get cluster by service_id: %w", err) - } - - log.Debug("starting cluster restore from backup", - log.String("backup_name", backupName), - log.String("service_id", oldServiceID), - log.String("old_cluster", cluster.Name), - ) - - // 创建 Restore OpsRequest - ops, err := kbkit.CreateRestoreOpsRequest(ctx, s.client, cluster, backupName) - if err != nil { - return "", fmt.Errorf("create restore opsrequest: %w", err) - } - - log.Debug("restore opsrequest created, waiting for restored cluster", - log.String("ops_request", ops.Name), - log.String("new_cluster_name", ops.Spec.ClusterName), - ) - - // 等待新 cluster 创建,同时监控 OpsRequest 状态 - newCluster, err := s.waitForRestoredCluster(ctx, ops, cluster.Name) - if err != nil { - return "", fmt.Errorf("wait for restored cluster: %w", err) - } - - // 为新 cluster 添加 service_id 标签,建立与 KubeBlocks Component 的关联 - if err := s.associateToKubeBlocksComponent(ctx, newCluster, newServiceID); err != nil { - return "", fmt.Errorf("associate cluster to kubeblocks component: %w", err) - } - - log.Debug("cluster restore from backup completed successfully", - log.String("old_cluster", cluster.Name), - log.String("backup_name", backupName), - log.String("new_cluster", newCluster.Name), - ) - - return fmt.Sprintf("%s-%s", newCluster.Name, newCluster.Spec.ClusterDef), nil -} - -// waitForRestoredCluster 等待由 Restore OpsRequest 创建的新 cluster 出现在集群中 -// -// 该函数会轮询检查新 cluster 是否存在,超时时间为 20 秒 -// 同时监控 OpsRequest 状态,如果 OpsRequest 失败则立即退出。 -func (s *Service) waitForRestoredCluster(ctx context.Context, ops *opsv1alpha1.OpsRequest, oldClusterName string) (*kbappsv1.Cluster, error) { - newClusterName := ops.Spec.ClusterName - namespace := ops.Namespace - - log.Debug("waiting for restored cluster to be created", - log.String("new_cluster", newClusterName), - log.String("namespace", namespace), - log.String("ops_request", ops.Name), - ) - - // 20 秒超时 - timeoutCtx, cancel := context.WithTimeout(ctx, 20*time.Second) - defer cancel() - - var newCluster *kbappsv1.Cluster - err := wait.PollUntilContextCancel(timeoutCtx, 500*time.Millisecond, true, func(ctx context.Context) (bool, error) { - // 检查 OpsRequest 状态,如果失败则立即退出 - var latestOps opsv1alpha1.OpsRequest - if err := s.client.Get(ctx, client.ObjectKey{ - Name: ops.Name, - Namespace: ops.Namespace, - }, &latestOps); err != nil { - log.Debug("failed to get opsrequest status", log.Err(err)) - return false, nil - } - - // 检查 OpsRequest 是否失败 - if latestOps.Status.Phase == opsv1alpha1.OpsFailedPhase || - latestOps.Status.Phase == opsv1alpha1.OpsCancelledPhase || - latestOps.Status.Phase == opsv1alpha1.OpsAbortedPhase { - - // 处理失败的 OpsRequest, 将失败的 Ops 标记为旧 cluster 所属 - if handleErr := s.handleFailedRestoreOps(ctx, &latestOps, oldClusterName); handleErr != nil { - log.Error("failed to handle failed restore kbkit", log.Err(handleErr)) - } - return false, fmt.Errorf("restore opsrequest failed with phase: %s", latestOps.Status.Phase) - } - - // 检查新 cluster 是否存在 - var cluster kbappsv1.Cluster - if err := s.client.Get(ctx, client.ObjectKey{ - Name: newClusterName, - Namespace: namespace, - }, &cluster); err != nil { - log.Debug("restored cluster not found yet, continuing to wait", - log.String("cluster", newClusterName), - log.String("namespace", namespace), - ) - return false, nil - } - - log.Debug("restored cluster found", - log.String("cluster", cluster.Name), - log.String("namespace", cluster.Namespace), - ) - newCluster = &cluster - return true, nil - }) - - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - // 超时时清理 OpsRequest - log.Warn("timeout detected, cleaning up opsrequest", - log.String("ops_request", ops.Name), - log.String("new_cluster", newClusterName), - log.String("namespace", namespace), - ) - if cleanupErr := s.cleanupOpsRequest(ctx, ops, "timeout"); cleanupErr != nil { - log.Error("failed to cleanup timed out opsrequest", - log.String("ops_request", ops.Name), - log.Err(cleanupErr)) - } - return nil, fmt.Errorf("timeout waiting for restored cluster %s/%s to be created", namespace, newClusterName) - } - return nil, fmt.Errorf("error waiting for restored cluster: %w", err) - } - - log.Info("restored cluster successfully created and found", - log.String("cluster", newCluster.Name), - log.String("namespace", newCluster.Namespace), - ) - - return newCluster, nil -} - -// handleFailedRestoreOps 处理失败的 Restore OpsRequest -// -// 修改失败的 OpsRequest 的 app.kubernetes.io/instance 标签值为旧 cluster 名称 -func (s *Service) handleFailedRestoreOps(ctx context.Context, ops *opsv1alpha1.OpsRequest, oldClusterName string) error { - log.Debug("handling failed restore opsrequest", - log.String("ops_request", ops.Name), - log.String("old_cluster", oldClusterName), - ) - - patchData := fmt.Sprintf(`{ - "metadata": { - "labels": { - "%s": "%s" - } - } - }`, constant.AppInstanceLabelKey, oldClusterName) - - if err := s.client.Patch(ctx, &opsv1alpha1.OpsRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: ops.Name, - Namespace: ops.Namespace, - }, - }, client.RawPatch(types.MergePatchType, []byte(patchData))); err != nil { - return fmt.Errorf("patch failed restore opsrequest %s/%s app instance label: %w", ops.Namespace, ops.Name, err) - } - - log.Debug("updated failed restore opsrequest app instance label", - log.String("ops_request", ops.Name), - log.String("old_cluster", oldClusterName), - ) - - return nil -} - -// cleanupOpsRequest 清理指定的 OpsRequest -// -// 用于清理超时的 OpsRequest,防止资源泄漏和状态不一致 -func (s *Service) cleanupOpsRequest(ctx context.Context, ops *opsv1alpha1.OpsRequest, reason string) error { - log.Debug("cleaning up opsrequest", - log.String("ops_request", ops.Name), - log.String("namespace", ops.Namespace), - log.String("reason", reason), - ) - - // 删除 OpsRequest - if err := s.client.Delete(ctx, &opsv1alpha1.OpsRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: ops.Name, - Namespace: ops.Namespace, - }, - }); err != nil { - return fmt.Errorf("delete opsrequest %s/%s: %w", ops.Namespace, ops.Name, err) - } - - log.Debug("successfully cleaned up opsrequest", - log.String("ops_request", ops.Name), - log.String("namespace", ops.Namespace), - log.String("reason", reason), - ) - - return nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go deleted file mode 100644 index 91cd3ec18..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.restore-from-backup -func TestRestoreFromBackup(t *testing.T) { - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - oldServiceID string - newServiceID string - backupName string - expectErr bool - errContains string - }{ - { - name: "source_cluster_not_found", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { return nil }, // 不创建任何集群 - oldServiceID: "nonexistent-service", - newServiceID: "new-service", - backupName: "test-backup", - expectErr: true, - errContains: "get cluster by service_id", - }, - { - name: "get_cluster_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list clusters failed")). - Build() - }, - oldServiceID: "any-service", - newServiceID: "new-service", - backupName: "test-backup", - expectErr: true, - errContains: "get cluster by service_id", - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - k8sClient := tt.clientSetup() - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - service := NewService(k8sClient) - - result, err := service.RestoreFromBackup(ctx, tt.oldServiceID, tt.newServiceID, tt.backupName) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - assert.NotEmpty(t, result) - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.restore-from-backup -func TestWaitForRestoredCluster(t *testing.T) { - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - opsPhase opsv1alpha1.OpsPhase - createCluster bool - expectErr bool - expectTimeout bool - expectClusterName string - verify func(*testing.T, client.Client) - }{ - { - name: "cluster_created_successfully", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - opsPhase: opsv1alpha1.OpsSucceedPhase, - createCluster: true, - expectClusterName: "restore-cluster-xyz", - }, - { - name: "ops_failed_calls_handler", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - opsPhase: opsv1alpha1.OpsFailedPhase, - expectErr: true, - verify: func(t *testing.T, c client.Client) { - // 验证失败的 OpsRequest 被正确处理 - verifyOpsRequestLabelUpdated(t, c, "restore-ops", testutil.TestNamespace, "old-cluster") - }, - }, - { - name: "ops_cancelled_calls_handler", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - opsPhase: opsv1alpha1.OpsCancelledPhase, - expectErr: true, - verify: func(t *testing.T, c client.Client) { - verifyOpsRequestLabelUpdated(t, c, "restore-ops", testutil.TestNamespace, "old-cluster") - }, - }, - { - name: "ops_aborted_calls_handler", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - opsPhase: opsv1alpha1.OpsAbortedPhase, - expectErr: true, - verify: func(t *testing.T, c client.Client) { - verifyOpsRequestLabelUpdated(t, c, "restore-ops", testutil.TestNamespace, "old-cluster") - }, - }, - { - name: "timeout_calls_cleanup", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - opsPhase: opsv1alpha1.OpsRunningPhase, - expectErr: true, - expectTimeout: true, - verify: func(t *testing.T, c client.Client) { - // 验证超时后 OpsRequest 被清理 - verifyOpsRequestDeleted(t, c, "restore-ops", testutil.TestNamespace) - }, - }, - { - name: "get_ops_status_error_continues", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithGetError(errors.New("get ops failed")). - Build() - }, - opsPhase: opsv1alpha1.OpsRunningPhase, - expectErr: true, - expectTimeout: true, - }, - { - name: "general_polling_error", - clientSetup: func() client.Client { - // 创建一个会导致 poll 函数返回非超时错误的客户端 - return testutil.NewErrorClientBuilder(). - WithGetError(errors.New("unexpected polling error")). - Build() - }, - opsPhase: opsv1alpha1.OpsRunningPhase, - expectErr: true, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - k8sClient := tt.clientSetup() - - objects := []client.Object{} - - restoreOps := testutil.NewOpsRequestBuilder("restore-ops", testutil.TestNamespace). - WithClusterName("restore-cluster-xyz"). - WithType(opsv1alpha1.RestoreType). - WithRestore("test-backup"). - WithPhase(tt.opsPhase). - WithInstanceLabel("restore-cluster-xyz"). - Build() - objects = append(objects, restoreOps) - - if tt.createCluster { - newCluster := testutil.NewMySQLCluster("restore-cluster-xyz", testutil.TestNamespace).Build() - objects = append(objects, newCluster) - } - - require.NoError(t, testutil.CreateObjects(ctx, k8sClient, objects)) - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - service := NewService(k8sClient) - - testCtx := ctx - if tt.expectTimeout { - var cancel context.CancelFunc - testCtx, cancel = context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() - } - - cluster, err := service.waitForRestoredCluster(testCtx, restoreOps, "old-cluster") - - if tt.expectErr { - require.Error(t, err) - if tt.expectTimeout { - assert.Contains(t, err.Error(), "timeout") - } - if tt.verify != nil { - tt.verify(t, k8sClient) - } - return - } - - require.NoError(t, err) - require.NotNil(t, cluster) - if tt.expectClusterName != "" { - assert.Equal(t, tt.expectClusterName, cluster.Name) - } - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.restore-from-backup -func TestHandleFailedRestoreOps(t *testing.T) { - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - oldCluster string - expectErr bool - verify func(*testing.T, client.Client, *opsv1alpha1.OpsRequest) - }{ - { - name: "patch_label_success", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - oldCluster: "source-cluster", - verify: func(t *testing.T, c client.Client, ops *opsv1alpha1.OpsRequest) { - verifyOpsRequestLabelUpdated(t, c, ops.Name, ops.Namespace, "source-cluster") - }, - }, - { - name: "patch_operation_failure", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithPatchError(errors.New("patch failed")). - Build() - }, - oldCluster: "source-cluster", - expectErr: true, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - k8sClient := tt.clientSetup() - - ops := testutil.NewOpsRequestBuilder("failed-restore", testutil.TestNamespace). - WithClusterName("restore-cluster-xyz"). - WithType(opsv1alpha1.RestoreType). - WithPhase(opsv1alpha1.OpsFailedPhase). - WithInstanceLabel("restore-cluster-xyz"). - Build() - - require.NoError(t, testutil.CreateObjects(ctx, k8sClient, []client.Object{ops})) - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - service := NewService(k8sClient) - - err := service.handleFailedRestoreOps(ctx, ops, tt.oldCluster) - - if tt.expectErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient, ops) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.cluster.restore-from-backup -func TestCleanupOpsRequest(t *testing.T) { - testCases := []struct { - name string - clientSetup func() client.Client - createOps bool - reason string - expectErr bool - verify func(*testing.T, client.Client) - }{ - { - name: "delete_success", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - createOps: true, - reason: "timeout", - verify: func(t *testing.T, c client.Client) { - verifyOpsRequestDeleted(t, c, "cleanup-ops", testutil.TestNamespace) - }, - }, - { - name: "delete_operation_failure", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithDeleteError(errors.New("delete failed")). - Build() - }, - createOps: true, - reason: "timeout", - expectErr: true, - }, - { - name: "resource_not_found_error", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - createOps: false, - reason: "timeout", - expectErr: true, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - k8sClient := tt.clientSetup() - - ops := testutil.NewOpsRequestBuilder("cleanup-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.RestoreType). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - - if tt.createOps { - require.NoError(t, testutil.CreateObjects(ctx, k8sClient, []client.Object{ops})) - } - - service := NewService(k8sClient) - - err := service.cleanupOpsRequest(ctx, ops, tt.reason) - - if tt.expectErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// verifyOpsRequestDeleted 验证 OpsRequest 是否被正确删除 -func verifyOpsRequestDeleted(t *testing.T, c client.Client, opsName, namespace string) { - ctx := context.Background() - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{ - Name: opsName, - Namespace: namespace, - }, ops) - assert.True(t, client.IgnoreNotFound(err) == nil, "OpsRequest should be deleted") -} - -// verifyOpsRequestLabelUpdated 验证 OpsRequest 的 app.kubernetes.io/instance 标签被正确更新 -func verifyOpsRequestLabelUpdated(t *testing.T, c client.Client, opsName, namespace, expectedClusterName string) { - ctx := context.Background() - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{ - Name: opsName, - Namespace: namespace, - }, ops) - require.NoError(t, err, "failed to get OpsRequest") - - if assert.NotNil(t, ops.Labels, "OpsRequest should have labels") { - assert.Equal(t, expectedClusterName, ops.Labels[constant.AppInstanceLabelKey], - "OpsRequest should have updated app instance label") - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/scaling.go b/plugins/kb-adapter-rbdplugin/service/cluster/scaling.go deleted file mode 100644 index 29aacee02..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/scaling.go +++ /dev/null @@ -1,310 +0,0 @@ -package cluster - -import ( - "context" - "errors" - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - corev1 "k8s.io/api/core/v1" - storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// ExpansionCluster 对 Cluster 进行伸缩操作 -// -// 使用 opsrequest 将 Cluster 的资源规格进行伸缩,使其变为 model.ExpansionInput 的期望状态 -func (s *Service) ExpansionCluster(ctx context.Context, expansion model.ExpansionInput) error { - log.Debug("Expansion", - log.String("service_id", expansion.ServiceID), - log.Any("expansion", expansion), - ) - - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, expansion.ServiceID) - if err != nil { - return err - } - - // 禁止对停止或停止中的 Cluster 进行伸缩 - if cluster.Status.Phase == kbappsv1.StoppedClusterPhase || cluster.Status.Phase == kbappsv1.StoppingClusterPhase { - return fmt.Errorf("cluster %s/%s is not running", cluster.Namespace, cluster.Name) - } - - if len(cluster.Spec.ComponentSpecs) == 0 { - return fmt.Errorf("cluster %s/%s has no componentSpecs", cluster.Namespace, cluster.Name) - } - - // 解析期望的资源配置 - desiredResources, err := expansion.ParseResources() - if err != nil { - return fmt.Errorf("parse desired resources: %w", err) - } - - // 为各个组件构建当前状态 - // 用于记录所有组件的伸缩操作的上下文 - var components = make( - map[model.ComponentName]model.ComponentExpansionContext, - len(cluster.Spec.ComponentSpecs), - ) - - // 为每个组件分别构建完整的伸缩上下文 - for _, spec := range cluster.Spec.ComponentSpecs { - componentName := spec.Name - if componentName == "" { - componentName = cluster.Spec.ClusterDef - } - - // 构建当前组件的 CPU 和内存状态 - currentCPU := spec.Resources.Limits.Cpu() - currentMem := spec.Resources.Limits.Memory() - - // 构建当前组件的存储状态 - var ( - hasPVC = len(spec.VolumeClaimTemplates) > 0 - volumeTplName string - currentStorage resource.Quantity - storageClassRef *string - ) - if hasPVC { - volumeTpl := spec.VolumeClaimTemplates[0] - volumeTplName = volumeTpl.Name - if size, ok := volumeTpl.Spec.Resources.Requests[corev1.ResourceStorage]; ok { - currentStorage = size - } - storageClassRef = volumeTpl.Spec.StorageClassName - } - - // 构建完整的组件伸缩上下文 - components[model.ComponentName(componentName)] = model.ComponentExpansionContext{ - // 水平伸缩 - CurrentReplicas: spec.Replicas, - DesiredReplicas: expansion.Replicas, - // 垂直伸缩 - CurrentCPU: *currentCPU, - CurrentMem: *currentMem, - DesiredCPU: desiredResources.CPU, - DesiredMem: desiredResources.Memory, - // 存储扩容 - HasPVC: hasPVC, - VolumeTplName: volumeTplName, - CurrentStorage: currentStorage, - DesiredStorage: desiredResources.Storage, - StorageClassRef: storageClassRef, - } - } - - var opsCreated bool - - expansionCtx := model.ExpansionContext{ - Cluster: cluster, - Components: components, // 传递所有组件的完整状态 - } - - hCreated, err := s.handleHorizontalScaling(ctx, expansionCtx) - if err != nil { - return fmt.Errorf("horizontal scaling: %w", err) - } - opsCreated = opsCreated || hCreated - - vCreated, err := s.handleVerticalScaling(ctx, expansionCtx) - if err != nil { - return fmt.Errorf("vertical scaling: %w", err) - } - opsCreated = opsCreated || vCreated - - sCreated, err := s.handleVolumeExpansion(ctx, expansionCtx) - if err != nil { - return fmt.Errorf("volume expansion: %w", err) - } - opsCreated = opsCreated || sCreated - - if !opsCreated { - log.Info("No expansion needed, cluster already matches desired spec", - log.String("cluster", cluster.Name), - log.String("service_id", expansion.ServiceID)) - } - - return nil -} - -// handleHorizontalScaling 处理水平伸缩(副本数) -func (s *Service) handleHorizontalScaling(ctx context.Context, expansionCtx model.ExpansionContext) (bool, error) { - // 检查是否有任何组件需要水平伸缩 - var needHScaling bool - var components []model.ComponentHorizontalScaling - - for componentName, componentCtx := range expansionCtx.Components { - if componentCtx.DesiredReplicas != componentCtx.CurrentReplicas { - needHScaling = true - delta := componentCtx.DesiredReplicas - componentCtx.CurrentReplicas - components = append(components, model.ComponentHorizontalScaling{ - Name: string(componentName), - DeltaReplicas: delta, - }) - } - } - - if !needHScaling { - return false, nil - } - - opsParams := model.HorizontalScalingOpsParams{ - Cluster: expansionCtx.Cluster, - Components: components, - } - - if err := kbkit.CreateHorizontalScalingOpsRequest(ctx, s.client, opsParams); err != nil { - if errors.Is(err, kbkit.ErrCreateOpsSkipped) { - return false, nil - } - return false, fmt.Errorf("create horizontal scaling opsrequest: %w", err) - } - - log.Info("Created horizontal scaling OpsRequest for multiple components", - log.String("cluster", expansionCtx.Cluster.Name), - log.Any("components", components)) - - return true, nil -} - -// handleVerticalScaling 处理垂直伸缩(CPU/内存) -func (s *Service) handleVerticalScaling(ctx context.Context, expansionCtx model.ExpansionContext) (bool, error) { - // 检查是否有任何组件需要垂直伸缩 - var needVScaling bool - var components []model.ComponentVerticalScaling - - for componentName, componentCtx := range expansionCtx.Components { - needCPUScale := componentCtx.CurrentCPU.Cmp(componentCtx.DesiredCPU) != 0 - needMemScale := componentCtx.CurrentMem.Cmp(componentCtx.DesiredMem) != 0 - - if needCPUScale || needMemScale { - needVScaling = true - components = append(components, model.ComponentVerticalScaling{ - Name: string(componentName), - CPU: componentCtx.DesiredCPU, - Memory: componentCtx.DesiredMem, - }) - } - } - - if !needVScaling { - return false, nil - } - - opsParams := model.VerticalScalingOpsParams{ - Cluster: expansionCtx.Cluster, - Components: components, - } - - if err := kbkit.CreateVerticalScalingOpsRequest(ctx, s.client, opsParams); err != nil { - if errors.Is(err, kbkit.ErrCreateOpsSkipped) { - return false, nil - } - return false, fmt.Errorf("create vertical scaling opsrequest: %w", err) - } - - log.Info("Created vertical scaling OpsRequest for multiple components", - log.String("cluster", expansionCtx.Cluster.Name), - log.Any("components", components)) - - return true, nil -} - -// handleVolumeExpansion 处理存储扩容 -func (s *Service) handleVolumeExpansion(ctx context.Context, expansionCtx model.ExpansionContext) (bool, error) { - // 检查是否有任何组件需要存储扩容 - var needVolumeExpansion bool - var components []model.ComponentVolumeExpansion - - for componentName, componentCtx := range expansionCtx.Components { - // 如果该组件没有 PVC,跳过 - if !componentCtx.HasPVC { - continue - } - - switch componentCtx.DesiredStorage.Cmp(componentCtx.CurrentStorage) { - case 0: - // 存储大小相同,无需扩容 - continue - case -1: - // 存储缩容,记录警告但不处理 - log.Warn("Storage shrinking detected but not supported, skipping component", - log.String("cluster", expansionCtx.Cluster.Name), - log.String("component", string(componentName)), - log.String("volumeTemplate", componentCtx.VolumeTplName), - log.String("currentStorage", componentCtx.CurrentStorage.String()), - log.String("desiredStorage", componentCtx.DesiredStorage.String())) - continue - case 1: - // 需要存储扩容,先验证存储类 - canExpand := true - var skipReason string - - if componentCtx.StorageClassRef == nil || *componentCtx.StorageClassRef == "" { - canExpand = false - skipReason = "storageClass not set on volumeClaimTemplate" - } else { - var sc storagev1.StorageClass - if err := s.client.Get(ctx, client.ObjectKey{Name: *componentCtx.StorageClassRef}, &sc); err != nil { - log.Warn("Failed to get StorageClass, skipping component volume expansion", - log.String("cluster", expansionCtx.Cluster.Name), - log.String("component", string(componentName)), - log.String("volumeTemplate", componentCtx.VolumeTplName), - log.String("storageClass", *componentCtx.StorageClassRef), - log.String("error", err.Error())) - canExpand = false - skipReason = "failed to get StorageClass" - } else if sc.AllowVolumeExpansion == nil || !*sc.AllowVolumeExpansion { - canExpand = false - skipReason = "StorageClass does not allow volume expansion" - } - } - - if !canExpand { - log.Warn("Volume expansion skipped due to configuration constraints", - log.String("cluster", expansionCtx.Cluster.Name), - log.String("component", string(componentName)), - log.String("volumeTemplate", componentCtx.VolumeTplName), - log.String("reason", skipReason), - log.String("currentStorage", componentCtx.CurrentStorage.String()), - log.String("desiredStorage", componentCtx.DesiredStorage.String())) - continue - } - - // 添加到需要扩容的组件列表 - needVolumeExpansion = true - components = append(components, model.ComponentVolumeExpansion{ - Name: string(componentName), - VolumeClaimTemplateName: componentCtx.VolumeTplName, - Storage: componentCtx.DesiredStorage, - }) - } - } - - if !needVolumeExpansion { - return false, nil - } - - opsParams := model.VolumeExpansionOpsParams{ - Cluster: expansionCtx.Cluster, - Components: components, - } - - if err := kbkit.CreateVolumeExpansionOpsRequest(ctx, s.client, opsParams); err != nil { - if errors.Is(err, kbkit.ErrCreateOpsSkipped) { - return false, nil - } - return false, fmt.Errorf("create volume expansion opsrequest: %w", err) - } - - log.Info("Created volume expansion OpsRequest for multiple components", - log.String("cluster", expansionCtx.Cluster.Name), - log.Any("components", components)) - - return true, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go b/plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go deleted file mode 100644 index 1e4477ee1..000000000 --- a/plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go +++ /dev/null @@ -1,925 +0,0 @@ -package cluster_test - -import ( - "context" - "errors" - "testing" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/cluster" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/resource" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.cluster.scale -func TestExpansionCluster(t *testing.T) { - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - input model.ExpansionInput - expectErr bool - errContains string - verify func(*testing.T, client.Client) - }{ - { - name: "cluster_not_found", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { return nil }, // 不创建任何集群 - input: newExpansionInput("nonexistent-service", "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "resource not found", - }, - { - name: "multiple_clusters_found", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("cluster1", testutil.TestNamespace). - WithServiceID("duplicate-service"). - Build(), - testutil.NewMySQLCluster("cluster2", testutil.TestNamespace). - WithServiceID("duplicate-service"). - Build(), - }) - }, - input: newExpansionInput("duplicate-service", "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "multiple resources found", - }, - { - name: "cluster_stopped", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithPhase(kbappsv1.StoppedClusterPhase). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "is not running", - }, - { - name: "cluster_stopping", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithPhase(kbappsv1.StoppingClusterPhase). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "is not running", - }, - { - name: "no_component_specs", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewClusterBuilder("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "has no componentSpecs", - }, - { - name: "invalid_resource_parsing", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "invalid-cpu", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "parse desired resources", - }, - - // 组件上下文构建测试 - { - name: "single_component_expansion", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - verify: verifyVerticalScalingCreated, - }, - { - name: "empty_component_name_uses_clusterdef", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewClusterBuilder("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithClusterDef("mysql"). - WithComponent("", "mysql-8.0"). - WithComponentResources("", - testutil.Resources("250m", "500Mi"), - testutil.Resources("250m", "500Mi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - verify: verifyVerticalScalingCreated, - }, - - // 扩容操作组合测试 - { - name: "no_expansion_needed", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("mysql", 2). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - verify: verifyNoOpsRequestCreated, - }, - { - name: "horizontal_scale_up", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("mysql", 1). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), - verify: verifyHorizontalScalingCreated, - }, - { - name: "horizontal_scale_down", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("mysql", 3). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 1), - verify: verifyHorizontalScalingCreated, - }, - { - name: "vertical_cpu_only", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources( - "mysql", - testutil.Resources("250m", "1Gi"), - testutil.Resources("250m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 1), - verify: verifyVerticalScalingCreated, - }, - { - name: "vertical_memory_only", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources( - "mysql", - testutil.Resources("500m", "512Mi"), - testutil.Resources("500m", "512Mi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 1), - verify: verifyVerticalScalingCreated, - }, - { - name: "horizontal_and_vertical", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("mysql", 1). - WithComponentResources( - "mysql", - testutil.Resources("250m", "512Mi"), - testutil.Resources("250m", "512Mi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), - verify: verifyBothHorizontalAndVerticalScalingCreated, - }, - - // 存储扩容专项测试 - { - name: "volume_expansion_no_pvc", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "20Gi", 1), - verify: verifyNoVolumeExpansionCreated, - }, - { - name: "volume_expansion_enabled", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - // 创建支持扩容的 StorageClass - testutil.NewStorageClassBuilder("expandable-storage"). - WithAllowVolumeExpansion(true). - Build(), - - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("mysql", "data", "expandable-storage", "10Gi"). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "20Gi", 1), - verify: verifyVolumeExpansionCreated, - }, - { - name: "volume_expansion_disabled", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - // 创建不支持扩容的 StorageClass - testutil.NewStorageClassBuilder("non-expandable-storage"). - WithAllowVolumeExpansion(false). - Build(), - - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("mysql", "data", "non-expandable-storage", "10Gi"). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "20Gi", 1), - verify: verifyNoVolumeExpansionCreated, // 应该跳过存储扩容 - }, - { - name: "storage_class_not_found", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("mysql", "data", "nonexistent-storage", "10Gi"). - Build(), - }) - - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "20Gi", 1), - verify: verifyNoVolumeExpansionCreated, // 应该跳过存储扩容 - }, - - // 错误处理测试 - { - name: "get_cluster_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list failed")). - Build() - }, - input: newExpansionInput("any-service", "500m", "1Gi", "10Gi", 2), - expectErr: true, - errContains: "list clusters by service_id", - }, - { - name: "ops_create_skipped_handling", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentResources( - "mysql", - testutil.Resources("250m", "512Mi"), - testutil.Resources("250m", "512Mi")). - Build(), - testutil.NewOpsRequestBuilder("blocking-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VerticalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel("test-cluster"). - Build(), - }) - - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 1), - verify: verifyCancelOpsStrategy, - }, - - // 多组件扩容测试 - { - name: "multi_component_horizontal_scaling_both", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). - WithComponentReplicas("redis-sentinel", 3). - WithComponentResources("redis", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentResources("redis-sentinel", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 5), - verify: func(t *testing.T, c client.Client) { - verifyHorizontalScalingDetails(t, c, []model.ComponentHorizontalScaling{ - {Name: "redis", DeltaReplicas: 2}, - {Name: "redis-sentinel", DeltaReplicas: 2}, - }) - }, - }, - { - name: "multi_component_vertical_scaling_both", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). - WithComponentReplicas("redis-sentinel", 3). - WithComponentResources("redis", - testutil.Resources("250m", "512Mi"), - testutil.Resources("250m", "512Mi")). - WithComponentResources("redis-sentinel", - testutil.Resources("250m", "512Mi"), - testutil.Resources("250m", "512Mi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), - verify: func(t *testing.T, c client.Client) { - verifyVerticalScalingDetails(t, c, []model.ComponentVerticalScaling{ - {Name: "redis", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - {Name: "redis-sentinel", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - }) - }, - }, - { - name: "multi_component_scale_down", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 5). - WithComponentReplicas("redis-sentinel", 5). - WithComponentResources("redis", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentResources("redis-sentinel", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 2), - verify: func(t *testing.T, c client.Client) { - verifyHorizontalScalingDetails(t, c, []model.ComponentHorizontalScaling{ - {Name: "redis", DeltaReplicas: -3}, - {Name: "redis-sentinel", DeltaReplicas: -3}, - }) - }, - }, - - // 多组件部分扩容测试 - { - name: "multi_component_partial_horizontal_scaling", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). - WithComponentReplicas("redis-sentinel", 5). // sentinel 已经是期望副本数 - WithComponentResources("redis", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentResources("redis-sentinel", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 5), - verify: func(t *testing.T, c client.Client) { - // 只有 redis 组件需要扩容,sentinel 保持不变 - verifyHorizontalScalingDetails(t, c, []model.ComponentHorizontalScaling{ - {Name: "redis", DeltaReplicas: 2}, // redis 从 3 扩到 5 - }) - }, - }, - { - name: "multi_component_partial_vertical_scaling", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). - WithComponentReplicas("redis-sentinel", 3). - WithComponentResources("redis", - testutil.Resources("250m", "512Mi"), // redis 需要垂直扩容 - testutil.Resources("250m", "512Mi")). - WithComponentResources("redis-sentinel", - testutil.Resources("500m", "1Gi"), // sentinel 已经是期望资源 - testutil.Resources("500m", "1Gi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), - verify: func(t *testing.T, c client.Client) { - // 只有 redis 组件需要垂直扩容,sentinel 保持不变 - verifyVerticalScalingDetails(t, c, []model.ComponentVerticalScaling{ - {Name: "redis", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - }) - }, - }, - - // 混合扩容类型测试 - { - name: "multi_component_mixed_scaling_types", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). // redis 需要水平扩容 - WithComponentReplicas("redis-sentinel", 5). // sentinel 不需要水平扩容 - WithComponentResources("redis", - testutil.Resources("500m", "1Gi"), // redis 不需要垂直扩容 - testutil.Resources("500m", "1Gi")). - WithComponentResources("redis-sentinel", - testutil.Resources("250m", "512Mi"), // sentinel 需要垂直扩容 - testutil.Resources("250m", "512Mi")). - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 5), - verify: func(t *testing.T, c client.Client) { - // 验证水平扩容 - verifyHorizontalScalingDetails(t, c, []model.ComponentHorizontalScaling{ - {Name: "redis", DeltaReplicas: 2}, // redis 从 3 扩到 5 - }) - // 验证垂直扩容 - verifyVerticalScalingDetails(t, c, []model.ComponentVerticalScaling{ - {Name: "redis-sentinel", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - }) - }, - }, - { - name: "multi_component_all_scaling_types", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - // 创建支持扩容的 StorageClass - testutil.NewStorageClassBuilder("expandable-storage"). - WithAllowVolumeExpansion(true). - Build(), - - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). // redis 需要水平扩容 - WithComponentReplicas("redis-sentinel", 3). - WithComponentResources("redis", - testutil.Resources("250m", "512Mi"), // redis 需要垂直扩容 - testutil.Resources("250m", "512Mi")). - WithComponentResources("redis-sentinel", - testutil.Resources("250m", "512Mi"), // sentinel 需要垂直扩容 - testutil.Resources("250m", "512Mi")). - WithComponentVolumeClaimTemplate("redis", "data", "expandable-storage", "5Gi"). // redis 需要存储扩容 - WithComponentVolumeClaimTemplate("redis-sentinel", "data", "expandable-storage", "10Gi"). // sentinel 不需要存储扩容 - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 5), - verify: func(t *testing.T, c client.Client) { - // 验证水平扩容 - verifyHorizontalScalingDetails(t, c, []model.ComponentHorizontalScaling{ - {Name: "redis", DeltaReplicas: 2}, // redis 从 3 扩到 5 - {Name: "redis-sentinel", DeltaReplicas: 2}, // sentinel 从 3 扩到 5 - }) - // 验证垂直扩容 - verifyVerticalScalingDetails(t, c, []model.ComponentVerticalScaling{ - {Name: "redis", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - {Name: "redis-sentinel", CPU: resource.MustParse("500m"), Memory: resource.MustParse("1Gi")}, - }) - // 验证存储扩容 - verifyVolumeExpansionDetails(t, c, []model.ComponentVolumeExpansion{ - {Name: "redis", VolumeClaimTemplateName: "data", Storage: resource.MustParse("10Gi")}, - }) - }, - }, - - // 存储缩容警告测试 - { - name: "storage_shrinking_warning_test", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - // 创建支持扩容的 StorageClass - testutil.NewStorageClassBuilder("expandable-storage"). - WithAllowVolumeExpansion(true). - Build(), - - testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("mysql", 3). - WithComponentResources("mysql", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("mysql", "data", "expandable-storage", "20Gi"). // 当前存储 20Gi - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), // 期望存储 10Gi < 当前 20Gi - verify: func(t *testing.T, c client.Client) { - // 验证没有创建 VolumeExpansion OpsRequest,因为存储缩容不支持 - verifyNoVolumeExpansionCreated(t, c) - // 注意:这里只验证没有创建 OpsRequest,实际的日志验证需要 mock logger - }, - }, - { - name: "multi_component_mixed_storage_shrinking", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - return testutil.CreateObjects(context.Background(), c, []client.Object{ - // 创建支持扩容的 StorageClass - testutil.NewStorageClassBuilder("expandable-storage"). - WithAllowVolumeExpansion(true). - Build(), - - testutil.NewRedisCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - WithComponentReplicas("redis", 3). - WithComponentReplicas("redis-sentinel", 3). - WithComponentResources("redis", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentResources("redis-sentinel", - testutil.Resources("500m", "1Gi"), - testutil.Resources("500m", "1Gi")). - WithComponentVolumeClaimTemplate("redis", "data", "expandable-storage", "5Gi"). // redis 需要扩容 5Gi -> 10Gi - WithComponentVolumeClaimTemplate("redis-sentinel", "data", "expandable-storage", "20Gi"). // sentinel 尝试缩容 20Gi -> 10Gi - Build(), - }) - }, - input: newExpansionInput(testutil.TestServiceID, "500m", "1Gi", "10Gi", 3), - verify: func(t *testing.T, c client.Client) { - // 只有 redis 组件应该创建存储扩容,sentinel 组件应该被跳过 - verifyVolumeExpansionDetails(t, c, []model.ComponentVolumeExpansion{ - {Name: "redis", VolumeClaimTemplateName: "data", Storage: resource.MustParse("10Gi")}, - }) - }, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - k8sClient := tt.clientSetup() - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - service := cluster.NewService(k8sClient) - - err := service.ExpansionCluster(ctx, tt.input) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// newExpansionInput 构建 ExpansionInput 测试数据 -func newExpansionInput(serviceID, cpu, memory, storage string, replicas int32) model.ExpansionInput { - return model.ExpansionInput{ - RBDService: model.RBDService{ - ServiceID: serviceID, - }, - ClusterResource: model.ClusterResource{ - CPU: cpu, - Memory: memory, - Storage: storage, - Replicas: replicas, - }, - } -} - -// verifyOpsRequestCreated 验证指定类型的 OpsRequest 是否被创建 -func verifyOpsRequestCreated(t *testing.T, c client.Client, clusterName string, opsType opsv1alpha1.OpsType) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": clusterName, - "operations.kubeblocks.io/ops-type": string(opsType), - })) - require.NoError(t, err) - assert.NotEmpty(t, opsList.Items, "Expected %s OpsRequest to be created for cluster %s", opsType, clusterName) -} - -// verifyNoOpsRequest 验证指定类型的 OpsRequest 是否未被创建 -func verifyNoOpsRequest(t *testing.T, c client.Client, clusterName string, opsType opsv1alpha1.OpsType) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": clusterName, - "operations.kubeblocks.io/ops-type": string(opsType), - })) - require.NoError(t, err) - assert.Empty(t, opsList.Items, "Expected no %s OpsRequest for cluster %s", opsType, clusterName) -} - -// verifyBothHorizontalAndVerticalScalingCreated 验证水平和垂直扩容 OpsRequest 都被创建 -func verifyBothHorizontalAndVerticalScalingCreated(t *testing.T, c client.Client) { - verifyHorizontalScalingCreated(t, c) - verifyVerticalScalingCreated(t, c) -} - -// verifyCancelOpsStrategy 验证取消操作策略是否正确执行 -func verifyCancelOpsStrategy(t *testing.T, c client.Client) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - "operations.kubeblocks.io/ops-type": string(opsv1alpha1.VerticalScalingType), - })) - require.NoError(t, err) - - // 应该有两个 VerticalScaling OpsRequest:原有的(被取消)和新创建的 - assert.Len(t, opsList.Items, 2, "Expected original blocking OpsRequest and new created OpsRequest") - - // 验证原有的 blocking-ops 是否被取消 - var blockingOps, newOps *opsv1alpha1.OpsRequest - for i := range opsList.Items { - if opsList.Items[i].Name == "blocking-ops" { - blockingOps = &opsList.Items[i] - } else { - newOps = &opsList.Items[i] - } - } - - require.NotNil(t, blockingOps, "Original blocking OpsRequest should exist") - require.NotNil(t, newOps, "New OpsRequest should be created") - - // 验证原有的 OpsRequest 被设置为取消状态 - assert.True(t, blockingOps.Spec.Cancel, "Original blocking OpsRequest should be cancelled") -} - -// verifyHorizontalScalingCreated 验证水平扩容 OpsRequest 被创建 -func verifyHorizontalScalingCreated(t *testing.T, c client.Client) { - verifyOpsRequestCreated(t, c, "test-cluster", opsv1alpha1.HorizontalScalingType) -} - -// verifyNoOpsRequestCreated 验证没有任何 OpsRequest 被创建 -func verifyNoOpsRequestCreated(t *testing.T, c client.Client) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - })) - require.NoError(t, err) - assert.Empty(t, opsList.Items, "Expected no OpsRequest to be created") -} - -// verifyNoVolumeExpansionCreated 验证存储扩容 OpsRequest 未被创建 -func verifyNoVolumeExpansionCreated(t *testing.T, c client.Client) { - verifyNoOpsRequest(t, c, "test-cluster", opsv1alpha1.VolumeExpansionType) -} - -// verifyVerticalScalingCreated 验证垂直扩容 OpsRequest 被创建 -func verifyVerticalScalingCreated(t *testing.T, c client.Client) { - verifyOpsRequestCreated(t, c, "test-cluster", opsv1alpha1.VerticalScalingType) -} - -// verifyVolumeExpansionCreated 验证存储扩容 OpsRequest 被创建 -func verifyVolumeExpansionCreated(t *testing.T, c client.Client) { - verifyOpsRequestCreated(t, c, "test-cluster", opsv1alpha1.VolumeExpansionType) -} - -// verifyHorizontalScalingDetails 验证水平扩容的具体参数 -func verifyHorizontalScalingDetails(t *testing.T, c client.Client, expectedComponents []model.ComponentHorizontalScaling) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - "operations.kubeblocks.io/ops-type": string(opsv1alpha1.HorizontalScalingType), - })) - require.NoError(t, err) - require.NotEmpty(t, opsList.Items, "Expected HorizontalScaling OpsRequest to exist") - - opsRequest := opsList.Items[0] - require.NotNil(t, opsRequest.Spec.HorizontalScalingList, "HorizontalScalingList should not be nil") - - // 验证组件数量 - assert.Len(t, opsRequest.Spec.HorizontalScalingList, len(expectedComponents), "Component count should match") - - // 验证每个组件的 DeltaReplicas - actualComponents := make(map[string]int32) - for _, comp := range opsRequest.Spec.HorizontalScalingList { - var deltaReplicas int32 - if comp.ScaleOut != nil && comp.ScaleOut.ReplicaChanges != nil { - deltaReplicas = *comp.ScaleOut.ReplicaChanges - } else if comp.ScaleIn != nil && comp.ScaleIn.ReplicaChanges != nil { - deltaReplicas = -*comp.ScaleIn.ReplicaChanges // ScaleIn 时为负数 - } - actualComponents[comp.ComponentName] = deltaReplicas - } - - for _, expected := range expectedComponents { - actual, exists := actualComponents[expected.Name] - assert.True(t, exists, "Component %s should exist in OpsRequest", expected.Name) - assert.Equal(t, expected.DeltaReplicas, actual, "DeltaReplicas for component %s should match", expected.Name) - } -} - -// verifyVerticalScalingDetails 验证垂直扩容的具体资源值 -func verifyVerticalScalingDetails(t *testing.T, c client.Client, expectedComponents []model.ComponentVerticalScaling) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - "operations.kubeblocks.io/ops-type": string(opsv1alpha1.VerticalScalingType), - })) - require.NoError(t, err) - require.NotEmpty(t, opsList.Items, "Expected VerticalScaling OpsRequest to exist") - - opsRequest := opsList.Items[0] - require.NotNil(t, opsRequest.Spec.VerticalScalingList, "VerticalScalingList should not be nil") - - // 验证组件数量 - assert.Len(t, opsRequest.Spec.VerticalScalingList, len(expectedComponents), "Component count should match") - - // 验证每个组件的 CPU/Memory - actualComponents := make(map[string]opsv1alpha1.VerticalScaling) - for _, comp := range opsRequest.Spec.VerticalScalingList { - actualComponents[comp.ComponentName] = comp - } - - for _, expected := range expectedComponents { - actual, exists := actualComponents[expected.Name] - assert.True(t, exists, "Component %s should exist in OpsRequest", expected.Name) - - // 验证 CPU - actualCPU := actual.ResourceRequirements.Limits.Cpu() - assert.Equal(t, expected.CPU.Cmp(*actualCPU), 0, "CPU for component %s should match", expected.Name) - - // 验证 Memory - actualMem := actual.ResourceRequirements.Limits.Memory() - assert.Equal(t, expected.Memory.Cmp(*actualMem), 0, "Memory for component %s should match", expected.Name) - } -} - -// verifyVolumeExpansionDetails 验证存储扩容的具体参数 -func verifyVolumeExpansionDetails(t *testing.T, c client.Client, expectedComponents []model.ComponentVolumeExpansion) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - "operations.kubeblocks.io/ops-type": string(opsv1alpha1.VolumeExpansionType), - })) - require.NoError(t, err) - require.NotEmpty(t, opsList.Items, "Expected VolumeExpansion OpsRequest to exist") - - opsRequest := opsList.Items[0] - require.NotNil(t, opsRequest.Spec.VolumeExpansionList, "VolumeExpansionList should not be nil") - - // 验证组件数量 - assert.Len(t, opsRequest.Spec.VolumeExpansionList, len(expectedComponents), "Component count should match") - - // 验证每个组件的存储大小 - actualComponents := make(map[string]opsv1alpha1.VolumeExpansion) - for _, comp := range opsRequest.Spec.VolumeExpansionList { - actualComponents[comp.ComponentName] = comp - } - - for _, expected := range expectedComponents { - actual, exists := actualComponents[expected.Name] - assert.True(t, exists, "Component %s should exist in OpsRequest", expected.Name) - - // 验证存储大小 - for _, vct := range actual.VolumeClaimTemplates { - if vct.Name == expected.VolumeClaimTemplateName { - actualStorage := vct.Storage - assert.Equal(t, expected.Storage.Cmp(actualStorage), 0, - "Storage size for component %s volume %s should match", expected.Name, expected.VolumeClaimTemplateName) - } - } - } -} - -// verifyMultiComponentOpsRequest 验证多组件 OpsRequest 的组件数量 -func verifyMultiComponentOpsRequest(t *testing.T, c client.Client, opsType opsv1alpha1.OpsType, expectedComponentCount int) { - ctx := context.Background() - var opsList opsv1alpha1.OpsRequestList - - err := c.List(ctx, &opsList, client.MatchingLabels(map[string]string{ - "app.kubernetes.io/instance": "test-cluster", - "operations.kubeblocks.io/ops-type": string(opsType), - })) - require.NoError(t, err) - require.NotEmpty(t, opsList.Items, "Expected %s OpsRequest to exist", opsType) - - opsRequest := opsList.Items[0] - var actualComponentCount int - - switch opsType { - case opsv1alpha1.HorizontalScalingType: - actualComponentCount = len(opsRequest.Spec.HorizontalScalingList) - case opsv1alpha1.VerticalScalingType: - actualComponentCount = len(opsRequest.Spec.VerticalScalingList) - case opsv1alpha1.VolumeExpansionType: - actualComponentCount = len(opsRequest.Spec.VolumeExpansionList) - default: - t.Fatalf("Unsupported OpsType: %s", opsType) - } - - assert.Equal(t, expectedComponentCount, actualComponentCount, - "Component count in %s OpsRequest should match", opsType) -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator.go b/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator.go deleted file mode 100644 index 3cf64c452..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package coordinator 提供 adapter.Coordinator 的实现 -// -// Coordinator 用于协调 KubeBlocks 和 Rainbond -package coordinator - -import ( - "fmt" - "strconv" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" -) - -var _ adapter.Coordinator = &Coordinator{} - -// Coordinator 实现 Coordinator 接口,所有的 Coordinator 都应基于 Coordinator 实现 -type Coordinator struct { -} - -func (c *Coordinator) TargetPort() int { - return -1 -} - -func (c *Coordinator) GetSecretName(clusterName string) string { - // Coordinator 实现使用通用的 root 账户格式,但实际不应被直接使用 - // 每个具体的 Coordinator 都应该重写此方法 - return fmt.Sprintf("%s-account-root", clusterName) -} - -// GetBackupMethod 返回空字符串,任何支持备份的 Addon 都应该重写此方法 -func (c *Coordinator) GetBackupMethod() string { - return "" -} - -// GetParametersConfigMap 返回 nil,任何支持参数配置的 Addon 都应该重写此方法 -func (c *Coordinator) GetParametersConfigMap(clusterName string) *string { - return nil -} - -// ParseParameters 返回空切片,任何支持参数配置的 Addon 都应该重写此方法 -func (c *Coordinator) ParseParameters(configData map[string]string) ([]model.ParameterEntry, error) { - return []model.ParameterEntry{}, nil -} - -// SystemAccount 返回 nil,任何启用 custom secret 的 Addon 都应该重写此方法 -func (c *Coordinator) SystemAccount() *string { - return nil -} - -// convParameterValue 解析配置参数值,尝试转换为合适的类型 -// 支持自动类型推断: int -> int, float -> float64, bool -> bool(仅 true/false), -func convParameterValue(value string) any { - if value == "" { - return value - } - - trimmed := strings.Trim(value, "'\"") - - // bool - if strings.EqualFold(trimmed, "true") { - return true - } - if strings.EqualFold(trimmed, "false") { - return false - } - - // int64 - if intVal, err := strconv.ParseInt(trimmed, 10, 64); err == nil { - return intVal - } - - // uint64 - if uintVal, err := strconv.ParseUint(trimmed, 10, 64); err == nil { - return uintVal - } - - // float64 - if floatVal, err := strconv.ParseFloat(trimmed, 64); err == nil { - return floatVal - } - - // 处理带单位的值,应当原样返回 - if len(trimmed) > 1 { - lastChar := strings.ToUpper(trimmed[len(trimmed)-1:]) - if lastChar == "K" || lastChar == "M" || lastChar == "G" || lastChar == "T" { - numPart := trimmed[:len(trimmed)-1] - if _, err := strconv.ParseFloat(numPart, 64); err == nil { - return trimmed - } - } - } - - // 处理时间单位,应当原样返回 - if len(trimmed) > 1 { - lastTwo := strings.ToLower(trimmed[len(trimmed)-2:]) - lastOne := strings.ToLower(trimmed[len(trimmed)-1:]) - if lastTwo == "ms" || lastTwo == "us" || lastOne == "s" || lastOne == "m" || lastOne == "h" { - return trimmed - } - } - - return trimmed -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go b/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go deleted file mode 100644 index 03ceb9836..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package coordinator - -import ( - "testing" -) - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestBase_ParseParameters(t *testing.T) { - testCases := []struct { - name string - configData map[string]string - expectedLen int - expectedError bool - }{ - { - name: "empty config data", - configData: map[string]string{}, - expectedLen: 0, - expectedError: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - base := &Coordinator{} - params, err := base.ParseParameters(tc.configData) - - if tc.expectedError && err == nil { - t.Fatalf("expected error, got nil") - } - - if !tc.expectedError && err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if len(params) != tc.expectedLen { - t.Fatalf("expected %d parameters, got %d", tc.expectedLen, len(params)) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_EmptyString(t *testing.T) { - testCases := []struct { - name string - input string - expected any - }{ - { - name: "empty string", - input: "", - expected: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v, got %v", tc.expected, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_Boolean(t *testing.T) { - testCases := []struct { - input string - expected any - }{ - {"true", true}, - {"True", true}, - {"TRUE", true}, - {"false", false}, - {"False", false}, - {"FALSE", false}, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_Integer(t *testing.T) { - testCases := []struct { - input string - expected any - }{ - {"123", int64(123)}, - {"-456", int64(-456)}, - {"0", int64(0)}, - {"2147483647", int64(2147483647)}, // max int32 - {"-2147483648", int64(-2147483648)}, // min int32 - {"2147483648", int64(2147483648)}, // beyond int32, should be int64 - {"-2147483649", int64(-2147483649)}, // beyond int32, should be int64 - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_Float(t *testing.T) { - testCases := []struct { - input string - expected float64 - }{ - {"3.14", 3.14}, - {"-2.5", -2.5}, - {"0.0", 0.0}, - {"1.23e-4", 1.23e-4}, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_SizeUnits(t *testing.T) { - testCases := []struct { - input string - expected string - }{ - {"128M", "128M"}, - {"1G", "1G"}, - {"512K", "512K"}, - {"2T", "2T"}, - {"64.5M", "64.5M"}, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_TimeUnits(t *testing.T) { - testCases := []struct { - input string - expected string - }{ - {"30s", "30s"}, - {"5m", "5m"}, - {"1h", "1h"}, - {"100ms", "100ms"}, - {"50us", "50us"}, - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.coordinator.parameter-value-parse -func TestParseParameterValue_QuotedStrings(t *testing.T) { - testCases := []struct { - input string - expected any - }{ - {"'true'", true}, // PostgreSQL style quoted boolean - {"\"false\"", false}, // Double quoted boolean - {"'123'", int64(123)}, // Quoted integer should still parse as int64 - {"'hello'", "hello"}, // Regular quoted string - {"\"world\"", "world"}, // Double quoted string - } - - for _, tc := range testCases { - t.Run(tc.input, func(t *testing.T) { - result := convParameterValue(tc.input) - if result != tc.expected { - t.Fatalf("expected %v for input %s, got %v", tc.expected, tc.input, result) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/mysql.go b/plugins/kb-adapter-rbdplugin/service/coordinator/mysql.go deleted file mode 100644 index ee66f4c5b..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/mysql.go +++ /dev/null @@ -1,80 +0,0 @@ -package coordinator - -import ( - "fmt" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - - "gopkg.in/ini.v1" -) - -var _ adapter.Coordinator = &MySQL{} - -// MySQL 实现 Coordinator 接口 -type MySQL struct { - Coordinator -} - -func (c *MySQL) TargetPort() int { - return 3306 -} - -func (c *MySQL) GetSecretName(clusterName string) string { - // MySQL 使用 mysql 作为中间部分和 root 作为账户类型 - return fmt.Sprintf("%s-mysql-account-root", clusterName) -} - -func (c *MySQL) GetBackupMethod() string { - return "xtrabackup" -} - -func (c *MySQL) GetParametersConfigMap(clusterName string) *string { - cmName := fmt.Sprintf("%s-mysql-mysql-replication-config", clusterName) - return &cmName -} - -// ParseParameters 解析 MySQL ConfigMap 中的 my.cnf 配置参数 -// 基于实际的 ConfigMap 格式: data.my.cnf 包含 INI 格式的配置内容 -func (c *MySQL) ParseParameters(configData map[string]string) ([]model.ParameterEntry, error) { - // 获取 my.cnf 配置内容 - myCnfContent, exists := configData["my.cnf"] - if !exists { - log.Warn("my.cnf not found in ConfigMap data") - return []model.ParameterEntry{}, nil - } - - if strings.TrimSpace(myCnfContent) == "" { - log.Info("my.cnf content is empty") - return []model.ParameterEntry{}, nil - } - - cfg, err := ini.LoadSources(ini.LoadOptions{ - AllowBooleanKeys: true, - Loose: true, - }, []byte(myCnfContent)) - if err != nil { - log.Warn("failed to parse my.cnf content", log.Err(err)) - return []model.ParameterEntry{}, fmt.Errorf("parse my.cnf: %w", err) - } - - var parameters []model.ParameterEntry - - mysqldSettings := cfg.Section("mysqld") - if mysqldSettings == nil { - log.Info("mysqld section not found in my.cnf") - return []model.ParameterEntry{}, nil - } - - for _, key := range mysqldSettings.Keys() { - param := model.ParameterEntry{ - Name: key.Name(), - Value: convParameterValue(key.String()), - } - parameters = append(parameters, param) - } - - return parameters, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/postgresql.go b/plugins/kb-adapter-rbdplugin/service/coordinator/postgresql.go deleted file mode 100644 index dc1fda59d..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/postgresql.go +++ /dev/null @@ -1,112 +0,0 @@ -package coordinator - -import ( - "fmt" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" -) - -var _ adapter.Coordinator = &PostgreSQL{} - -// PostgreSQL 实现 Coordinator 接口 -type PostgreSQL struct { - Coordinator -} - -func (c *PostgreSQL) TargetPort() int { - return 6432 -} - -func (c *PostgreSQL) GetSecretName(clusterName string) string { - // PostgreSQL 使用 postgresql 作为中间部分和 postgres 作为账户类型 - return fmt.Sprintf("%s-postgresql-account-postgres", clusterName) -} - -func (c *PostgreSQL) GetBackupMethod() string { - return "pg-basebackup" -} - -func (c *PostgreSQL) GetParametersConfigMap(clusterName string) *string { - cmName := fmt.Sprintf("%s-postgresql-postgresql-configuration", clusterName) - return &cmName -} - -// ParseParameters 解析 PostgreSQL ConfigMap 中的 postgresql.conf 配置参数 -// 基于实际的 ConfigMap 格式: data.postgresql.conf 包含键值对格式的配置内容 -func (c *PostgreSQL) ParseParameters(configData map[string]string) ([]model.ParameterEntry, error) { - // 获取 postgresql.conf 配置内容 - pgConfContent, exists := configData["postgresql.conf"] - if !exists { - log.Warn("postgresql.conf not found in ConfigMap data") - return []model.ParameterEntry{}, nil - } - - if strings.TrimSpace(pgConfContent) == "" { - log.Info("postgresql.conf content is empty") - return []model.ParameterEntry{}, nil - } - - lines := strings.Split(pgConfContent, "\n") - parameters := make([]model.ParameterEntry, 0, len(lines)/2) - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - if strings.HasPrefix(line, "#") { - continue - } - line = removeInlineComment(line) - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - if key == "" { - continue - } - - param := model.ParameterEntry{ - Name: key, - Value: convParameterValue(value), - } - parameters = append(parameters, param) - } - - return parameters, nil -} - -// removeInlineComment 移除行尾注释,但保留引号内的 # 字符 -// -// 例如: "key = 'value # not comment' # this is comment" -> "key = 'value # not comment'" -func removeInlineComment(line string) string { - inSingleQuote := false - inDoubleQuote := false - - for i, ch := range line { - switch ch { - case '\'': - if !inDoubleQuote { - inSingleQuote = !inSingleQuote - } - case '"': - if !inSingleQuote { - inDoubleQuote = !inDoubleQuote - } - case '#': - // 如果不在引号内,这是注释的开始 - if !inSingleQuote && !inDoubleQuote { - return strings.TrimSpace(line[:i]) - } - } - } - - return line -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/rabbitmq.go b/plugins/kb-adapter-rbdplugin/service/coordinator/rabbitmq.go deleted file mode 100644 index c664df970..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/rabbitmq.go +++ /dev/null @@ -1,26 +0,0 @@ -package coordinator - -import ( - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" -) - -var _ adapter.Coordinator = &RabbitMQ{} - -// RabbitMQ 实现 RabbitMQ 的 Coordinator -// -// - 不支持备份功能 -// -// - 不支持参数配置 -type RabbitMQ struct { - Coordinator -} - -func (r *RabbitMQ) TargetPort() int { - return 5672 -} - -func (r *RabbitMQ) GetSecretName(clusterName string) string { - return fmt.Sprintf("%s-rabbitmq-account-root", clusterName) -} diff --git a/plugins/kb-adapter-rbdplugin/service/coordinator/redis.go b/plugins/kb-adapter-rbdplugin/service/coordinator/redis.go deleted file mode 100644 index 91a8c7aaf..000000000 --- a/plugins/kb-adapter-rbdplugin/service/coordinator/redis.go +++ /dev/null @@ -1,120 +0,0 @@ -package coordinator - -import ( - "fmt" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - "k8s.io/utils/ptr" -) - -var _ adapter.Coordinator = &Redis{} - -type Redis struct { -} - -func (r *Redis) TargetPort() int { - return 6379 -} - -func (r *Redis) GetSecretName(clusterName string) string { - return fmt.Sprintf("%s-redis-account-default", clusterName) -} - -func (r *Redis) GetBackupMethod() string { - return "datafile" -} - -func (r *Redis) GetParametersConfigMap(clusterName string) *string { - cmName := fmt.Sprintf("%s-redis-redis-replication-config", clusterName) - return &cmName -} - -// SystemAccount 来自 componentDefinition -func (r *Redis) SystemAccount() *string { - return ptr.To("default") -} - -// ParseParameters 解析 Redis ConfigMap 中的 redis.conf 配置参数 -// 基于实际的 ConfigMap 格式: data.redis.conf 包含 Redis 配置格式的内容 -func (r *Redis) ParseParameters(configData map[string]string) ([]model.ParameterEntry, error) { - // 获取 redis.conf 配置内容 - redisConfContent, exists := configData["redis.conf"] - if !exists { - log.Warn("redis.conf not found in ConfigMap data") - return []model.ParameterEntry{}, nil - } - - if strings.TrimSpace(redisConfContent) == "" { - log.Info("redis.conf content is empty") - return []model.ParameterEntry{}, nil - } - - var parameters []model.ParameterEntry - - // 逐行解析配置文件 - lines := strings.SplitSeq(redisConfContent, "\n") - for line := range lines { - entry := r.parseConfigLine(line) - if entry != nil { - parameters = append(parameters, *entry) - } - } - - return parameters, nil -} - -// parseConfigLine 解析单行 Redis 配置 -// 返回 nil 表示该行应被跳过(注释或空行) -func (r *Redis) parseConfigLine(line string) *model.ParameterEntry { - line = strings.TrimSpace(line) - - if line == "" { - return nil - } - - if strings.HasPrefix(line, "#") { - return nil - } - - if commentPos := strings.Index(line, "#"); commentPos >= 0 { - line = strings.TrimSpace(line[:commentPos]) - // 如果移除注释后变为空行,跳过 - if line == "" { - return nil - } - } - - parts := strings.Fields(line) - if len(parts) < 2 { - // 没有值的参数,跳过 - return nil - } - - paramName := parts[0] - paramValue := strings.Join(parts[1:], " ") - - paramValue = r.cleanQuotedValue(paramValue) - - return &model.ParameterEntry{ - Name: paramName, - Value: paramValue, - } -} - -// cleanQuotedValue 清理带引号的值,去除外层引号但保持内容 -func (r *Redis) cleanQuotedValue(value string) string { - value = strings.TrimSpace(value) - - if len(value) >= 2 && strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { - return value[1 : len(value)-1] - } - - if len(value) >= 2 && strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'") { - return value[1 : len(value)-1] - } - - return value -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/doc.go b/plugins/kb-adapter-rbdplugin/service/kbkit/doc.go deleted file mode 100644 index 36acda712..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package kbkit 包含 service 中各个子包共用的工具函数 -package kbkit diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/errors.go b/plugins/kb-adapter-rbdplugin/service/kbkit/errors.go deleted file mode 100644 index 6f0692212..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/errors.go +++ /dev/null @@ -1,74 +0,0 @@ -package kbkit - -import ( - "errors" - "fmt" -) - -var ( - // ErrTargetNotFound 目标资源不存在 - ErrTargetNotFound = errors.New("resource not found") - - // ErrMultipleFounded 表示存在多个同类型资源 - ErrMultipleFounded = errors.New("multiple resources found") - - // ErrCreateOpsSkipped 表示创建 OpsRequest 因预检检查而被跳过 - ErrCreateOpsSkipped = errors.New("operation skipped by preflight check") - - // ErrClusterRequired 表示集群信息缺失 - ErrClusterRequired = errors.New("cluster is required") -) - -// 参数变更操作中错误常量 -const ( - // ParamNotExist 参数在集群定义中不存在 - ParamNotExist ParameterErrCode = "NOT_EXIST" - - // ParamImmutable 参数不可变,无法进行热更新 - ParamImmutable ParameterErrCode = "IMMUTABLE" - - // ParamInvalidType 参数类型校验失败 - ParamInvalidType ParameterErrCode = "INVALID_TYPE" - - // ParamOutOfRange 数值参数超出允许范围 - ParamOutOfRange ParameterErrCode = "OUT_OF_RANGE" - - // ParamInvalidEnum 枚举参数值不在允许列表中 - ParamInvalidEnum ParameterErrCode = "INVALID_ENUM" - - // ParamRequiredMissing 必需参数缺失 - ParamRequiredMissing ParameterErrCode = "REQUIRED_MISSING" -) - -type ParameterErrCode string - -// ParameterValidationError 参数验证错误的结构化表示 -// 提供详细的错误信息和标准化的错误码 -type ParameterValidationError struct { - ParameterName string // 参数名称 - ErrorCode ParameterErrCode // 用于向 Rainbond 展示的错误码 - ErrorMessage string // 详细错误信息 - Cause error // 底层错误 -} - -// Error - -func (e *ParameterValidationError) Error() string { - if e.Cause != nil { - return fmt.Sprintf("%s: %v", e.ErrorMessage, e.Cause) - } - return e.ErrorMessage -} - -// Is supports errors.Is -func (e *ParameterValidationError) Is(target error) bool { - var t *ParameterValidationError - if errors.As(target, &t) { - return e.ErrorCode == t.ErrorCode && e.ParameterName == t.ParameterName - } - return false -} - -// Unwrap supports errors.Unwrap -func (e *ParameterValidationError) Unwrap() error { - return e.Cause -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go b/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go deleted file mode 100644 index 7f9962db6..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go +++ /dev/null @@ -1,718 +0,0 @@ -package kbkit - -import ( - "context" - "crypto/md5" - "fmt" - "strings" - "time" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "golang.org/x/sync/errgroup" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// OpsRequest 配置项 -var ( - opsTimeoutSecond int32 = 10 * 60 - opsPreConditionDeadline int32 = 5 * 60 - - defaultBackupDeletionPolicy = "Delete" - - // opsCleanupConcurrency 控制并发清理 OpsRequest 时的最大并发数 - opsCleanupConcurrency = 4 -) - -const ( - preflightProceed preflightDecision = iota + 1 // 创建 - preflightSkip // 跳过 - preflightCleanupAndProceed // 清理阻塞操作后创建 -) - -type preflightDecision int - -type preflightResult struct { - Decision preflightDecision -} - -// preflight 规定 OpsRequest 创建前/后的决策逻辑 -type preflight interface { - // decide 根据创建目标判断是否允许创建 - decide(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) (preflightResult, error) -} - -// uniqueOps 检查是否存在会阻塞新 OpsRequest 的同类型 OpsRequest, -// 如果存在阻塞操作,则跳过创建;否则允许创建 -type uniqueOps struct{} - -func (uniqueOps) decide(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) (preflightResult, error) { - opsList, err := getOpsRequestsByIndex(ctx, c, ops.Namespace, ops.Spec.ClusterName, ops.Spec.Type) - if err != nil { - if !errors.IsNotFound(err) { - return preflightResult{}, fmt.Errorf("list opsrequests for preflight: %w", err) - } - return preflightResult{Decision: preflightProceed}, nil - } - - for _, ops := range opsList { - if !isOpsRequestNonBlocking(&ops) { - return preflightResult{Decision: preflightSkip}, nil - } - } - - return preflightResult{Decision: preflightProceed}, nil -} - -// priorityOps 优先级操作预检策略,用于处理高优先级操作(重启、停止、启动), -// 当使用此策略时,会主动清理所有阻塞的 OpsRequest,确保优先级操作能够立即执行, -// 清理操作必须完全成功,否则整个创建过程将失败 -type priorityOps struct{} - -func (priorityOps) decide(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) (preflightResult, error) { - // 查询集群的所有非终态 OpsRequest - blockingOps, err := GetAllNonFinalOpsRequests(ctx, c, ops.Namespace, ops.Spec.ClusterName) - if err != nil { - return preflightResult{}, fmt.Errorf("get existing opsrequests: %w", err) - } - - // 如果没有需要清理的操作,直接创建 - if len(blockingOps) == 0 { - return preflightResult{Decision: preflightProceed}, nil - } - - if err := CleanupBlockingOps(ctx, c, blockingOps); err != nil { - return preflightResult{}, err - } - - return preflightResult{Decision: preflightCleanupAndProceed}, nil -} - -// cancelOps 优雅取消操作预检策略,用于处理伸缩操作(水平伸缩、垂直伸缩) -// -// 取消将同类型会阻塞的 OpsRequest,然后允许创建新的伸缩操作 -type cancelOps struct{} - -func (cancelOps) decide(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) (preflightResult, error) { - // 查找同类型的会阻塞的 OpsRequest - existingOps, err := getOpsRequestsByIndex(ctx, c, ops.Namespace, ops.Spec.ClusterName, ops.Spec.Type) - if err != nil { - if !errors.IsNotFound(err) { - return preflightResult{}, fmt.Errorf("list opsrequests for preflight: %w", err) - } - return preflightResult{Decision: preflightProceed}, nil - } - - // 收集需要优雅取消的 OpsRequest - var toCancel []*opsv1alpha1.OpsRequest - for i := range existingOps { - opsReq := &existingOps[i] - if !isOpsRequestNonBlocking(opsReq) { - toCancel = append(toCancel, opsReq) - } - } - - // 如果没有需要取消的 OpsRequest,直接创建 - if len(toCancel) == 0 { - return preflightResult{Decision: preflightProceed}, nil - } - - // 取消 OpsRequest - if err := cancelOpsRequests(ctx, c, toCancel); err != nil { - return preflightResult{}, fmt.Errorf("failed to gracefully cancel existing operations: %w", err) - } - - return preflightResult{Decision: preflightProceed}, nil -} - -type createOpts struct { - preflight preflight -} - -type createOption func(*createOpts) - -func validateCluster(cluster *kbappsv1.Cluster) error { - if cluster == nil { - return ErrClusterRequired - } - return nil -} - -// withPreflight 自定义预检策略 -func withPreflight(p preflight) createOption { - return func(o *createOpts) { o.preflight = p } -} - -// CreateLifecycleOpsRequest 创建生命周期管理相关的 OpsRequest,供 Reconciler 使用 -// 重启、停止,将被设置为 force: true 和 enqueueOnForce: false, 同时使用 priorityOps 移除正在阻塞的 OpsRequest,确保能够立即执行 -func CreateLifecycleOpsRequest(ctx context.Context, - c client.Client, - cluster *kbappsv1.Cluster, - opsType opsv1alpha1.OpsType, -) error { - if err := validateCluster(cluster); err != nil { - return err - } - - opsSpecific := opsv1alpha1.SpecificOpsRequest{} - if opsType == opsv1alpha1.RestartType { - opsSpecific.RestartList = []opsv1alpha1.ComponentOps{ - { - ComponentName: ClusterType(cluster), - }, - } - } - - if _, err := createOpsRequest(ctx, c, cluster, opsType, opsSpecific, withPreflight(priorityOps{})); err != nil { - return err - } - - return nil -} - -// CreateBackupOpsRequest 为指定的 Cluster 创建备份 OpsRequest -// -// backupMethod 为备份方法,不做任何额外预检,支持同时多次备份操作 -func CreateBackupOpsRequest(ctx context.Context, - c client.Client, - cluster *kbappsv1.Cluster, - backupMethod string, -) error { - if err := validateCluster(cluster); err != nil { - return err - } - - specificOps := opsv1alpha1.SpecificOpsRequest{ - Backup: &opsv1alpha1.Backup{ - BackupPolicyName: fmt.Sprintf("%s-%s-backup-policy", cluster.Name, ClusterType(cluster)), - BackupMethod: backupMethod, - DeletionPolicy: defaultBackupDeletionPolicy, - }, - } - - _, err := createOpsRequest(ctx, c, cluster, opsv1alpha1.BackupType, specificOps) - return err -} - -// CreateHorizontalScalingOpsRequest 为指定的 Cluster 创建水平伸缩 OpsRequest -// -// 将设置 enqueueOnForce: false 并将已经创建了的同类型 OpsRequest 设置为取消状态以避免阻塞 -func CreateHorizontalScalingOpsRequest(ctx context.Context, - c client.Client, - params model.HorizontalScalingOpsParams, -) error { - if err := validateCluster(params.Cluster); err != nil { - return err - } - var horizontalScalingList []opsv1alpha1.HorizontalScaling - - // 遍历所有组件,为每个组件创建对应的伸缩配置 - for _, component := range params.Components { - var scaling opsv1alpha1.HorizontalScaling - - if component.DeltaReplicas > 0 { - // ScaleOut - scaling = opsv1alpha1.HorizontalScaling{ - ComponentOps: opsv1alpha1.ComponentOps{ComponentName: component.Name}, - ScaleOut: &opsv1alpha1.ScaleOut{ - ReplicaChanger: opsv1alpha1.ReplicaChanger{ReplicaChanges: &component.DeltaReplicas}, - }, - } - } else { - // ScaleIn - absReplicas := -component.DeltaReplicas - scaling = opsv1alpha1.HorizontalScaling{ - ComponentOps: opsv1alpha1.ComponentOps{ComponentName: component.Name}, - ScaleIn: &opsv1alpha1.ScaleIn{ - ReplicaChanger: opsv1alpha1.ReplicaChanger{ReplicaChanges: &absReplicas}, - }, - } - } - - horizontalScalingList = append(horizontalScalingList, scaling) - } - - specificOps := opsv1alpha1.SpecificOpsRequest{ - HorizontalScalingList: horizontalScalingList, - } - - _, err := createOpsRequest(ctx, c, params.Cluster, opsv1alpha1.HorizontalScalingType, specificOps, withPreflight(cancelOps{})) - return err -} - -// CreateVerticalScalingOpsRequest 为指定的 Cluster 创建垂直伸缩 OpsRequest -// -// 将设置 enqueueOnForce: false 并将已经创建了的同类型 OpsRequest 设置为取消状态以避免阻塞 -func CreateVerticalScalingOpsRequest(ctx context.Context, - c client.Client, - params model.VerticalScalingOpsParams, -) error { - if err := validateCluster(params.Cluster); err != nil { - return err - } - var verticalScalingList []opsv1alpha1.VerticalScaling - - // 遍历所有组件,为每个组件创建对应的资源配置 - for _, component := range params.Components { - verticalScalingList = append(verticalScalingList, opsv1alpha1.VerticalScaling{ - ComponentOps: opsv1alpha1.ComponentOps{ComponentName: component.Name}, - ResourceRequirements: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: component.CPU, - corev1.ResourceMemory: component.Memory, - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: component.CPU, - corev1.ResourceMemory: component.Memory, - }, - }, - }) - } - - specificOps := opsv1alpha1.SpecificOpsRequest{ - VerticalScalingList: verticalScalingList, - } - - _, err := createOpsRequest(ctx, c, params.Cluster, opsv1alpha1.VerticalScalingType, specificOps, withPreflight(cancelOps{})) - return err -} - -// CreateVolumeExpansionOpsRequest 为指定的 Cluster 创建存储扩容 OpsRequest -// -// 使用 uniqueOps 预检策略,确保不会创建重复的存储扩容 OpsRequest(VolumeExpansion 不支持 cancel 的折中方案) -func CreateVolumeExpansionOpsRequest(ctx context.Context, - c client.Client, - params model.VolumeExpansionOpsParams, -) error { - if err := validateCluster(params.Cluster); err != nil { - return err - } - var volumeExpansionList []opsv1alpha1.VolumeExpansion - - // 遍历所有组件,为每个组件创建对应的存储扩容配置 - for _, component := range params.Components { - volumeExpansionList = append(volumeExpansionList, opsv1alpha1.VolumeExpansion{ - ComponentOps: opsv1alpha1.ComponentOps{ComponentName: component.Name}, - VolumeClaimTemplates: []opsv1alpha1.OpsRequestVolumeClaimTemplate{ - { - Name: component.VolumeClaimTemplateName, - Storage: component.Storage, - }, - }, - }) - } - - specificOps := opsv1alpha1.SpecificOpsRequest{ - VolumeExpansionList: volumeExpansionList, - } - - _, err := createOpsRequest(ctx, c, params.Cluster, opsv1alpha1.VolumeExpansionType, specificOps, withPreflight(uniqueOps{})) - return err -} - -// CreateParameterChangeOpsRequest 创建参数变更 OpsRequest -// -// 不做任何额外预检,由 KubeBlocks 处理 -func CreateParameterChangeOpsRequest(ctx context.Context, - c client.Client, - cluster *kbappsv1.Cluster, - parameters []model.ParameterEntry, -) error { - if err := validateCluster(cluster); err != nil { - return err - } - specificOps := opsv1alpha1.SpecificOpsRequest{ - Reconfigures: []opsv1alpha1.Reconfigure{ - { - ComponentOps: opsv1alpha1.ComponentOps{ComponentName: ClusterType(cluster)}, - }, - }, - } - - var parameterPairs []opsv1alpha1.ParameterPair - for _, parameter := range parameters { - if strValue, ok := parameter.Value.(*string); ok { - parameterPairs = append(parameterPairs, opsv1alpha1.ParameterPair{ - Key: parameter.Name, - Value: strValue, - }) - } - } - - specificOps.Reconfigures[0].Parameters = parameterPairs - - _, err := createOpsRequest(ctx, c, cluster, opsv1alpha1.ReconfiguringType, specificOps) - return err -} - -// CreateRestoreOpsRequest 使用 backupName 指定一个 backup 创建 Restore OpsRequest,从备份中恢复 cluster -// -// 通过 backup 恢复的 Cluster 的名称格式为 {cluster.Name(去除四位后缀)}-restore-{四位随机后缀}, -// 串行恢复卷声明,在集群进行 running 状态后执行 PostReady -// -// 不需要做任何额外预检,由 KubeBlocks 处理 -func CreateRestoreOpsRequest(ctx context.Context, - c client.Client, - cluster *kbappsv1.Cluster, - backupName string, -) (*opsv1alpha1.OpsRequest, error) { - if err := validateCluster(cluster); err != nil { - return nil, err - } - specificOps := opsv1alpha1.SpecificOpsRequest{ - Restore: &opsv1alpha1.Restore{ - BackupName: backupName, - VolumeRestorePolicy: "Serial", - DeferPostReadyUntilClusterRunning: true, - }, - } - - return createOpsRequest(ctx, c, cluster, opsv1alpha1.RestoreType, specificOps) -} - -// createOpsRequest 创建 OpsRequest -// -// OpsRequest 的名称格式为 {clustername}-{opsType}-{timestamp}, -// 使用时间戳确保每次操作都有唯一的名称 -func createOpsRequest( - ctx context.Context, - c client.Client, - cluster *kbappsv1.Cluster, - opsType opsv1alpha1.OpsType, - specificOps opsv1alpha1.SpecificOpsRequest, - opts ...createOption, -) (*opsv1alpha1.OpsRequest, error) { - if err := validateCluster(cluster); err != nil { - return nil, err - } - options := applyCreateOptions(opts...) - - ops := buildOpsRequest(cluster, opsType, specificOps) - - if options.preflight != nil { - res, err := options.preflight.decide(ctx, c, ops) - if err != nil { - return nil, fmt.Errorf("preflight check for opsruqest %s failed: %w", ops.Name, err) - } - - if res.Decision == preflightSkip { - return nil, ErrCreateOpsSkipped - } - } - - if err := c.Create(ctx, ops); err != nil { - if errors.IsAlreadyExists(err) { - return nil, ErrCreateOpsSkipped - } - return nil, fmt.Errorf("create opsrequest %s: %w", ops.Name, err) - } - - return ops, nil -} - -// buildOpsRequest 构造 OpsRequest 对象 -func buildOpsRequest( - cluster *kbappsv1.Cluster, - opsType opsv1alpha1.OpsType, - specificOps opsv1alpha1.SpecificOpsRequest, -) *opsv1alpha1.OpsRequest { - name := makeOpsRequestName(cluster.Name, opsType) - - serviceID := cluster.GetLabels()[index.ServiceIDLabel] - - labels := map[string]string{ - constant.AppInstanceLabelKey: cluster.Name, - constant.OpsRequestTypeLabelKey: string(opsType), - index.ServiceIDLabel: serviceID, - } - - ops := &opsv1alpha1.OpsRequest{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: cluster.Namespace, - Labels: labels, - }, - Spec: opsv1alpha1.OpsRequestSpec{ - ClusterName: cluster.Name, - Type: opsType, - TimeoutSeconds: &opsTimeoutSecond, - PreConditionDeadlineSeconds: &opsPreConditionDeadline, - - SpecificOpsRequest: specificOps, - }, - } - - // 依据 opsType 设置不同的 spec 字段 - switch opsType { - case opsv1alpha1.RestoreType: - // Restore 中 ClusterName 为通过备份恢复的 Cluster 的名称,会创建一个新的 Cluster, - // 应当按照 {cluster.Name(去除后缀)}-restore-{四位随机后缀}" 的格式 - ops.Spec.ClusterName = generateRestoredClusterName(cluster.Name) - case opsv1alpha1.VerticalScalingType, opsv1alpha1.HorizontalScalingType: - // 对于伸缩操作,应当绕过系统预检查 - ops.Spec.Force = true - case opsv1alpha1.RestartType, opsv1alpha1.StopType: - // 对于生命周期操作,应当绕过系统预检查并且不再排队(启动不支持 Force) - ops.Spec.Force = true - ops.Spec.EnqueueOnForce = false - } - - return ops -} - -func applyCreateOptions(opts ...createOption) *createOpts { - o := &createOpts{} - for _, f := range opts { - if f != nil { - f(o) - } - } - return o -} - -// makeOpsRequestName 生成 OpsRequest 名称 -// 格式:{clustername}-{opsType}-{timestamp} -func makeOpsRequestName(clusterName string, opsType opsv1alpha1.OpsType) string { - timestamp := time.Now().UnixNano() - return fmt.Sprintf("%s-%s-%x", clusterName, strings.ToLower(string(opsType)), timestamp) -} - -// getOpsRequestsByIndex 使用索引查询 OpsRequest,失败时回退到标签查询 -func getOpsRequestsByIndex(ctx context.Context, c client.Client, namespace, clusterName string, opsType opsv1alpha1.OpsType) ([]opsv1alpha1.OpsRequest, error) { - var list opsv1alpha1.OpsRequestList - - indexKey := fmt.Sprintf("%s/%s/%s", namespace, clusterName, opsType) - if err := c.List(ctx, &list, client.MatchingFields{index.NamespaceClusterOpsTypeField: indexKey}); err == nil { - return list.Items, nil - } - - if err := c.List(ctx, &list, - client.InNamespace(namespace), - client.MatchingLabels(map[string]string{ - constant.AppInstanceLabelKey: clusterName, - constant.OpsRequestTypeLabelKey: string(opsType), - }), - ); err != nil { - return nil, fmt.Errorf("list opsrequests for preflight: %w", err) - } - - return list.Items, nil -} - -// isOpsRequestNonBlocking 检查 OpsRequest 是否会阻塞其他 OpsRequest -func isOpsRequestNonBlocking(ops *opsv1alpha1.OpsRequest) bool { - phase := ops.Status.Phase - return phase == opsv1alpha1.OpsSucceedPhase || - phase == opsv1alpha1.OpsCancelledPhase || - phase == opsv1alpha1.OpsFailedPhase || - phase == opsv1alpha1.OpsAbortedPhase || - phase == opsv1alpha1.OpsCancellingPhase -} - -// generateRestoredClusterName 生成 restore cluster 的名称 -// 格式:{cluster.Name(去除后缀)}-restore-{四位随机后缀} -func generateRestoredClusterName(originalClusterName string) string { - var baseName string - - // 避免重复叠加 restore 后缀 - if strings.Contains(originalClusterName, "-restore-") { - restoreIndex := strings.LastIndex(originalClusterName, "-restore-") - baseName = originalClusterName[:restoreIndex] - } else { - lastDash := strings.LastIndex(originalClusterName, "-") - baseName = originalClusterName[:lastDash] - } - - // 生成4位随机后缀 - timestamp := time.Now().UnixNano() - input := fmt.Sprintf("%s-restore-%d", baseName, timestamp) - hash := md5.Sum([]byte(input)) - hashSuffix := fmt.Sprintf("%x", hash[:2]) - - return fmt.Sprintf("%s-restore-%s", baseName, hashSuffix) -} - -// GetAllNonFinalOpsRequests 获取指定集群的所有非终态 OpsRequest -// 不限制操作类型,返回所有可能阻塞的 OpsRequest -func GetAllNonFinalOpsRequests(ctx context.Context, c client.Client, namespace, clusterName string) ([]opsv1alpha1.OpsRequest, error) { - var list opsv1alpha1.OpsRequestList - - if err := c.List(ctx, &list, - client.InNamespace(namespace), - client.MatchingLabels(map[string]string{ - constant.AppInstanceLabelKey: clusterName, - }), - ); err != nil { - return nil, fmt.Errorf("list all opsrequests for cluster %s/%s: %w", namespace, clusterName, err) - } - - var nonFinalOps []opsv1alpha1.OpsRequest - for _, ops := range list.Items { - if !isOpsRequestNonBlocking(&ops) { - nonFinalOps = append(nonFinalOps, ops) - } - } - - return nonFinalOps, nil -} - -// classifyBlockingOps 按照是否支持 cancel 将阻塞的 OpsRequest 分成两组 -// 横向/纵向伸缩 OpsRequest 支持 cancel,其他类型需要通过 expire 处理 -func classifyBlockingOps(blockingOps []opsv1alpha1.OpsRequest) (toCancel, toExpire []*opsv1alpha1.OpsRequest) { - - for i := range blockingOps { - op := &blockingOps[i] - switch op.Spec.Type { - case opsv1alpha1.HorizontalScalingType, opsv1alpha1.VerticalScalingType: - toCancel = append(toCancel, op) - default: - toExpire = append(toExpire, op) - } - } - - return toCancel, toExpire -} - -// CleanupBlockingOps 清理阻塞的 OpsRequest,先取消可优雅处理的,再缩短其余超时 -func CleanupBlockingOps( - ctx context.Context, - c client.Client, - blockingOps []opsv1alpha1.OpsRequest, -) error { - toCancel, toExpire := classifyBlockingOps(blockingOps) - - group, gctx := errgroup.WithContext(ctx) - - if len(toCancel) > 0 { - group.Go(func() error { - if err := cancelOpsRequests(gctx, c, toCancel); err != nil { - return fmt.Errorf("cancel blocking scaling operations: %w", err) - } - return nil - }) - } - - if len(toExpire) > 0 { - group.Go(func() error { - if err := expireOpsRequests(gctx, c, toExpire); err != nil { - return fmt.Errorf("expire blocking operations: %w", err) - } - return nil - }) - } - - return group.Wait() -} - -// cancelOpsRequests 取消所有给定的 OpsRequest -func cancelOpsRequests(ctx context.Context, c client.Client, toCancel []*opsv1alpha1.OpsRequest) error { - if len(toCancel) == 0 { - return nil - } - - group, gctx := errgroup.WithContext(ctx) - group.SetLimit(opsCleanupConcurrency) - - for i := range toCancel { - op := toCancel[i] - group.Go(func() error { - if err := setOpsRequestToCancel(gctx, c, op); err != nil { - return fmt.Errorf("failed to cancel opsrequest %s: %w", op.Name, err) - } - return nil - }) - } - - return group.Wait() -} - -// expireOpsRequests 将给定的 OpsRequest 的 timeoutSeconds 缩短到 1 秒,使其快速结束 -func expireOpsRequests(ctx context.Context, c client.Client, toExpire []*opsv1alpha1.OpsRequest) error { - if len(toExpire) == 0 { - return nil - } - - group, gctx := errgroup.WithContext(ctx) - group.SetLimit(opsCleanupConcurrency) - - for i := range toExpire { - op := toExpire[i] - group.Go(func() error { - if err := shortenOpsRequestTimeout(gctx, c, op); err != nil { - return fmt.Errorf("failed to shorten timeout for opsrequest %s: %w", op.Name, err) - } - return nil - }) - } - - return group.Wait() -} - -// setOpsRequestToCancel 设置单个 OpsRequest 为 cancel: true -// -// -func setOpsRequestToCancel(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - current := &opsv1alpha1.OpsRequest{} - if err := c.Get(ctx, client.ObjectKeyFromObject(ops), current); err != nil { - if errors.IsNotFound(err) { - return nil - } - return err - } - - // 检查操作是否已经处于终态或已经被取消 - if isOpsRequestNonBlocking(current) || current.Spec.Cancel { - return nil - } - - // 构造 Strategic Merge Patch,参考 associateToKubeBlocksComponent 的方式 - patchData := `{ - "spec": { - "cancel": true - } - }` - - return c.Patch(ctx, current, client.RawPatch(types.MergePatchType, []byte(patchData))) - }) -} - -// shortenOpsRequestTimeout 缩短 OpsRequest 的 timeoutSeconds 到 1 秒,使其快速结束 -// -// -func shortenOpsRequestTimeout(ctx context.Context, c client.Client, ops *opsv1alpha1.OpsRequest) error { - return retry.RetryOnConflict(retry.DefaultRetry, func() error { - current := &opsv1alpha1.OpsRequest{} - if err := c.Get(ctx, client.ObjectKeyFromObject(ops), current); err != nil { - if errors.IsNotFound(err) { - return nil - } - return err - } - - if isOpsRequestNonBlocking(current) { - return nil - } - - if current.Spec.TimeoutSeconds != nil && *current.Spec.TimeoutSeconds <= 1 { - return nil - } - - patchData := `{ - "spec": { - "timeoutSeconds": 1 - } - }` - - return c.Patch(ctx, current, client.RawPatch(types.MergePatchType, []byte(patchData))) - }) -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go b/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go deleted file mode 100644 index a2277b41e..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go +++ /dev/null @@ -1,832 +0,0 @@ -package kbkit - -import ( - "context" - "errors" - "testing" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.opsrequest.preflight-arbitration -func TestUniqueOpsDecide(t *testing.T) { - tests := []struct { - name string - setupBgOps func(client.Client) error - expectDecision preflightDecision - expectError bool - errorMsg string - }{ - { - name: "no_existing_ops", - setupBgOps: func(client.Client) error { return nil }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "all_ops_non_blocking", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("succeeded", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("aborted", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsAbortedPhase). - Build(), - testutil.NewOpsRequestBuilder("cancelled", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build(), - testutil.NewOpsRequestBuilder("cancelling", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCancellingPhase). - WithCancel(). - Build(), - testutil.NewOpsRequestBuilder("failed", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "all_non_blocking_ops_cancelled", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-2", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "all_non_blocking_ops_failed", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-3", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "all_non_blocking_ops_aborted", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-4", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsAbortedPhase). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "all_non_blocking_ops_cancelling", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-5", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCancellingPhase). - WithCancel(). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "multiple_non_blocking_ops", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("succeeded-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - testutil.NewOpsRequestBuilder("cancelled-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build(), - testutil.NewOpsRequestBuilder("failed-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsFailedPhase). - Build(), - }) - }, - expectDecision: preflightProceed, - expectError: false, - }, - { - name: "has_running_ops", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-running", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }) - }, - expectDecision: preflightSkip, - expectError: false, - }, - { - name: "has_pending_ops", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-pending", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build(), - }) - }, - expectDecision: preflightSkip, - expectError: false, - }, - { - name: "has_creating_ops", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("test-ops-creating", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsCreatingPhase). - Build(), - }) - }, - expectDecision: preflightSkip, - expectError: false, - }, - { - name: "mixed_blocking_and_non_blocking", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - // 一个成功的(非阻塞) - testutil.NewOpsRequestBuilder("succeeded-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build(), - // 一个运行中的(阻塞) - testutil.NewOpsRequestBuilder("running-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }) - }, - expectDecision: preflightSkip, // 有任何阻塞的就要跳过 - expectError: false, - }, - { - name: "multiple_blocking_ops", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("running-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - testutil.NewOpsRequestBuilder("pending-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build(), - }) - }, - expectDecision: preflightSkip, - expectError: false, - }, - { - name: "different_ops_type_should_not_interfere", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("different-type-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.HorizontalScalingType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }) - }, - expectDecision: preflightProceed, // 不应该被阻塞 - expectError: false, - }, - { - name: "different_cluster_should_not_interfere", - setupBgOps: func(c client.Client) error { - ctx := context.Background() - return testutil.CreateObjects(ctx, c, []client.Object{ - testutil.NewOpsRequestBuilder("different-cluster-ops", testutil.TestNamespace). - WithClusterName("different-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("different-cluster"). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build(), - }) - }, - expectDecision: preflightProceed, // 不应该被阻塞 - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := testutil.NewFakeClient() - ctx := context.Background() - - cluster := testutil.NewMySQLCluster("test-cluster", testutil.TestNamespace). - WithServiceID(testutil.TestServiceID). - Build() - require.NoError(t, client.Create(ctx, cluster)) - - err := tt.setupBgOps(client) - require.NoError(t, err, "setup OpsRequests failed") - - targetOps := testutil.NewOpsRequestBuilder("target-ops", testutil.TestNamespace). - WithClusterName("test-cluster"). - WithType(opsv1alpha1.VolumeExpansionType). - WithInstanceLabel("test-cluster"). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build() - - uniqueOpsChecker := uniqueOps{} - result, err := uniqueOpsChecker.decide(ctx, client, targetOps) - - if tt.expectError { - assert.Error(t, err) - if tt.errorMsg != "" { - assert.Contains(t, err.Error(), tt.errorMsg) - } - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectDecision, result.Decision, "Decision 不符合预期") - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.preflight-arbitration -func TestPriorityOpsDecide(t *testing.T) { - ctx := context.Background() - clusterName := "test-cluster" - - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - targetType opsv1alpha1.OpsType - expectDecision preflightDecision - expectErr bool - errContains string - verify func(t *testing.T, c client.Client) - }{ - { - name: "no_non_final_ops", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - succeeded := testutil.NewOpsRequestBuilder("succeeded", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithPhase(opsv1alpha1.OpsSucceedPhase). - WithInstanceLabel(clusterName). - Build() - - cancelled := testutil.NewOpsRequestBuilder("cancelled", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{succeeded, cancelled}) - }, - targetType: opsv1alpha1.RestartType, - expectDecision: preflightProceed, - }, - { - name: "blocking_ops_cleanup_succeeds", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - blockingToCancel := testutil.NewOpsRequestBuilder("scaling-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - blockingToExpire := testutil.NewOpsRequestBuilder("restart-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithPhase(opsv1alpha1.OpsPendingPhase). - WithInstanceLabel(clusterName). - Build() - blockingToExpire.Spec.TimeoutSeconds = ptr.To(int32(120)) - - return testutil.CreateObjects(ctx, c, []client.Object{blockingToCancel, blockingToExpire}) - }, - targetType: opsv1alpha1.StopType, - expectDecision: preflightCleanupAndProceed, - verify: func(t *testing.T, c client.Client) { - updatedCancel := &opsv1alpha1.OpsRequest{} - require.NoError(t, c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "scaling-block"}, updatedCancel)) - assert.True(t, updatedCancel.Spec.Cancel, "横向伸缩阻塞操作应被标记为取消") - - updatedExpire := &opsv1alpha1.OpsRequest{} - require.NoError(t, c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "restart-block"}, updatedExpire)) - if assert.NotNil(t, updatedExpire.Spec.TimeoutSeconds) { - assert.Equal(t, int32(1), *updatedExpire.Spec.TimeoutSeconds, "非伸缩阻塞操作的 timeoutSeconds 应被缩短") - } - }, - }, - { - name: "list_existing_ops_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list failed")). - Build() - }, - targetType: opsv1alpha1.RestartType, - expectErr: true, - errContains: "get existing opsrequests", - }, - { - name: "cleanup_blocking_ops_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithPatchError(errors.New("patch failed")). - Build() - }, - setup: func(c client.Client) error { - blockingToCancel := testutil.NewOpsRequestBuilder("scaling-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - blockingToExpire := testutil.NewOpsRequestBuilder("restart-block", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - blockingToExpire.Spec.TimeoutSeconds = ptr.To(int32(300)) - - return testutil.CreateObjects(ctx, c, []client.Object{blockingToCancel, blockingToExpire}) - }, - targetType: opsv1alpha1.RestartType, - expectErr: true, - errContains: "patch failed", - }, - } - - for _, tt := range testCases { - tt := tt - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - targetOps := testutil.NewOpsRequestBuilder("priority", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(tt.targetType). - WithPhase(opsv1alpha1.OpsCreatingPhase). - WithInstanceLabel(clusterName). - Build() - result, err := (priorityOps{}).decide(ctx, k8sClient, targetOps) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - assert.Equal(t, tt.expectDecision, result.Decision) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} - -// verifyCancelledOps 验证指定的 OpsRequest 是否被标记为 cancel -func verifyCancelledOps(t *testing.T, c client.Client, opsNames []string) { - ctx := context.Background() - for _, name := range opsNames { - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: name}, ops) - require.NoError(t, err, "failed to get OpsRequest %s", name) - assert.True(t, ops.Spec.Cancel, "OpsRequest %s should be marked as cancelled", name) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.preflight-arbitration -func TestCancelOpsDecide(t *testing.T) { - ctx := context.Background() - clusterName := "test-cluster" - - testCases := []struct { - name string - clientSetup func() client.Client - setup func(client.Client) error - targetType opsv1alpha1.OpsType - expectDecision preflightDecision - expectErr bool - errContains string - verify func(t *testing.T, c client.Client) - }{ - { - name: "no_existing_ops", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { return nil }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - }, - { - name: "only_non_blocking_ops", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - succeeded := testutil.NewOpsRequestBuilder("succeeded", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsSucceedPhase). - WithInstanceLabel(clusterName). - Build() - - cancelled := testutil.NewOpsRequestBuilder("cancelled", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - WithInstanceLabel(clusterName). - Build() - - failed := testutil.NewOpsRequestBuilder("failed", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsFailedPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{succeeded, cancelled, failed}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - }, - { - name: "blocking_ops_cancel_success", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - running := testutil.NewOpsRequestBuilder("running-scale", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - pending := testutil.NewOpsRequestBuilder("pending-scale", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsPendingPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{running, pending}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - verifyCancelledOps(t, c, []string{"running-scale", "pending-scale"}) - }, - }, - { - name: "mixed_blocking_and_non_blocking", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - // 非阻塞操作:已成功 - succeeded := testutil.NewOpsRequestBuilder("succeeded", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsSucceedPhase). - WithInstanceLabel(clusterName). - Build() - - // 非阻塞操作:正在取消中 - cancelling := testutil.NewOpsRequestBuilder("cancelling", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsCancellingPhase). - WithCancel(). - WithInstanceLabel(clusterName). - Build() - - // 阻塞操作:运行中 - running := testutil.NewOpsRequestBuilder("running", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{succeeded, cancelling, running}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - // 只有运行中的操作应该被取消 - verifyCancelledOps(t, c, []string{"running"}) - - // 验证其他操作保持不变 - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "cancelling"}, ops) - require.NoError(t, err) - assert.True(t, ops.Spec.Cancel, "already cancelling ops should remain cancelled") - }, - }, - { - name: "multiple_blocking_ops", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - running := testutil.NewOpsRequestBuilder("running", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - pending := testutil.NewOpsRequestBuilder("pending", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsPendingPhase). - WithInstanceLabel(clusterName). - Build() - - creating := testutil.NewOpsRequestBuilder("creating", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsCreatingPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{running, pending, creating}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - verifyCancelledOps(t, c, []string{"running", "pending", "creating"}) - }, - }, - { - name: "different_ops_type_isolation", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - // 不同类型的运行操作不应干扰 - restart := testutil.NewOpsRequestBuilder("restart", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.RestartType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - volumeExpansion := testutil.NewOpsRequestBuilder("volume", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.VolumeExpansionType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{restart, volumeExpansion}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - // 验证不同类型的操作没有被取消 - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "restart"}, ops) - require.NoError(t, err) - assert.False(t, ops.Spec.Cancel, "different type ops should not be cancelled") - - err = c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "volume"}, ops) - require.NoError(t, err) - assert.False(t, ops.Spec.Cancel, "different type ops should not be cancelled") - }, - }, - { - name: "different_cluster_isolation", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - // 不同集群的同类型操作不应干扰 - otherCluster := testutil.NewOpsRequestBuilder("other-cluster-scale", testutil.TestNamespace). - WithClusterName("other-cluster"). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel("other-cluster"). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{otherCluster}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - // 验证不同集群的操作没有被取消 - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "other-cluster-scale"}, ops) - require.NoError(t, err) - assert.False(t, ops.Spec.Cancel, "different cluster ops should not be cancelled") - }, - }, - { - name: "vertical_scaling_target_type", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - running := testutil.NewOpsRequestBuilder("running-vertical", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.VerticalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{running}) - }, - targetType: opsv1alpha1.VerticalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - verifyCancelledOps(t, c, []string{"running-vertical"}) - }, - }, - { - name: "list_ops_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list failed")). - Build() - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectErr: true, - errContains: "list opsrequests for preflight", - }, - { - name: "cancel_ops_error", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithPatchError(errors.New("mock patch failed")). - Build() - }, - setup: func(c client.Client) error { - running := testutil.NewOpsRequestBuilder("failing-ops", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{running}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectErr: true, - errContains: "failed to gracefully cancel", - }, - { - name: "not_found_error_handled", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - // 不创建任何 OpsRequest,模拟 NotFound 情况 - return nil - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - }, - { - name: "already_cancelled_ops_ignored", - clientSetup: func() client.Client { return testutil.NewFakeClientWithIndexes() }, - setup: func(c client.Client) error { - // 已经被标记为取消的操作 - alreadyCancelled := testutil.NewOpsRequestBuilder("already-cancelled", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - WithCancel(). - WithInstanceLabel(clusterName). - Build() - - return testutil.CreateObjects(ctx, c, []client.Object{alreadyCancelled}) - }, - targetType: opsv1alpha1.HorizontalScalingType, - expectDecision: preflightProceed, - verify: func(t *testing.T, c client.Client) { - // 验证已取消的操作保持 cancel 状态 - ops := &opsv1alpha1.OpsRequest{} - err := c.Get(ctx, types.NamespacedName{Namespace: testutil.TestNamespace, Name: "already-cancelled"}, ops) - require.NoError(t, err) - assert.True(t, ops.Spec.Cancel, "already cancelled ops should remain cancelled") - }, - }, - } - - for _, tt := range testCases { - tt := tt - t.Run(tt.name, func(t *testing.T) { - k8sClient := tt.clientSetup() - - if tt.setup != nil { - require.NoError(t, tt.setup(k8sClient)) - } - - targetOps := testutil.NewOpsRequestBuilder("target", testutil.TestNamespace). - WithClusterName(clusterName). - WithType(tt.targetType). - WithPhase(opsv1alpha1.OpsCreatingPhase). - WithInstanceLabel(clusterName). - Build() - - result, err := (cancelOps{}).decide(ctx, k8sClient, targetOps) - - if tt.expectErr { - require.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - return - } - - require.NoError(t, err) - assert.Equal(t, tt.expectDecision, result.Decision) - - if tt.verify != nil { - tt.verify(t, k8sClient) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go b/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go deleted file mode 100644 index a320f63a2..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go +++ /dev/null @@ -1,1081 +0,0 @@ -package kbkit_test - -import ( - "context" - "errors" - "strings" - "testing" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/stretchr/testify/assert" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateLifecycleOpsRequest(t *testing.T) { - tests := []struct { - name string - cluster *kbappsv1.Cluster - opsType opsv1alpha1.OpsType - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - cluster: nil, - opsType: opsv1alpha1.RestartType, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful restart", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - opsType: opsv1alpha1.RestartType, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "successful stop", - cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - opsType: opsv1alpha1.StopType, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "successful start", - cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - opsType: opsv1alpha1.StartType, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error should return error", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - opsType: opsv1alpha1.RestartType, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create opsrequest", - }, - { - name: "already exists error should be handled gracefully", - cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - opsType: opsv1alpha1.RestartType, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateLifecycleOpsRequest(context.Background(), client, tt.cluster, tt.opsType) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateBackupOpsRequest(t *testing.T) { - tests := []struct { - name string - cluster *kbappsv1.Cluster - backupMethod string - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - cluster: nil, - backupMethod: "", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful backup with default method", - cluster: testutil.NewMySQLCluster("mysql-cluster", "default").Build(), - backupMethod: "", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "backup for PostgreSQL cluster", - cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - backupMethod: "", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "backup for MySQL cluster with different name", - cluster: testutil.NewMySQLCluster("mysql-cluster-2", "default").Build(), - backupMethod: "", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "custom backup", - cluster: testutil.NewMySQLCluster("custom-cluster", "default").Build(), - backupMethod: "xtrabackup", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - backupMethod: "", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error", - cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - backupMethod: "", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateBackupOpsRequest(context.Background(), client, tt.cluster, tt.backupMethod) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateHorizontalScalingOpsRequest(t *testing.T) { - tests := []struct { - name string - params model.HorizontalScalingOpsParams - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - params: model.HorizontalScalingOpsParams{ - Cluster: nil, - Components: []model.ComponentHorizontalScaling{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful horizontal scaling with single component", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentHorizontalScaling{ - { - Name: "mysql", - DeltaReplicas: 1, - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "horizontal scaling with multiple components", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewPostgreSQLCluster("redis-cluster", "default").Build(), - Components: []model.ComponentHorizontalScaling{ - { - Name: "redis", - DeltaReplicas: 2, - }, - { - Name: "redis-sentinel", - DeltaReplicas: 1, - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "scale down", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("scale-down-cluster", "default").Build(), - Components: []model.ComponentHorizontalScaling{ - { - Name: "mysql", - DeltaReplicas: -1, - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "empty components list should still work", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("empty-components", "default").Build(), - Components: []model.ComponentHorizontalScaling{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error should return error", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentHorizontalScaling{ - { - Name: "mysql", - DeltaReplicas: 1, - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error should be handled gracefully", - params: model.HorizontalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - Components: []model.ComponentHorizontalScaling{ - { - Name: "mysql", - DeltaReplicas: 1, - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateHorizontalScalingOpsRequest(context.Background(), client, tt.params) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateVerticalScalingOpsRequest(t *testing.T) { - tests := []struct { - name string - params model.VerticalScalingOpsParams - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - params: model.VerticalScalingOpsParams{ - Cluster: nil, - Components: []model.ComponentVerticalScaling{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful vertical scaling with single component", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentVerticalScaling{ - { - Name: "mysql", - CPU: resource.MustParse("2"), - Memory: resource.MustParse("4Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "vertical scaling with multiple components", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewPostgreSQLCluster("redis-cluster", "default").Build(), - Components: []model.ComponentVerticalScaling{ - { - Name: "redis", - CPU: resource.MustParse("1"), - Memory: resource.MustParse("2Gi"), - }, - { - Name: "redis-sentinel", - CPU: resource.MustParse("500m"), - Memory: resource.MustParse("1Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "scale down resources", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - Components: []model.ComponentVerticalScaling{ - { - Name: "postgresql", - CPU: resource.MustParse("500m"), - Memory: resource.MustParse("1Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "empty components list should still work", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("empty-components", "default").Build(), - Components: []model.ComponentVerticalScaling{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error should return error", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentVerticalScaling{ - { - Name: "mysql", - CPU: resource.MustParse("2"), - Memory: resource.MustParse("4Gi"), - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error should be handled gracefully", - params: model.VerticalScalingOpsParams{ - Cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - Components: []model.ComponentVerticalScaling{ - { - Name: "mysql", - CPU: resource.MustParse("2"), - Memory: resource.MustParse("4Gi"), - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateVerticalScalingOpsRequest(context.Background(), client, tt.params) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateVolumeExpansionOpsRequest(t *testing.T) { - tests := []struct { - name string - params model.VolumeExpansionOpsParams - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - params: model.VolumeExpansionOpsParams{ - Cluster: nil, - Components: []model.ComponentVolumeExpansion{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful volume expansion with single component", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentVolumeExpansion{ - { - Name: "mysql", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("20Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "volume expansion with multiple components", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewPostgreSQLCluster("redis-cluster", "default").Build(), - Components: []model.ComponentVolumeExpansion{ - { - Name: "redis", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("30Gi"), - }, - { - Name: "redis-sentinel", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("10Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "multiple volume claim templates for single component", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - Components: []model.ComponentVolumeExpansion{ - { - Name: "postgresql", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("50Gi"), - }, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "empty components list should still work", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewMySQLCluster("empty-components", "default").Build(), - Components: []model.ComponentVolumeExpansion{}, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error should return error", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - Components: []model.ComponentVolumeExpansion{ - { - Name: "mysql", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("20Gi"), - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error should be handled gracefully", - params: model.VolumeExpansionOpsParams{ - Cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - Components: []model.ComponentVolumeExpansion{ - { - Name: "mysql", - VolumeClaimTemplateName: "data", - Storage: resource.MustParse("25Gi"), - }, - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateVolumeExpansionOpsRequest(context.Background(), client, tt.params) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateParameterChangeOpsRequest(t *testing.T) { - tests := []struct { - name string - cluster *kbappsv1.Cluster - parameters []model.ParameterEntry - clientSetup func() client.Client - expectError bool - errorContains string - }{ - { - name: "nil cluster should return error", - cluster: nil, - parameters: []model.ParameterEntry{}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful parameter change with single parameter", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - parameters: []model.ParameterEntry{ - { - Name: "max_connections", - Value: "200", - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "parameter change with multiple parameters", - cluster: testutil.NewPostgreSQLCluster("pg-cluster", "default").Build(), - parameters: []model.ParameterEntry{ - { - Name: "max_connections", - Value: "300", - }, - { - Name: "shared_buffers", - Value: "256MB", - }, - { - Name: "log_statement", - Value: "all", - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "empty parameters list should still work", - cluster: testutil.NewMySQLCluster("empty-params", "default").Build(), - parameters: []model.ParameterEntry{}, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "parameter with numeric value", - cluster: testutil.NewPostgreSQLCluster("redis-cluster", "default").Build(), - parameters: []model.ParameterEntry{ - { - Name: "timeout", - Value: 300, - }, - { - Name: "databases", - Value: 16, - }, - }, - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - }, - { - name: "client create error should return error", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - parameters: []model.ParameterEntry{ - { - Name: "max_connections", - Value: "200", - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error should be handled gracefully", - cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - parameters: []model.ParameterEntry{ - { - Name: "max_connections", - Value: "200", - }, - }, - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - err := kbkit.CreateParameterChangeOpsRequest(context.Background(), client, tt.cluster, tt.parameters) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.create-supported-ops -func TestCreateRestoreOpsRequest(t *testing.T) { - restoredNamePrefix := func(original string) string { - if strings.Contains(original, "-restore-") { - idx := strings.LastIndex(original, "-restore-") - return original[:idx] + "-restore-" - } - if lastDash := strings.LastIndex(original, "-"); lastDash > 0 { - return original[:lastDash] + "-restore-" - } - return original + "-restore-" - } - - tests := []struct { - name string - cluster *kbappsv1.Cluster - backupName string - clientSetup func() client.Client - expectError bool - errorContains string - validate func(t *testing.T, result *opsv1alpha1.OpsRequest, err error) - }{ - { - name: "nil cluster should return error", - cluster: nil, - backupName: "test-backup", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: true, - errorContains: "cluster is required", - }, - { - name: "successful restore operation", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - backupName: "mysql-backup-20240101", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - validate: func(t *testing.T, result *opsv1alpha1.OpsRequest, err error) { - assert.NotNil(t, result) - assert.Equal(t, opsv1alpha1.RestoreType, result.Spec.Type) - assert.Equal(t, "default", result.Namespace) - assert.NotEmpty(t, result.Name) - assert.True(t, strings.HasPrefix(result.Name, "test-cluster-restore-")) - - // 验证恢复规格 - assert.NotNil(t, result.Spec.Restore) - assert.Equal(t, "mysql-backup-20240101", result.Spec.Restore.BackupName) - }, - }, - { - name: "empty backup name should succeed, kubeblocks will handle it", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - backupName: "", - clientSetup: func() client.Client { return testutil.NewFakeClient() }, - expectError: false, - validate: func(t *testing.T, result *opsv1alpha1.OpsRequest, err error) { - assert.Equal(t, "", result.Spec.Restore.BackupName) - }, - }, - { - name: "client create error should return error", - cluster: testutil.NewMySQLCluster("test-cluster", "default").Build(), - backupName: "test-backup", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(errors.New("create failed")). - Build() - }, - expectError: true, - errorContains: "create failed", - }, - { - name: "already exists error should be handled gracefully", - cluster: testutil.NewMySQLCluster("existing-cluster", "default").Build(), - backupName: "existing-backup", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithCreateError(apierrors.NewAlreadyExists(schema.GroupResource{}, "test-resource")). - Build() - }, - expectError: true, - errorContains: "operation skipped", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - result, err := kbkit.CreateRestoreOpsRequest(context.Background(), client, tt.cluster, tt.backupName) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - } else { - assert.NoError(t, err) - if assert.NotNil(t, result) { - assert.True(t, strings.HasPrefix(result.Spec.ClusterName, restoredNamePrefix(tt.cluster.Name))) - } - if tt.validate != nil { - tt.validate(t, result, err) - } - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.blocking-ops-management -func TestGetAllNonFinalOpsRequests(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - clientSetup func() client.Client - namespace string - clusterName string - expectError bool - errorContains string - verify func(*testing.T, []opsv1alpha1.OpsRequest) - }{ - { - name: "list error should bubble up", - clientSetup: func() client.Client { - return testutil.NewErrorClientBuilder(). - WithListError(errors.New("list failed")). - Build() - }, - namespace: testutil.TestNamespace, - clusterName: "test-cluster", - expectError: true, - errorContains: "list all opsrequests", - }, - { - name: "filter out non blocking operations", - clientSetup: func() client.Client { - namespace := testutil.TestNamespace - clusterName := "filter-cluster" - timeout := int32(600) - - runningOps := testutil.NewOpsRequestBuilder("running-ops", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - runningOps.Spec.TimeoutSeconds = &timeout - - pendingOps := testutil.NewOpsRequestBuilder("pending-ops", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.VerticalScalingType). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build() - pendingOps.Spec.TimeoutSeconds = &timeout - - succeededOps := testutil.NewOpsRequestBuilder("succeeded-ops", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.BackupType). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build() - - return testutil.NewFakeClient(runningOps, pendingOps, succeededOps) - }, - namespace: testutil.TestNamespace, - clusterName: "filter-cluster", - verify: func(t *testing.T, ops []opsv1alpha1.OpsRequest) { - assert.Len(t, ops, 2) - var names []string - for _, op := range ops { - names = append(names, op.Name) - } - assert.ElementsMatch(t, []string{"running-ops", "pending-ops"}, names) - }, - }, - { - name: "all non blocking operations should return empty list", - clientSetup: func() client.Client { - namespace := testutil.TestNamespace - clusterName := "non-blocking" - - succeededOps := testutil.NewOpsRequestBuilder("ops-succeed", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.BackupType). - WithPhase(opsv1alpha1.OpsSucceedPhase). - Build() - - cancelledOps := testutil.NewOpsRequestBuilder("ops-cancelled", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.RestoreType). - WithPhase(opsv1alpha1.OpsCancelledPhase). - WithCancel(). - Build() - - return testutil.NewFakeClient(succeededOps, cancelledOps) - }, - namespace: testutil.TestNamespace, - clusterName: "non-blocking", - verify: func(t *testing.T, ops []opsv1alpha1.OpsRequest) { - assert.Empty(t, ops) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := tt.clientSetup() - opsList, err := kbkit.GetAllNonFinalOpsRequests(ctx, client, tt.namespace, tt.clusterName) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - return - } - - assert.NoError(t, err) - if tt.verify != nil { - tt.verify(t, opsList) - } - }) - } -} - -// capability_id: rainbond.kb-adapter.opsrequest.blocking-ops-management -func TestCleanupBlockingOps(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - prepare func() (client.Client, []opsv1alpha1.OpsRequest) - expectError bool - errorContains string - verify func(*testing.T, client.Client) - }{ - { - name: "empty blocking list should succeed", - prepare: func() (client.Client, []opsv1alpha1.OpsRequest) { - return testutil.NewFakeClient(), nil - }, - }, - { - name: "cancel supported ops and expire the rest", - prepare: func() (client.Client, []opsv1alpha1.OpsRequest) { - namespace := testutil.TestNamespace - clusterName := "cleanup-cluster" - timeoutScale := int32(600) - timeoutBackup := int32(900) - - scalingOps := testutil.NewOpsRequestBuilder("scale-ops", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - scalingOps.Spec.TimeoutSeconds = &timeoutScale - - blockingBackup := testutil.NewOpsRequestBuilder("backup-ops", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.BackupType). - WithPhase(opsv1alpha1.OpsPendingPhase). - Build() - blockingBackup.Spec.TimeoutSeconds = &timeoutBackup - - client := testutil.NewFakeClient(scalingOps.DeepCopy(), blockingBackup.DeepCopy()) - - return client, []opsv1alpha1.OpsRequest{*scalingOps, *blockingBackup} - }, - verify: func(t *testing.T, c client.Client) { - ctx := context.Background() - namespace := testutil.TestNamespace - - scaling := &opsv1alpha1.OpsRequest{} - assert.NoError(t, c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: "scale-ops"}, scaling)) - assert.True(t, scaling.Spec.Cancel) - - backup := &opsv1alpha1.OpsRequest{} - assert.NoError(t, c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: "backup-ops"}, backup)) - if assert.NotNil(t, backup.Spec.TimeoutSeconds) { - assert.Equal(t, int32(1), *backup.Spec.TimeoutSeconds) - } - }, - }, - { - name: "cancel failure should return error", - prepare: func() (client.Client, []opsv1alpha1.OpsRequest) { - namespace := testutil.TestNamespace - clusterName := "cancel-error" - timeout := int32(600) - - scalingOps := testutil.NewOpsRequestBuilder("scale-error", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.HorizontalScalingType). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - scalingOps.Spec.TimeoutSeconds = &timeout - - client := testutil.NewErrorClientBuilder(scalingOps.DeepCopy()). - WithPatchError(errors.New("patch failed")). - Build() - - return client, []opsv1alpha1.OpsRequest{*scalingOps} - }, - expectError: true, - errorContains: "cancel blocking scaling operations", - }, - { - name: "expire failure should return error", - prepare: func() (client.Client, []opsv1alpha1.OpsRequest) { - namespace := testutil.TestNamespace - clusterName := "expire-error" - timeout := int32(1200) - - backupOps := testutil.NewOpsRequestBuilder("backup-error", namespace). - WithClusterName(clusterName). - WithInstanceLabel(clusterName). - WithType(opsv1alpha1.BackupType). - WithPhase(opsv1alpha1.OpsRunningPhase). - Build() - backupOps.Spec.TimeoutSeconds = &timeout - - client := testutil.NewErrorClientBuilder(backupOps.DeepCopy()). - WithPatchError(errors.New("patch failed")). - Build() - - return client, []opsv1alpha1.OpsRequest{*backupOps} - }, - expectError: true, - errorContains: "expire blocking operations", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client, blocking := tt.prepare() - err := kbkit.CleanupBlockingOps(ctx, client, blocking) - - if tt.expectError { - assert.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - return - } - - assert.NoError(t, err) - if tt.verify != nil { - tt.verify(t, client) - } - }) - } -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/util.go b/plugins/kb-adapter-rbdplugin/service/kbkit/util.go deleted file mode 100644 index d2500160f..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/util.go +++ /dev/null @@ -1,104 +0,0 @@ -package kbkit - -import ( - "context" - "fmt" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/index" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// GetClusterByServiceID 通过 service_id 获取对应的 KubeBlocks Cluster -// 优先使用 MatchingFields 索引查询,失败时回退到 MatchingLabels -func GetClusterByServiceID(ctx context.Context, c client.Client, serviceID string) (*kbappsv1.Cluster, error) { - var list kbappsv1.ClusterList - - // 使用 index - if err := c.List(ctx, &list, client.MatchingFields{index.ServiceIDField: serviceID}); err == nil { - switch len(list.Items) { - case 0: - return nil, ErrTargetNotFound - case 1: - return &list.Items[0], nil - default: - return nil, ErrMultipleFounded - } - } - - // 回退到 MatchingLabels - list = kbappsv1.ClusterList{} - if err := c.List(ctx, &list, client.MatchingLabels{index.ServiceIDLabel: serviceID}); err != nil { - return nil, fmt.Errorf("list clusters by service_id %s: %w", serviceID, err) - } - - switch len(list.Items) { - case 0: - return nil, ErrTargetNotFound - case 1: - return &list.Items[0], nil - default: - return nil, ErrMultipleFounded - } -} - -// Paginate 分页, 从 items 中提取指定页的数据 -func Paginate[T any](items []T, page, pageSize int) []T { - if page < 1 || pageSize < 1 || len(items) == 0 { - return nil - } - - offset := (page - 1) * pageSize - if offset >= len(items) { - return nil - } - - end := min(offset+pageSize, len(items)) - return items[offset:end:end] -} - -// ClusterType 获取 Cluster 对应的数据库类型 -func ClusterType(cluster *kbappsv1.Cluster) string { - if cluster == nil { - return "" - } - return cluster.Spec.ClusterDef -} - -// IsSupportBackup 判定给定的数据库类型是否支持备份 -func IsSupportBackup(addon string) bool { - adapter, ok := registry.Cluster[addon] - if !ok { - return false - } - return adapter.Coordinator.GetBackupMethod() != "" -} - -func IsSupportParameter(addon string) bool { - adapter, ok := registry.Cluster[addon] - if !ok { - return false - } - return adapter.Coordinator.GetParametersConfigMap("not support") != nil -} - -// GetAllOpsRequestsByCluster 获取指定集群的所有 OpsRequest -// 包括所有状态的 OpsRequest,用于彻底清理资源或审计目的 -func GetAllOpsRequestsByCluster(ctx context.Context, c client.Client, namespace, clusterName string) ([]opsv1alpha1.OpsRequest, error) { - var list opsv1alpha1.OpsRequestList - - if err := c.List(ctx, &list, - client.InNamespace(namespace), - client.MatchingLabels(map[string]string{ - constant.AppInstanceLabelKey: clusterName, - }), - ); err != nil { - return nil, fmt.Errorf("list all opsrequests for cluster %s/%s: %w", namespace, clusterName, err) - } - - return list.Items, nil -} diff --git a/plugins/kb-adapter-rbdplugin/service/kbkit/validator.go b/plugins/kb-adapter-rbdplugin/service/kbkit/validator.go deleted file mode 100644 index d1349f440..000000000 --- a/plugins/kb-adapter-rbdplugin/service/kbkit/validator.go +++ /dev/null @@ -1,328 +0,0 @@ -package kbkit - -import ( - "fmt" - "strconv" - "strings" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" -) - -// ParameterValidator 参数验证器 -// 负责集中处理参数验证逻辑,支持类型校验、范围校验、枚举校验等 -type ParameterValidator struct { - constraints map[string]model.Parameter -} - -// NewParameterValidator - -func NewParameterValidator(constraints []model.Parameter) *ParameterValidator { - constraintMap := make(map[string]model.Parameter, len(constraints)) - for _, param := range constraints { - constraintMap[param.Name] = param - } - return &ParameterValidator{constraints: constraintMap} -} - -// Validate 验证单个参数变更请求 -// 返回 nil 表示验证通过,否则返回具体的验证错误信息 -func (v *ParameterValidator) Validate(entry model.ParameterEntry) *ParameterValidationError { - // 检查参数是否存在 - constraint, exists := v.constraints[entry.Name] - if !exists { - log.Error("parameter not Exist", log.String("parameter_name", entry.Name), log.Any("constraints", v.constraints[entry.Name])) - return &ParameterValidationError{ - ParameterName: entry.Name, - ErrorCode: ParamNotExist, - ErrorMessage: fmt.Sprintf("parameter '%s' not found in cluster definition", entry.Name), - } - } - - // 检查参数可变性:仅当明确为不可变时拒绝 - // 当允许用户手动添加参数时才会生效,否则现有设计下不会出现 IsImmutable 为 true 的参数 - if constraint.IsImmutable { - return &ParameterValidationError{ - ParameterName: entry.Name, - ErrorCode: ParamImmutable, - ErrorMessage: fmt.Sprintf("parameter '%s' is immutable and cannot be changed", entry.Name), - } - } - - // 检查必需参数 - if constraint.IsRequired && entry.Value == nil { - return &ParameterValidationError{ - ParameterName: entry.Name, - ErrorCode: ParamRequiredMissing, - ErrorMessage: fmt.Sprintf("parameter '%s' is required and cannot be empty", entry.Name), - } - } - - // 如果没有类型信息(Type 为空),说明参数只在列表中声明但没有 schema 定义 - // 跳过类型、范围、枚举校验,只进行 immutable 检查 - if constraint.Type == "" { - log.Debug("parameter has no schema definition, skipping type/range/enum validation", - log.String("parameter", entry.Name)) - return nil - } - - // 验证参数类型 - if err := v.validateType(constraint, entry.Value); err != nil { - return err - } - - // 验证数值范围 - if err := v.validateRange(constraint, entry.Value); err != nil { - log.Error("value out of range", log.Err(err)) - return err - } - - // 验证枚举值 - if err := v.validateEnum(constraint, entry.Value); err != nil { - log.Error("invalid enum value", log.Err(err)) - return err - } - - return nil -} - -// validateType 验证参数类型 -func (v *ParameterValidator) validateType(param model.Parameter, value any) *ParameterValidationError { - if value == nil { - return nil - } - - switch param.Type { - case model.ParameterTypeString: - if _, ok := value.(string); !ok { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' expects string type, got %T", param.Name, value), - } - } - - case model.ParameterTypeInteger, "int32", "int64": - switch v := value.(type) { - case int, int32, int64, uint64, float64: - // 数值类型直接通过 - case string: - // 首先尝试解析为有符号整数 - if _, err := strconv.ParseInt(v, 10, 64); err != nil { - // 如果有符号整数解析失败,尝试无符号整数 - if _, err2 := strconv.ParseUint(v, 10, 64); err2 != nil { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' cannot parse '%s' as integer", param.Name, v), - Cause: err, - } - } - } - default: - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' expects integer type, got %T", param.Name, value), - } - } - - case model.ParameterTypeNumber: - switch v := value.(type) { - case int, int32, int64, float32, float64: - // 数值类型直接通过 - case string: - // 尝试解析字符串为浮点数 - if _, err := strconv.ParseFloat(v, 64); err != nil { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' cannot parse '%s' as number", param.Name, v), - Cause: err, - } - } - default: - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' expects number type, got %T", param.Name, value), - } - } - - case model.ParameterTypeBoolean: - switch v := value.(type) { - case bool: - // 布尔类型直接通过 - case string: - // 尝试解析字符串为布尔值 - if _, err := strconv.ParseBool(v); err != nil { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' cannot parse '%s' as boolean", param.Name, v), - Cause: err, - } - } - default: - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidType, - ErrorMessage: fmt.Sprintf("parameter '%s' expects boolean type, got %T", param.Name, value), - } - } - } - - return nil -} - -// validateRange 验证数值参数的范围约束 -func (v *ParameterValidator) validateRange(param model.Parameter, value any) *ParameterValidationError { - var maxVal, minVal any - if param.MaxValue != nil { - maxVal = *param.MaxValue - } - if param.MinValue != nil { - minVal = *param.MinValue - } - log.Debug("validateRange", - log.Any("maxValue", maxVal), - log.Any("minValue", minVal), - log.Any("value", value), - ) - if value == nil || (param.MinValue == nil && param.MaxValue == nil) { - return nil - } - - // 将值转换为 float64 以便统一比较 - numValue, parseErr := v.convertToFloat64(value) - if parseErr != nil { - return nil - } - - if err := v.validateMinValue(param, numValue); err != nil { - return err - } - - if err := v.validateMaxValue(param, numValue); err != nil { - return err - } - - return nil -} - -// convertToFloat64 将各种数值类型转换为 float64 -func (v *ParameterValidator) convertToFloat64(value any) (float64, error) { - switch v := value.(type) { - case int: - return float64(v), nil - case int32: - return float64(v), nil - case int64: - return float64(v), nil - case uint64: - return float64(v), nil - case float32: - return float64(v), nil - case float64: - return v, nil - case string: - return strconv.ParseFloat(v, 64) - default: - return 0, fmt.Errorf("unsupported type: %T", value) - } -} - -// validateMinValue 验证最小值约束 -func (v *ParameterValidator) validateMinValue(param model.Parameter, numValue float64) *ParameterValidationError { - if param.MinValue == nil { - return nil - } - - minVal := *param.MinValue - if numValue < minVal { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamOutOfRange, - ErrorMessage: fmt.Sprintf("parameter '%s' value %v is less than minimum %v", param.Name, numValue, minVal), - } - } - return nil -} - -// validateMaxValue 验证最大值约束 -func (v *ParameterValidator) validateMaxValue(param model.Parameter, numValue float64) *ParameterValidationError { - if param.MaxValue == nil { - return nil - } - - maxVal := *param.MaxValue - if numValue > maxVal { - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamOutOfRange, - ErrorMessage: fmt.Sprintf("parameter '%s' value %v is greater than maximum %v", param.Name, numValue, maxVal), - } - } - return nil -} - -// validateEnum 验证枚举参数的有效性 -func (v *ParameterValidator) validateEnum(param model.Parameter, value any) *ParameterValidationError { - if value == nil || len(param.EnumValues) == 0 { - return nil - } - - // 将值转换为字符串进行比较 - var valueStr string - switch v := value.(type) { - case string: - valueStr = v - default: - valueStr = fmt.Sprintf("%v", v) - } - - // 检查是否在枚举列表中 - for _, enumValue := range param.EnumValues { - // 去除 JSON 字符串的引号进行比较 - cleanEnum := strings.Trim(enumValue, "\"") - if valueStr == cleanEnum || valueStr == enumValue { - return nil - } - } - - return &ParameterValidationError{ - ParameterName: param.Name, - ErrorCode: ParamInvalidEnum, - ErrorMessage: fmt.Sprintf("parameter '%s' value '%s' is not in allowed values: %v", param.Name, valueStr, param.EnumValues), - } -} - -// ConvertToStringValue 将验证通过的值转换为 *string 格式,供 OpsRequest 使用 -func (v *ParameterValidator) ConvertToStringValue(value any) *string { - if value == nil { - return nil - } - - var strValue string - switch v := value.(type) { - case string: - strValue = v - case bool: - strValue = strconv.FormatBool(v) - case int: - strValue = strconv.Itoa(v) - case int32: - strValue = strconv.FormatInt(int64(v), 10) - case int64: - strValue = strconv.FormatInt(v, 10) - case uint64: - strValue = strconv.FormatUint(v, 10) - case float32: - strValue = strconv.FormatFloat(float64(v), 'f', -1, 32) - case float64: - strValue = strconv.FormatFloat(v, 'f', -1, 64) - default: - strValue = fmt.Sprintf("%v", v) - } - - return &strValue -} diff --git a/plugins/kb-adapter-rbdplugin/service/registry/registry.go b/plugins/kb-adapter-rbdplugin/service/registry/registry.go deleted file mode 100644 index b4d8f4551..000000000 --- a/plugins/kb-adapter-rbdplugin/service/registry/registry.go +++ /dev/null @@ -1,54 +0,0 @@ -package registry - -import ( - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/log" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/adapter" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/builder" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/coordinator" -) - -// Cluster 在这里注册 Block Mechanica 支持的数据库集群 -var Cluster = map[string]adapter.ClusterAdapter{ - "postgresql": _postgresql, - "mysql": _mysql, - "redis": _redis, - "rabbitmq": _rabbitmq, - // ... new types here -} - -var ( - _postgresql = adapter.ClusterAdapter{ - Builder: &builder.PostgreSQL{}, - Coordinator: &coordinator.PostgreSQL{}, - } - - _mysql = adapter.ClusterAdapter{ - Builder: &builder.MySQL{}, - Coordinator: &coordinator.MySQL{}, - } - - _redis = adapter.ClusterAdapter{ - Builder: &builder.Redis{}, - Coordinator: &coordinator.Redis{}, - } - _rabbitmq = adapter.ClusterAdapter{ - Builder: &builder.RabbitMQ{}, - Coordinator: &coordinator.RabbitMQ{}, - } -) - -// init 函数进行注册表验证 -func init() { - validateClusterRegistry() -} - -// validateClusterRegistry 验证集群注册表的完整性 -func validateClusterRegistry() { - for dbType, clusterAdapter := range Cluster { - if err := clusterAdapter.Validate(); err != nil { - log.Fatal("Critical validation error", log.String("DB Type", dbType), log.Err(err)) - } - log.Info("Database validation passed", log.String("DB Type", dbType)) - } - log.Info("All database validation passed") -} diff --git a/plugins/kb-adapter-rbdplugin/service/resource/resource.go b/plugins/kb-adapter-rbdplugin/service/resource/resource.go deleted file mode 100644 index e2c32d526..000000000 --- a/plugins/kb-adapter-rbdplugin/service/resource/resource.go +++ /dev/null @@ -1,118 +0,0 @@ -// Package resource 提供集群资源相关操作 -package resource - -import ( - "context" - "fmt" - "sort" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/mono" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/kbkit" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/registry" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - storagev1 "k8s.io/api/storage/v1" - utilversion "k8s.io/apimachinery/pkg/util/version" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Service 提供集群资源相关操作 -type Service struct { - client client.Client -} - -func NewService(c client.Client) *Service { - return &Service{ - client: c, - } -} - -// GetStorageClasses 返回集群中所有的 StorageClass 的名称 -func (s *Service) GetStorageClasses(ctx context.Context) (model.StorageClasses, error) { - var scList storagev1.StorageClassList - if err := s.client.List(ctx, &scList); err != nil { - return nil, fmt.Errorf("list StorageClass: %w", err) - } - names := make([]string, 0, len(scList.Items)) - for _, sc := range scList.Items { - names = append(names, sc.Name) - } - - return mono.Sorted(names), nil -} - -// GetAddons 获取所有可用的 Addon(数据库类型与版本) -func (s *Service) GetAddons(ctx context.Context) ([]*model.Addon, error) { - var cmpvList kbappsv1.ComponentVersionList - if err := s.client.List(ctx, &cmpvList); err != nil { - return nil, fmt.Errorf("get component version list: %w", err) - } - - addons := make([]*model.Addon, 0, len(cmpvList.Items)) - for _, item := range cmpvList.Items { - releases := make([]string, 0, len(item.Spec.Releases)) - for _, release := range item.Spec.Releases { - releases = append(releases, release.ServiceVersion) - } - - addon := &model.Addon{ - Type: item.Name, - Version: sortServiceVersionsLatestFirst(releases), - IsSupportBackup: kbkit.IsSupportBackup(item.Name), - } - addons = append(addons, addon) - } - - return mono.FilterThenSort(addons, filterSupportedAddons, func(a, b *model.Addon) bool { - return a.Type < b.Type - }), nil -} - -func sortServiceVersionsLatestFirst(versions []string) []string { - result := append([]string(nil), versions...) - sort.SliceStable(result, func(i, j int) bool { - left, leftErr := utilversion.Parse(result[i]) - right, rightErr := utilversion.Parse(result[j]) - switch { - case leftErr == nil && rightErr == nil: - if left.EqualTo(right) { - return result[i] > result[j] - } - return left.GreaterThan(right) - case leftErr == nil: - return true - case rightErr == nil: - return false - default: - return result[i] > result[j] - } - }) - return result -} - -// GetClusterPort 返回指定数据库在 KubeBlocks service 中的目标端口 -func (s *Service) GetClusterPort(ctx context.Context, serviceID string) int { - cluster, err := kbkit.GetClusterByServiceID(ctx, s.client, serviceID) - if err != nil { - return -1 - } - adapter, ok := registry.Cluster[cluster.Spec.ClusterDef] - if !ok { - return -1 - } - return adapter.Coordinator.TargetPort() -} - -// filterSupportedAddons mono.Filter 的过滤函数 -// 仅返回在 _clusterRegistry 中声明过的数据库类型,确保返回值与系统实际可创建的类型一致。 -// 判定是否受 Block Mechanica 支持时, 不同 toplogy 的 addon 视为同一类型 -func filterSupportedAddons(addon *model.Addon) bool { - t := addon.Type - // TODO - /* if i := strings.LastIndex(t, "-"); i > 0 { - t = t[:i] - } */ - _, ok := registry.Cluster[t] - return ok -} diff --git a/plugins/kb-adapter-rbdplugin/service/resource/resource_test.go b/plugins/kb-adapter-rbdplugin/service/resource/resource_test.go deleted file mode 100644 index 7c8280f57..000000000 --- a/plugins/kb-adapter-rbdplugin/service/resource/resource_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package resource - -import ( - "context" - "testing" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/testutil" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// capability_id: rainbond.kb-adapter.addon-version-order -func TestGetAddonsReturnsVersionsLatestFirst(t *testing.T) { - componentVersion := &kbappsv1.ComponentVersion{ - ObjectMeta: metav1.ObjectMeta{Name: "mysql"}, - Spec: kbappsv1.ComponentVersionSpec{ - Releases: []kbappsv1.ComponentVersionRelease{ - {ServiceVersion: "8.0.2"}, - {ServiceVersion: "8.0.30"}, - {ServiceVersion: "8.1.0"}, - {ServiceVersion: "5.7.44"}, - }, - }, - } - service := NewService(testutil.NewFakeClient(componentVersion)) - - addons, err := service.GetAddons(context.Background()) - require.NoError(t, err) - require.Len(t, addons, 1) - assert.Equal(t, "mysql", addons[0].Type) - assert.Equal(t, []string{"8.1.0", "8.0.30", "8.0.2", "5.7.44"}, addons[0].Version) -} diff --git a/plugins/kb-adapter-rbdplugin/service/service.go b/plugins/kb-adapter-rbdplugin/service/service.go deleted file mode 100644 index 0141987d3..000000000 --- a/plugins/kb-adapter-rbdplugin/service/service.go +++ /dev/null @@ -1,254 +0,0 @@ -// Package service 提供 Block Mechanica 的核心服务 -// -// - Cluster: 提供 KubeBlocks 的 Cluster 相关操作 -// -// - Resource: 提供 k8s 资源的相关操作 -// -// - Backup: 提供 KubeBlocks 的 Backup 相关操作 -package service - -import ( - "context" - - "github.com/furutachiKurea/kb-adapter-rbdplugin/internal/model" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/backup" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/cluster" - "github.com/furutachiKurea/kb-adapter-rbdplugin/service/resource" - - kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ Services = (*DefaultServices)(nil) - -// Services 聚合接口:供上层(handler/controller)使用 -type Services interface { - Resource - Backup - Cluster -} - -// Backup 提供 KubeBlocks 的 Backup 相关操作 -type Backup interface { - // ListAvailableBackupRepos 返回所有 Available 的 BackupRepo - ListAvailableBackupRepos(ctx context.Context) ([]*model.BackupRepo, error) - CreateBackupRepo(ctx context.Context, input model.BackupRepoInput) (*model.BackupRepo, error) - UpdateBackupRepo(ctx context.Context, name string, input model.BackupRepoInput) (*model.BackupRepo, error) - DeleteBackupRepo(ctx context.Context, name string) error - - // ReScheduleBackup 重新调度 Cluster 的备份配置 - // - // 通过 Patch cluster 中的备份字段来实现 back schedule 的更新 - ReScheduleBackup(ctx context.Context, schedule model.BackupScheduleInput) error - - // BackupCluster 执行集群备份操作 - BackupCluster(ctx context.Context, backup model.BackupInput) error - - // ListBackups 返回给定的 Cluster 的备份列表 - ListBackups(ctx context.Context, query model.BackupListQuery) (*model.PaginatedResult[model.BackupItem], error) - - // DeleteBackups 批量删除指定备份 - // - // 根据 service_id 查找对应的 Cluster,然后删除请求中指定名称的备份 - // 返回成功删除的备份名称列表 - DeleteBackups(ctx context.Context, rbd model.RBDService, backups []string) ([]string, error) -} - -// Cluster 提供 KubeBlocks Cluster 相关操作 -type Cluster interface { - // CreateCluster 依据 req 创建 KubeBlocks Cluster - // - // 在创建 Cluster 时,会更新由 Rainbond 创建的 KubeBlocks Component (Deployment) 的 args - // 以确保将来自其他 Rainbond 组件的连接请求转发至该 Cluster 的 Service 上; - // 同时,通过将 service_id 添加至 Cluster 的 labels 中以关联 KubeBlocks Component 与 Cluster, - // Rainbond 也通过这层关系来判断 Rainbond 组件是否为 KubeBlocks Component - // - // 返回成功创建的 KubeBlocks Cluster 实例 - CreateCluster(ctx context.Context, cluster model.ClusterInput) (*kbappsv1.Cluster, error) - - // DeleteClusters 删除 KubeBlocks 数据库集群 - // - // 批量删除指定 serviceIDs 对应的 Cluster,忽略找不到的 service_id - DeleteClusters(ctx context.Context, serviceIDs []string) error - - // CancelClusterCreate 取消集群创建 - // - // 在删除前将 TerminationPolicy 调整为 WipeOut,确保 PVC/PV 等存储资源一并清理 - // https://kubeblocks.io/docs/preview/user_docs/references/api-reference/cluster#apps.kubeblocks.io/v1.TerminationPolicyType - CancelClusterCreate(ctx context.Context, rbd model.RBDService) error - - // GetConnectInfo 获取指定 Cluster 的连接账户信息, - // 从 Kubernetes Secret 中获取数据库账户的用户名和密码 - // - // Secret 名称由对应数据库类型的 Coordinator 适配器生成 - GetConnectInfo(ctx context.Context, rbd model.RBDService) ([]model.ConnectInfo, error) - - // GetClusterDetail 通过 RBDService.ID 获取 Cluster 的详细信息 - GetClusterDetail(ctx context.Context, rbd model.RBDService) (*model.ClusterDetail, error) - - // ExpansionCluster 对 Cluster 进行伸缩操作 - // - // 使用 opsrequest 将 Cluster 的资源规格进行伸缩,使其变为 req 的期望状态 - ExpansionCluster(ctx context.Context, expansion model.ExpansionInput) error - - // ManageClustersLifecycle 通过创建 OpsRequest 批量管理多个 Cluster 的生命周期 - // - // 支持 operation: Start, Stop, Restart - ManageClustersLifecycle(ctx context.Context, operation opsv1alpha1.OpsType, serviceIDs []string) *model.BatchOperationResult - - // GetPodDetail 获取指定 Cluster 的 Pod detail - // 获取指定 service_id 的 Cluster 管理的指定 Pod 的详细信息 - GetPodDetail(ctx context.Context, serviceID string, podName string) (*model.PodDetail, error) - - // GetClusterEvents 获取指定 KubeBlocks Cluster 的 events - // - // 从 Cluster 拥有的 OpsRequest 中获取,按创建时间降序排序 - GetClusterEvents(ctx context.Context, serviceID string, pagination model.Pagination) (*model.PaginatedResult[model.EventItem], error) - - // RestoreFromBackup 从用户通过 backupName 指定的备份中 restore cluster, - // 返回 restored cluster 的名称 + clusterDef, 用于 Rainbond 更新 KubeBlocks Component 信息 - // - // 该方法将为恢复的 cluster 通过 newServiceID 绑定到一个新的 KubeBlocks Component 中 - RestoreFromBackup(ctx context.Context, oldServiceID, newServiceID, backupName string) (string, error) - - // GetClusterParameter 获取指定 KubeBlocks Cluster 的参数 - GetClusterParameter(ctx context.Context, query model.ClusterParametersQuery) (*model.PaginatedResult[model.Parameter], error) - - // ChangeClusterParameter 变更指定 KubeBlocks Cluster 的参数 - ChangeClusterParameter(ctx context.Context, req model.ClusterParametersChange) (*model.ParameterChangeResult, error) -} - -// Resource 提供集群资源发现和 Rainbond 集成操作 -type Resource interface { - // GetAddons 获取所有可用的 Addon(数据库类型与版本) - GetAddons(ctx context.Context) ([]*model.Addon, error) - - // GetStorageClasses 返回集群中所有的 StorageClass 的名称 - GetStorageClasses(ctx context.Context) (model.StorageClasses, error) - - // GetClusterPort 返回指定数据库在 KubeBlocks service 中的目标端口 - GetClusterPort(ctx context.Context, serviceID string) int -} - -// DefaultServices 为聚合接口的默认实现,委托到具体子服务 -type DefaultServices struct { - Backup Backup - Cluster Cluster - Resource Resource -} - -// NewServices 构建聚合服务实例 -func NewServices(backup *backup.Service, cluster *cluster.Service, resource *resource.Service) Services { - return &DefaultServices{ - Backup: backup, - Cluster: cluster, - Resource: resource, - } -} - -// New 构造 Services -func New(c client.Client) Services { - return NewServices( - backup.NewService(c), - cluster.NewService(c), - resource.NewService(c), - ) -} - -// Backup - -func (s *DefaultServices) ListAvailableBackupRepos(ctx context.Context) ([]*model.BackupRepo, error) { - return s.Backup.ListAvailableBackupRepos(ctx) -} - -func (s *DefaultServices) CreateBackupRepo(ctx context.Context, input model.BackupRepoInput) (*model.BackupRepo, error) { - return s.Backup.CreateBackupRepo(ctx, input) -} - -func (s *DefaultServices) UpdateBackupRepo(ctx context.Context, name string, input model.BackupRepoInput) (*model.BackupRepo, error) { - return s.Backup.UpdateBackupRepo(ctx, name, input) -} - -func (s *DefaultServices) DeleteBackupRepo(ctx context.Context, name string) error { - return s.Backup.DeleteBackupRepo(ctx, name) -} - -func (s *DefaultServices) ReScheduleBackup(ctx context.Context, schedule model.BackupScheduleInput) error { - return s.Backup.ReScheduleBackup(ctx, schedule) -} - -func (s *DefaultServices) BackupCluster(ctx context.Context, backup model.BackupInput) error { - return s.Backup.BackupCluster(ctx, backup) -} - -func (s *DefaultServices) ListBackups(ctx context.Context, query model.BackupListQuery) (*model.PaginatedResult[model.BackupItem], error) { - return s.Backup.ListBackups(ctx, query) -} - -func (s *DefaultServices) DeleteBackups(ctx context.Context, rbd model.RBDService, backups []string) ([]string, error) { - return s.Backup.DeleteBackups(ctx, rbd, backups) -} - -// Cluster - -func (s *DefaultServices) CreateCluster(ctx context.Context, cluster model.ClusterInput) (*kbappsv1.Cluster, error) { - return s.Cluster.CreateCluster(ctx, cluster) -} - -func (s *DefaultServices) GetConnectInfo(ctx context.Context, rbd model.RBDService) ([]model.ConnectInfo, error) { - return s.Cluster.GetConnectInfo(ctx, rbd) -} - -func (s *DefaultServices) GetClusterDetail(ctx context.Context, rbd model.RBDService) (*model.ClusterDetail, error) { - return s.Cluster.GetClusterDetail(ctx, rbd) -} - -func (s *DefaultServices) ExpansionCluster(ctx context.Context, expansion model.ExpansionInput) error { - return s.Cluster.ExpansionCluster(ctx, expansion) -} - -func (s *DefaultServices) DeleteClusters(ctx context.Context, serviceIDs []string) error { - return s.Cluster.DeleteClusters(ctx, serviceIDs) -} - -func (s *DefaultServices) CancelClusterCreate(ctx context.Context, rbd model.RBDService) error { - return s.Cluster.CancelClusterCreate(ctx, rbd) -} - -func (s *DefaultServices) ManageClustersLifecycle(ctx context.Context, operation opsv1alpha1.OpsType, serviceIDs []string) *model.BatchOperationResult { - return s.Cluster.ManageClustersLifecycle(ctx, operation, serviceIDs) -} - -func (s *DefaultServices) GetPodDetail(ctx context.Context, serviceID string, podName string) (*model.PodDetail, error) { - return s.Cluster.GetPodDetail(ctx, serviceID, podName) -} - -func (s *DefaultServices) GetClusterEvents(ctx context.Context, serviceID string, pagination model.Pagination) (*model.PaginatedResult[model.EventItem], error) { - return s.Cluster.GetClusterEvents(ctx, serviceID, pagination) -} - -func (s *DefaultServices) RestoreFromBackup(ctx context.Context, oldServiceID, newServiceID, backupName string) (string, error) { - return s.Cluster.RestoreFromBackup(ctx, oldServiceID, newServiceID, backupName) -} - -// Resource - -func (s *DefaultServices) GetAddons(ctx context.Context) ([]*model.Addon, error) { - return s.Resource.GetAddons(ctx) -} -func (s *DefaultServices) GetStorageClasses(ctx context.Context) (model.StorageClasses, error) { - return s.Resource.GetStorageClasses(ctx) -} - -func (s *DefaultServices) GetClusterPort(ctx context.Context, serviceID string) int { - return s.Resource.GetClusterPort(ctx, serviceID) -} - -func (s *DefaultServices) GetClusterParameter(ctx context.Context, query model.ClusterParametersQuery) (*model.PaginatedResult[model.Parameter], error) { - return s.Cluster.GetClusterParameter(ctx, query) -} - -func (s *DefaultServices) ChangeClusterParameter(ctx context.Context, req model.ClusterParametersChange) (*model.ParameterChangeResult, error) { - return s.Cluster.ChangeClusterParameter(ctx, req) -} diff --git a/test-manifest.json b/test-manifest.json index 0668b6b72..9a2382719 100644 --- a/test-manifest.json +++ b/test-manifest.json @@ -2823,483 +2823,6 @@ "test_type": "regression", "status": "active" }, - { - "id": "rainbond.kb-adapter.addon-version-order", - "title": "List kb-adapter addons with deterministic version order", - "title_zh": "\u6309\u7a33\u5b9a\u987a\u5e8f\u8fd4\u56de kb-adapter \u63d2\u4ef6 Addon \u7248\u672c", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/resource.Service.GetAddons", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/resource/resource.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/resource/resource_test.go" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.backup-repo.list-all", - "title": "List backup repositories with all phases for kb-adapter", - "title_zh": "\u5217\u51fa kb-adapter \u5168\u90e8\u72b6\u6001\u7684\u5907\u4efd\u4ed3\u5e93", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.ListAvailableBackupRepos", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/internal/model/backup.go", - "plugins/kb-adapter-rbdplugin/service/backup/backup.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestListAvailableBackupRepos" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.backup-repo.mutate", - "title": "Create, update, and delete backup repositories in kb-adapter", - "title_zh": "kb-adapter \u521b\u5efa\u3001\u66f4\u65b0\u548c\u5220\u9664\u5907\u4efd\u4ed3\u5e93", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.CreateBackupRepo", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/api/handler/handler.go", - "plugins/kb-adapter-rbdplugin/api/router.go", - "plugins/kb-adapter-rbdplugin/deploy/k8s/deploy.yaml", - "plugins/kb-adapter-rbdplugin/internal/model/backup.go", - "plugins/kb-adapter-rbdplugin/service/backup/backup.go", - "plugins/kb-adapter-rbdplugin/service/service.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestCreateBackupRepo" - }, - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestDeleteBackupRepoRejectsClusterInUse" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster-backup.delete", - "title": "Delete eligible cluster backups by service scope", - "title_zh": "\u6309\u670d\u52a1\u8303\u56f4\u5220\u9664\u5141\u8bb8\u6e05\u7406\u7684\u96c6\u7fa4\u5907\u4efd", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.DeleteBackups", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/backup/backup.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestDeleteBackups" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster-backup.delete-guard", - "title": "Decide whether a cluster backup can be safely deleted", - "title_zh": "\u5224\u65ad\u96c6\u7fa4\u5907\u4efd\u662f\u5426\u5141\u8bb8\u5b89\u5168\u5220\u9664", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.canDeleteBackup", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/backup/backup.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestCanDeleteBackup" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster-backup.list", - "title": "List cluster backups for the target service", - "title_zh": "\u5217\u51fa\u76ee\u6807\u670d\u52a1\u5bf9\u5e94\u7684\u96c6\u7fa4\u5907\u4efd", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.ListBackups", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/backup/backup.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestListBackups" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster-backup.schedule-reconcile", - "title": "Create update or disable cluster backup schedules from plugin input", - "title_zh": "\u6839\u636e\u63d2\u4ef6\u8f93\u5165\u521b\u5efa\u66f4\u65b0\u6216\u5173\u95ed\u96c6\u7fa4\u5907\u4efd\u8ba1\u5212", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/backup.Service.ReScheduleBackup", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/backup/backup.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/backup/backup_test.go", - "selector": "TestReScheduleBackup" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.associate-service-id", - "title": "Associate a KubeBlocks cluster with a Rainbond service id", - "title_zh": "\u5c06 KubeBlocks \u96c6\u7fa4\u5173\u8054\u5230 Rainbond \u670d\u52a1 ID", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.associateToKubeBlocksComponent", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/cluster.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go", - "selector": "TestAssociateToKubeBlocksComponent" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.connection-info", - "title": "Fetch cluster connection credentials for plugin detail panels", - "title_zh": "\u83b7\u53d6\u63d2\u4ef6\u8be6\u60c5\u9875\u6240\u9700\u7684\u96c6\u7fa4\u8fde\u63a5\u51ed\u636e", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.GetConnectInfo", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/info.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/info_test.go", - "selector": "TestGetConnectInfo" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.create", - "title": "Create a KubeBlocks cluster through the kb-adapter plugin", - "title_zh": "\u901a\u8fc7 kb-adapter \u63d2\u4ef6\u521b\u5efa KubeBlocks \u96c6\u7fa4", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.CreateCluster", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go", - "selector": "TestCreateCluster" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.delete-cleanup", - "title": "Clean up cluster OpsRequests and referenced secrets during teardown", - "title_zh": "\u5728\u96c6\u7fa4\u6e05\u7406\u65f6\u5220\u9664 OpsRequest \u4e0e\u5173\u8054 Secret", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.cleanupClusterOpsRequests", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go", - "selector": "TestCleanupClusterOpsRequests" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.detail-summary", - "title": "Build plugin cluster detail summary including resource and backup info", - "title_zh": "\u6784\u5efa\u63d2\u4ef6\u96c6\u7fa4\u8be6\u60c5\u6458\u8981\u5e76\u5305\u542b\u8d44\u6e90\u4e0e\u5907\u4efd\u4fe1\u606f", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterDetail", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/info.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/info_test.go", - "selector": "TestGetClusterDetail" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.event-timeline", - "title": "Build cluster operation event timeline from OpsRequests", - "title_zh": "\u6839\u636e OpsRequest \u6784\u5efa\u96c6\u7fa4\u64cd\u4f5c\u4e8b\u4ef6\u65f6\u95f4\u7ebf", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterEvents", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/event.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/event_test.go", - "selector": "TestGetClusterEvents" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.list-pods", - "title": "List cluster pods and resolve InstanceSets for plugin views", - "title_zh": "\u4e3a\u63d2\u4ef6\u89c6\u56fe\u5217\u51fa\u96c6\u7fa4 Pod \u5e76\u89e3\u6790 InstanceSet", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.getClusterPods", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/cluster.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go", - "selector": "TestGetClusterPods" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.parameter-constraint-merge", - "title": "Merge live parameter entries with parameter constraints", - "title_zh": "\u5408\u5e76\u5b9e\u65f6\u53c2\u6570\u9879\u4e0e\u53c2\u6570\u7ea6\u675f\u5b9a\u4e49", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.mergeEntriesAndConstraints", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/parameter.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go", - "selector": "TestMergeEntriesAndConstraints" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.pod-detail", - "title": "Build detailed pod diagnostics for plugin troubleshooting", - "title_zh": "\u4e3a\u63d2\u4ef6\u8bca\u65ad\u9875\u6784\u5efa\u8be6\u7ec6 Pod \u8bca\u65ad\u4fe1\u606f", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.GetPodDetail", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/pod.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go", - "selector": "TestGetPodDetail" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.restore-from-backup", - "title": "Restore clusters from backup and clean failed restore operations", - "title_zh": "\u4ece\u5907\u4efd\u6062\u590d\u96c6\u7fa4\u5e76\u6e05\u7406\u5931\u8d25\u7684\u6062\u590d\u64cd\u4f5c", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.RestoreFromBackup", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/restore.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go", - "selector": "TestRestoreFromBackup" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.cluster.scale", - "title": "Scale cluster replicas resources and volumes through plugin workflows", - "title_zh": "\u901a\u8fc7\u63d2\u4ef6\u5de5\u4f5c\u6d41\u6269\u7f29\u96c6\u7fa4\u526f\u672c\u8d44\u6e90\u4e0e\u5b58\u50a8", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/cluster.Service.ExpansionCluster", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/cluster/scaling.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go", - "selector": "TestExpansionCluster" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.coordinator.parameter-value-parse", - "title": "Parse addon parameter values into typed coordinator entries", - "title_zh": "\u5c06\u63d2\u4ef6\u53c2\u6570\u503c\u89e3\u6790\u4e3a\u5e26\u7c7b\u578b\u7684\u534f\u8c03\u5668\u53c2\u6570\u9879", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/coordinator.Coordinator.ParseParameters", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/coordinator/coordinator.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go", - "selector": "TestBase_ParseParameters" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.opsrequest.blocking-ops-management", - "title": "List and clean up blocking non-final OpsRequests", - "title_zh": "\u5217\u51fa\u5e76\u6e05\u7406\u963b\u585e\u4e2d\u7684\u672a\u7ec8\u6001 OpsRequest", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/kbkit.GetAllNonFinalOpsRequests", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go", - "selector": "TestGetAllNonFinalOpsRequests" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.opsrequest.create-supported-ops", - "title": "Create supported lifecycle backup scaling parameter and restore OpsRequests", - "title_zh": "\u521b\u5efa\u751f\u547d\u5468\u671f\u5907\u4efd\u6269\u7f29\u5bb9\u53c2\u6570\u53d8\u66f4\u4e0e\u6062\u590d\u7b49 OpsRequest", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/kbkit.CreateLifecycleOpsRequest", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go", - "selector": "TestCreateLifecycleOpsRequest" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.opsrequest.preflight-arbitration", - "title": "Arbitrate conflicting OpsRequests before submitting new operations", - "title_zh": "\u5728\u63d0\u4ea4\u65b0\u64cd\u4f5c\u524d\u88c1\u51b3\u51b2\u7a81\u7684 OpsRequest", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/service/kbkit.preflightCheck", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go", - "selector": "TestUniqueOpsDecide" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.server-config.dev-mode", - "title": "Detect kb-adapter development environment mode", - "title_zh": "\u8bc6\u522b kb-adapter \u5f00\u53d1\u73af\u5883\u6a21\u5f0f", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/internal/config.InDevelopment", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/internal/config/config.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/internal/config/config_test.go", - "selector": "TestInDevelopment" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.server-config.load-from-env", - "title": "Load kb-adapter server configuration from environment", - "title_zh": "\u4ece\u73af\u5883\u53d8\u91cf\u52a0\u8f7d kb-adapter \u670d\u52a1\u914d\u7f6e", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/internal/config.LoadConfigFromEnv", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/internal/config/config.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/internal/config/config_test.go", - "selector": "TestLoadConfigFromEnv" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.server-config.must-load", - "title": "Must-load kb-adapter server configuration with validation", - "title_zh": "\u5f3a\u5236\u52a0\u8f7d\u5e76\u6821\u9a8c kb-adapter \u670d\u52a1\u914d\u7f6e", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/internal/config.MustLoad", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/internal/config/config.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/internal/config/config_test.go", - "selector": "TestMustLoad" - } - ], - "test_type": "regression", - "status": "active" - }, - { - "id": "rainbond.kb-adapter.server-config.validate", - "title": "Validate kb-adapter server configuration values", - "title_zh": "\u6821\u9a8c kb-adapter \u670d\u52a1\u914d\u7f6e\u9879", - "interface_type": "workflow", - "interface": "plugins/kb-adapter-rbdplugin/internal/config.ServerConfig.Validate", - "code_paths": [ - "plugins/kb-adapter-rbdplugin/internal/config/config.go" - ], - "tests": [ - { - "path": "plugins/kb-adapter-rbdplugin/internal/config/config_test.go", - "selector": "TestServerConfig_Validate" - } - ], - "test_type": "regression", - "status": "active" - }, { "id": "rainbond.kubeblocks.component-selector", "title": "Generate label selectors for KubeBlocks components", diff --git a/test-manifest.md b/test-manifest.md index 6527203d6..3b1dfa691 100644 --- a/test-manifest.md +++ b/test-manifest.md @@ -157,32 +157,6 @@ | rainbond.ingress-nginx.node-ip-resolve | 为 ingress-nginx helper 解析节点内外网 IP | active | regression | util/ingress-nginx/k8s.GetNodeIPOrName | util/ingress-nginx/k8s/main_test.go::TestGetNodeIPOrName | | rainbond.ingress-nginx.pod-details | 根据环境变量和集群状态解析 ingress-nginx Pod 详情 | active | regression | util/ingress-nginx/k8s.GetPodDetails | util/ingress-nginx/k8s/main_test.go::TestGetPodDetails | | rainbond.k8s.scheme-registers-kubevirt-vm | K8s scheme registers KubeVirt VirtualMachine | active | regression | pkg/component/k8s.init | pkg/component/k8s/k8sComponent_test.go::TestSchemeRegistersKubeVirtVirtualMachine | -| rainbond.kb-adapter.addon-version-order | 按稳定顺序返回 kb-adapter 插件 Addon 版本 | active | regression | plugins/kb-adapter-rbdplugin/service/resource.Service.GetAddons | plugins/kb-adapter-rbdplugin/service/resource/resource_test.go | -| rainbond.kb-adapter.backup-repo.list-all | 列出 kb-adapter 全部状态的备份仓库 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.ListAvailableBackupRepos | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestListAvailableBackupRepos | -| rainbond.kb-adapter.backup-repo.mutate | kb-adapter 创建、更新和删除备份仓库 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.CreateBackupRepo | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestCreateBackupRepo
plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestDeleteBackupRepoRejectsClusterInUse | -| rainbond.kb-adapter.cluster-backup.delete | 按服务范围删除允许清理的集群备份 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.DeleteBackups | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestDeleteBackups | -| rainbond.kb-adapter.cluster-backup.delete-guard | 判断集群备份是否允许安全删除 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.canDeleteBackup | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestCanDeleteBackup | -| rainbond.kb-adapter.cluster-backup.list | 列出目标服务对应的集群备份 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.ListBackups | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestListBackups | -| rainbond.kb-adapter.cluster-backup.schedule-reconcile | 根据插件输入创建更新或关闭集群备份计划 | active | regression | plugins/kb-adapter-rbdplugin/service/backup.Service.ReScheduleBackup | plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestReScheduleBackup | -| rainbond.kb-adapter.cluster.associate-service-id | 将 KubeBlocks 集群关联到 Rainbond 服务 ID | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.associateToKubeBlocksComponent | plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go::TestAssociateToKubeBlocksComponent | -| rainbond.kb-adapter.cluster.connection-info | 获取插件详情页所需的集群连接凭据 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.GetConnectInfo | plugins/kb-adapter-rbdplugin/service/cluster/info_test.go::TestGetConnectInfo | -| rainbond.kb-adapter.cluster.create | 通过 kb-adapter 插件创建 KubeBlocks 集群 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.CreateCluster | plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go::TestCreateCluster | -| rainbond.kb-adapter.cluster.delete-cleanup | 在集群清理时删除 OpsRequest 与关联 Secret | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.cleanupClusterOpsRequests | plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go::TestCleanupClusterOpsRequests | -| rainbond.kb-adapter.cluster.detail-summary | 构建插件集群详情摘要并包含资源与备份信息 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterDetail | plugins/kb-adapter-rbdplugin/service/cluster/info_test.go::TestGetClusterDetail | -| rainbond.kb-adapter.cluster.event-timeline | 根据 OpsRequest 构建集群操作事件时间线 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterEvents | plugins/kb-adapter-rbdplugin/service/cluster/event_test.go::TestGetClusterEvents | -| rainbond.kb-adapter.cluster.list-pods | 为插件视图列出集群 Pod 并解析 InstanceSet | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.getClusterPods | plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go::TestGetClusterPods | -| rainbond.kb-adapter.cluster.parameter-constraint-merge | 合并实时参数项与参数约束定义 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.mergeEntriesAndConstraints | plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go::TestMergeEntriesAndConstraints | -| rainbond.kb-adapter.cluster.pod-detail | 为插件诊断页构建详细 Pod 诊断信息 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.GetPodDetail | plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go::TestGetPodDetail | -| rainbond.kb-adapter.cluster.restore-from-backup | 从备份恢复集群并清理失败的恢复操作 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.RestoreFromBackup | plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go::TestRestoreFromBackup | -| rainbond.kb-adapter.cluster.scale | 通过插件工作流扩缩集群副本资源与存储 | active | regression | plugins/kb-adapter-rbdplugin/service/cluster.Service.ExpansionCluster | plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go::TestExpansionCluster | -| rainbond.kb-adapter.coordinator.parameter-value-parse | 将插件参数值解析为带类型的协调器参数项 | active | regression | plugins/kb-adapter-rbdplugin/service/coordinator.Coordinator.ParseParameters | plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go::TestBase_ParseParameters | -| rainbond.kb-adapter.opsrequest.blocking-ops-management | 列出并清理阻塞中的未终态 OpsRequest | active | regression | plugins/kb-adapter-rbdplugin/service/kbkit.GetAllNonFinalOpsRequests | plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go::TestGetAllNonFinalOpsRequests | -| rainbond.kb-adapter.opsrequest.create-supported-ops | 创建生命周期备份扩缩容参数变更与恢复等 OpsRequest | active | regression | plugins/kb-adapter-rbdplugin/service/kbkit.CreateLifecycleOpsRequest | plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go::TestCreateLifecycleOpsRequest | -| rainbond.kb-adapter.opsrequest.preflight-arbitration | 在提交新操作前裁决冲突的 OpsRequest | active | regression | plugins/kb-adapter-rbdplugin/service/kbkit.preflightCheck | plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go::TestUniqueOpsDecide | -| rainbond.kb-adapter.server-config.dev-mode | 识别 kb-adapter 开发环境模式 | active | regression | plugins/kb-adapter-rbdplugin/internal/config.InDevelopment | plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestInDevelopment | -| rainbond.kb-adapter.server-config.load-from-env | 从环境变量加载 kb-adapter 服务配置 | active | regression | plugins/kb-adapter-rbdplugin/internal/config.LoadConfigFromEnv | plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestLoadConfigFromEnv | -| rainbond.kb-adapter.server-config.must-load | 强制加载并校验 kb-adapter 服务配置 | active | regression | plugins/kb-adapter-rbdplugin/internal/config.MustLoad | plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestMustLoad | -| rainbond.kb-adapter.server-config.validate | 校验 kb-adapter 服务配置项 | active | regression | plugins/kb-adapter-rbdplugin/internal/config.ServerConfig.Validate | plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestServerConfig_Validate | | rainbond.kubeblocks.component-selector | 为 KubeBlocks 组件生成标签选择器 | active | regression | util/kubeblocks.GenerateKubeBlocksSelector | util/kubeblocks/kubeblocks_test.go::TestGenerateKubeBlocksSelector | | rainbond.license.decode | 解码并解析许可证令牌内容 | active | regression | api/util/license.DecodeLicense | api/util/license/rsa_license_test.go::TestDecodeLicense | | rainbond.license.parse-public-key | 解析 PEM 编码的 RSA 公钥 | active | regression | api/util/license.ParsePublicKey | api/util/license/rsa_license_test.go::TestParsePublicKey | @@ -1989,266 +1963,6 @@ - 代码路径: `pkg/component/k8s/k8sComponent.go` - 测试路径: `pkg/component/k8s/k8sComponent_test.go::TestSchemeRegistersKubeVirtVirtualMachine` -### 按稳定顺序返回 kb-adapter 插件 Addon 版本 - -- Capability ID: `rainbond.kb-adapter.addon-version-order` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/resource.Service.GetAddons` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/resource/resource.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/resource/resource_test.go` - -### 列出 kb-adapter 全部状态的备份仓库 - -- Capability ID: `rainbond.kb-adapter.backup-repo.list-all` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.ListAvailableBackupRepos` -- 代码路径: `plugins/kb-adapter-rbdplugin/internal/model/backup.go`, `plugins/kb-adapter-rbdplugin/service/backup/backup.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestListAvailableBackupRepos` - -### kb-adapter 创建、更新和删除备份仓库 - -- Capability ID: `rainbond.kb-adapter.backup-repo.mutate` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.CreateBackupRepo` -- 代码路径: `plugins/kb-adapter-rbdplugin/api/handler/handler.go`, `plugins/kb-adapter-rbdplugin/api/router.go`, `plugins/kb-adapter-rbdplugin/deploy/k8s/deploy.yaml`, `plugins/kb-adapter-rbdplugin/internal/model/backup.go`, `plugins/kb-adapter-rbdplugin/service/backup/backup.go`, `plugins/kb-adapter-rbdplugin/service/service.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestCreateBackupRepo`, `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestDeleteBackupRepoRejectsClusterInUse` - -### 按服务范围删除允许清理的集群备份 - -- Capability ID: `rainbond.kb-adapter.cluster-backup.delete` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.DeleteBackups` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/backup/backup.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestDeleteBackups` - -### 判断集群备份是否允许安全删除 - -- Capability ID: `rainbond.kb-adapter.cluster-backup.delete-guard` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.canDeleteBackup` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/backup/backup.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestCanDeleteBackup` - -### 列出目标服务对应的集群备份 - -- Capability ID: `rainbond.kb-adapter.cluster-backup.list` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.ListBackups` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/backup/backup.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestListBackups` - -### 根据插件输入创建更新或关闭集群备份计划 - -- Capability ID: `rainbond.kb-adapter.cluster-backup.schedule-reconcile` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/backup.Service.ReScheduleBackup` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/backup/backup.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/backup/backup_test.go::TestReScheduleBackup` - -### 将 KubeBlocks 集群关联到 Rainbond 服务 ID - -- Capability ID: `rainbond.kb-adapter.cluster.associate-service-id` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.associateToKubeBlocksComponent` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/cluster.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go::TestAssociateToKubeBlocksComponent` - -### 获取插件详情页所需的集群连接凭据 - -- Capability ID: `rainbond.kb-adapter.cluster.connection-info` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.GetConnectInfo` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/info.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/info_test.go::TestGetConnectInfo` - -### 通过 kb-adapter 插件创建 KubeBlocks 集群 - -- Capability ID: `rainbond.kb-adapter.cluster.create` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.CreateCluster` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go::TestCreateCluster` - -### 在集群清理时删除 OpsRequest 与关联 Secret - -- Capability ID: `rainbond.kb-adapter.cluster.delete-cleanup` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.cleanupClusterOpsRequests` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/lifecycle.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/lifecycle_test.go::TestCleanupClusterOpsRequests` - -### 构建插件集群详情摘要并包含资源与备份信息 - -- Capability ID: `rainbond.kb-adapter.cluster.detail-summary` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterDetail` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/info.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/info_test.go::TestGetClusterDetail` - -### 根据 OpsRequest 构建集群操作事件时间线 - -- Capability ID: `rainbond.kb-adapter.cluster.event-timeline` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.GetClusterEvents` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/event.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/event_test.go::TestGetClusterEvents` - -### 为插件视图列出集群 Pod 并解析 InstanceSet - -- Capability ID: `rainbond.kb-adapter.cluster.list-pods` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.getClusterPods` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/cluster.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/cluster_test.go::TestGetClusterPods` - -### 合并实时参数项与参数约束定义 - -- Capability ID: `rainbond.kb-adapter.cluster.parameter-constraint-merge` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.mergeEntriesAndConstraints` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/parameter.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/parameter_test.go::TestMergeEntriesAndConstraints` - -### 为插件诊断页构建详细 Pod 诊断信息 - -- Capability ID: `rainbond.kb-adapter.cluster.pod-detail` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.GetPodDetail` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/pod.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/pod_test.go::TestGetPodDetail` - -### 从备份恢复集群并清理失败的恢复操作 - -- Capability ID: `rainbond.kb-adapter.cluster.restore-from-backup` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.RestoreFromBackup` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/restore.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/restore_test.go::TestRestoreFromBackup` - -### 通过插件工作流扩缩集群副本资源与存储 - -- Capability ID: `rainbond.kb-adapter.cluster.scale` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/cluster.Service.ExpansionCluster` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/cluster/scaling.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/cluster/scaling_test.go::TestExpansionCluster` - -### 将插件参数值解析为带类型的协调器参数项 - -- Capability ID: `rainbond.kb-adapter.coordinator.parameter-value-parse` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/coordinator.Coordinator.ParseParameters` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/coordinator/coordinator.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/coordinator/coordinator_test.go::TestBase_ParseParameters` - -### 列出并清理阻塞中的未终态 OpsRequest - -- Capability ID: `rainbond.kb-adapter.opsrequest.blocking-ops-management` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/kbkit.GetAllNonFinalOpsRequests` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go::TestGetAllNonFinalOpsRequests` - -### 创建生命周期备份扩缩容参数变更与恢复等 OpsRequest - -- Capability ID: `rainbond.kb-adapter.opsrequest.create-supported-ops` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/kbkit.CreateLifecycleOpsRequest` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_test.go::TestCreateLifecycleOpsRequest` - -### 在提交新操作前裁决冲突的 OpsRequest - -- Capability ID: `rainbond.kb-adapter.opsrequest.preflight-arbitration` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/service/kbkit.preflightCheck` -- 代码路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/service/kbkit/opsrequest_preflight_test.go::TestUniqueOpsDecide` - -### 识别 kb-adapter 开发环境模式 - -- Capability ID: `rainbond.kb-adapter.server-config.dev-mode` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/internal/config.InDevelopment` -- 代码路径: `plugins/kb-adapter-rbdplugin/internal/config/config.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestInDevelopment` - -### 从环境变量加载 kb-adapter 服务配置 - -- Capability ID: `rainbond.kb-adapter.server-config.load-from-env` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/internal/config.LoadConfigFromEnv` -- 代码路径: `plugins/kb-adapter-rbdplugin/internal/config/config.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestLoadConfigFromEnv` - -### 强制加载并校验 kb-adapter 服务配置 - -- Capability ID: `rainbond.kb-adapter.server-config.must-load` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/internal/config.MustLoad` -- 代码路径: `plugins/kb-adapter-rbdplugin/internal/config/config.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestMustLoad` - -### 校验 kb-adapter 服务配置项 - -- Capability ID: `rainbond.kb-adapter.server-config.validate` -- 状态: `active` -- 测试类型: `regression` -- 接口类型: `workflow` -- 业务入口: `plugins/kb-adapter-rbdplugin/internal/config.ServerConfig.Validate` -- 代码路径: `plugins/kb-adapter-rbdplugin/internal/config/config.go` -- 测试路径: `plugins/kb-adapter-rbdplugin/internal/config/config_test.go::TestServerConfig_Validate` - ### 为 KubeBlocks 组件生成标签选择器 - Capability ID: `rainbond.kubeblocks.component-selector`