From 5245805e23760b87065d8020d0add8eb63812743 Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 14 Aug 2024 15:50:08 +0800 Subject: [PATCH] chore: remove lorry and built-in action handler (#7964) --- .github/utils/utils.sh | 3 - .github/workflows/cicd-push.yml | 2 +- Makefile | 5 - apis/apps/v1alpha1/clusterdefinition_types.go | 38 - .../v1alpha1/componentdefinition_types.go | 121 +- apis/apps/v1alpha1/zz_generated.deepcopy.go | 92 +- .../legacy/replicatedstatemachine_types.go | 8 - .../workloads/legacy/zz_generated.deepcopy.go | 5 - apis/workloads/v1alpha1/instanceset_types.go | 8 - .../v1alpha1/zz_generated.deepcopy.go | 5 - cmd/cmd.mk | 27 +- cmd/lorry/ctl/main.go | 28 - cmd/lorry/main.go | 136 - cmd/lorry/vault/plugin.go | 51 - cmd/manager/main.go | 3 - ...ps.kubeblocks.io_componentdefinitions.yaml | 5153 ++++++++--------- .../workloads.kubeblocks.io_instancesets.yaml | 7 - controllers/apps/component_controller_test.go | 2 + .../apps/componentdefinition_controller.go | 65 - .../componentdefinition_controller_test.go | 2 +- controllers/apps/operations/datascript.go | 349 +- .../apps/operations/datascript_test.go | 383 +- controllers/k8score/event_controller_test.go | 9 +- ...ps.kubeblocks.io_componentdefinitions.yaml | 5153 ++++++++--------- .../workloads.kubeblocks.io_instancesets.yaml | 7 - docker/Dockerfile-tools | 18 - docs/developer_docs/api-reference/cluster.md | 306 +- go.mod | 63 +- go.sum | 145 +- hack/docgen/lorryctl/main.go | 165 - pkg/common/doc.go | 2 +- pkg/common/types.go | 13 - pkg/constant/env.go | 33 - pkg/constant/lorry.go | 47 - .../component/action_post_provision.go | 2 +- .../component/action_post_provision_test.go | 18 +- .../component/action_pre_terminate_test.go | 16 +- pkg/controller/component/action_utils.go | 8 +- pkg/controller/component/action_utils_test.go | 16 +- pkg/controller/component/kbagent.go | 40 +- pkg/controller/component/lifecycle/kbagent.go | 12 +- pkg/controller/component/lorry_utils.go | 571 -- pkg/controller/component/lorry_utils_test.go | 279 - .../component/synthesize_component.go | 2 - pkg/controller/instanceset/object_builder.go | 59 +- .../instanceset/object_builder_test.go | 28 +- .../instanceset/pod_role_event_handler.go | 14 +- .../pod_role_event_handler_test.go | 5 +- .../instanceset/revision_util_test.go | 9 +- pkg/controller/instanceset/types.go | 4 +- pkg/controllerutil/pod_utils.go | 38 - pkg/kbagent/util/event.go | 1 + pkg/lorry/README.md | 49 - pkg/lorry/client/client.go | 271 - pkg/lorry/client/client_mock.go | 312 - pkg/lorry/client/generate.go | 22 - pkg/lorry/client/httpclient.go | 304 - pkg/lorry/client/httpclient_test.go | 872 --- pkg/lorry/client/interface.go | 53 - pkg/lorry/client/k8sexec_client.go | 203 - pkg/lorry/client/suite_test.go | 123 - pkg/lorry/cronjobs/job.go | 106 - pkg/lorry/cronjobs/manager.go | 69 - pkg/lorry/ctl/createuser.go | 69 - pkg/lorry/ctl/ctr.go | 122 - pkg/lorry/ctl/deleteuser.go | 67 - pkg/lorry/ctl/describeuser.go | 70 - pkg/lorry/ctl/grantrole.go | 69 - pkg/lorry/ctl/listsystemaccounts.go | 69 - pkg/lorry/ctl/listusers.go | 69 - pkg/lorry/ctl/options.go | 344 -- pkg/lorry/ctl/revokerole.go | 69 - pkg/lorry/ctl/run.go | 111 - pkg/lorry/ctl/switchover.go | 77 - pkg/lorry/ctl/util.go | 30 - pkg/lorry/ctl/vault_plugin.go | 76 - pkg/lorry/dcs/dcs.go | 84 - pkg/lorry/dcs/dcs_mock.go | 337 -- pkg/lorry/dcs/generate.go | 22 - pkg/lorry/dcs/k8s.go | 721 --- pkg/lorry/dcs/types.go | 293 - pkg/lorry/engines/base.go | 259 - pkg/lorry/engines/cluster_commands.go | 123 - pkg/lorry/engines/cluster_commands_test.go | 44 - pkg/lorry/engines/custom/get_replica_role.go | 155 - pkg/lorry/engines/custom/manager.go | 344 -- pkg/lorry/engines/custom/manager_test.go | 98 - pkg/lorry/engines/custom/suite_test.go | 70 - pkg/lorry/engines/dbmanager_mock.go | 743 --- pkg/lorry/engines/etcd/get_replica_role.go | 44 - pkg/lorry/engines/etcd/manager.go | 109 - pkg/lorry/engines/etcd/manager_test.go | 105 - pkg/lorry/engines/etcd/suite_test.go | 147 - pkg/lorry/engines/foxlake/commands.go | 81 - pkg/lorry/engines/foxlake/commands_test.go | 59 - pkg/lorry/engines/foxlake/suite_test.go | 32 - pkg/lorry/engines/generate.go | 22 - pkg/lorry/engines/interface.go | 128 - pkg/lorry/engines/mock.go | 153 - pkg/lorry/engines/models/client_types.go | 48 - pkg/lorry/engines/models/engine_types.go | 42 - pkg/lorry/engines/models/errors.go | 42 - pkg/lorry/engines/models/replca_role_types.go | 40 - pkg/lorry/engines/models/role_types.go | 72 - pkg/lorry/engines/models/userinfo.go | 84 - pkg/lorry/engines/mongodb/client.go | 98 - pkg/lorry/engines/mongodb/commands.go | 84 - pkg/lorry/engines/mongodb/commands_test.go | 59 - pkg/lorry/engines/mongodb/config.go | 141 - pkg/lorry/engines/mongodb/config_test.go | 77 - pkg/lorry/engines/mongodb/get_replica_role.go | 30 - pkg/lorry/engines/mongodb/manager.go | 836 --- pkg/lorry/engines/mongodb/replset.go | 92 - pkg/lorry/engines/mongodb/roles.go | 124 - pkg/lorry/engines/mongodb/types.go | 292 - pkg/lorry/engines/mongodb/users.go | 96 - pkg/lorry/engines/mysql/commands.go | 302 - pkg/lorry/engines/mysql/commands_test.go | 60 - pkg/lorry/engines/mysql/config.go | 212 - pkg/lorry/engines/mysql/config_test.go | 165 - pkg/lorry/engines/mysql/conn.go | 53 - pkg/lorry/engines/mysql/get_replica_role.go | 115 - .../engines/mysql/get_replica_role_test.go | 68 - pkg/lorry/engines/mysql/gtid.go | 171 - pkg/lorry/engines/mysql/gtid_test.go | 167 - pkg/lorry/engines/mysql/json.go | 100 - pkg/lorry/engines/mysql/json_test.go | 70 - pkg/lorry/engines/mysql/manager.go | 753 --- pkg/lorry/engines/mysql/manager_test.go | 920 --- pkg/lorry/engines/mysql/query.go | 53 - pkg/lorry/engines/mysql/query_test.go | 117 - pkg/lorry/engines/mysql/semi_sync.go | 192 - pkg/lorry/engines/mysql/semi_sync_test.go | 159 - pkg/lorry/engines/mysql/suite_test.go | 66 - pkg/lorry/engines/mysql/user.go | 196 - pkg/lorry/engines/mysql/util.go | 255 - pkg/lorry/engines/mysql/util_test.go | 147 - pkg/lorry/engines/nebula/commands.go | 79 - pkg/lorry/engines/nebula/commands_test.go | 59 - pkg/lorry/engines/nebula/suite_test.go | 32 - pkg/lorry/engines/oceanbase/commands.go | 96 - pkg/lorry/engines/oceanbase/commands_test.go | 71 - pkg/lorry/engines/oceanbase/config.go | 95 - pkg/lorry/engines/oceanbase/config_test.go | 54 - pkg/lorry/engines/oceanbase/conn.go | 77 - .../engines/oceanbase/get_replica_role.go | 89 - .../oceanbase/get_replica_role_test.go | 106 - pkg/lorry/engines/oceanbase/manager.go | 454 -- pkg/lorry/engines/oceanbase/suite_test.go | 32 - pkg/lorry/engines/opengauss/commands.go | 76 - pkg/lorry/engines/opengauss/commands_test.go | 60 - pkg/lorry/engines/opengauss/suite_test.go | 32 - pkg/lorry/engines/oracle/commands.go | 75 - pkg/lorry/engines/oracle/commands_test.go | 60 - pkg/lorry/engines/oracle/suite_test.go | 32 - pkg/lorry/engines/polardbx/config.go | 41 - .../engines/polardbx/get_replica_role.go | 55 - pkg/lorry/engines/polardbx/manager.go | 51 - .../apecloudpostgres/get_replica_role.go | 30 - .../postgres/apecloudpostgres/manager.go | 415 -- .../postgres/apecloudpostgres/manager_test.go | 846 --- pkg/lorry/engines/postgres/commands.go | 297 - pkg/lorry/engines/postgres/commands_test.go | 59 - pkg/lorry/engines/postgres/config.go | 103 - pkg/lorry/engines/postgres/config_test.go | 105 - pkg/lorry/engines/postgres/local_command.go | 115 - pkg/lorry/engines/postgres/manager.go | 228 - pkg/lorry/engines/postgres/manager_test.go | 343 -- .../officalpostgres/get_replica_role.go | 30 - .../postgres/officalpostgres/manager.go | 942 --- .../postgres/officalpostgres/manager_test.go | 1049 ---- pkg/lorry/engines/postgres/query.go | 216 - pkg/lorry/engines/postgres/query_test.go | 239 - pkg/lorry/engines/postgres/types.go | 364 -- pkg/lorry/engines/postgres/types_test.go | 239 - pkg/lorry/engines/postgres/user.go | 238 - pkg/lorry/engines/pulsar/commands.go | 74 - pkg/lorry/engines/pulsar/commands_test.go | 76 - pkg/lorry/engines/pulsar/suite_test.go | 32 - pkg/lorry/engines/redis/commands.go | 87 - pkg/lorry/engines/redis/commands_test.go | 59 - pkg/lorry/engines/redis/get_replica_role.go | 91 - pkg/lorry/engines/redis/manager.go | 119 - pkg/lorry/engines/redis/manager_test.go | 586 -- pkg/lorry/engines/redis/metadata.go | 82 - pkg/lorry/engines/redis/query.go | 50 - pkg/lorry/engines/redis/redis.go | 172 - pkg/lorry/engines/redis/settings.go | 119 - pkg/lorry/engines/redis/settings_test.go | 145 - pkg/lorry/engines/redis/suite_test.go | 66 - pkg/lorry/engines/redis/user.go | 285 - pkg/lorry/engines/register/managers.go | 238 - pkg/lorry/engines/register/managers_test.go | 218 - pkg/lorry/engines/suite_test.go | 32 - pkg/lorry/engines/util.go | 54 - pkg/lorry/engines/wesql/config.go | 41 - pkg/lorry/engines/wesql/config_test.go | 57 - pkg/lorry/engines/wesql/conn.go | 76 - pkg/lorry/engines/wesql/conn_test.go | 132 - pkg/lorry/engines/wesql/get_replica_role.go | 75 - .../engines/wesql/get_replica_role_test.go | 107 - pkg/lorry/engines/wesql/manager.go | 308 - pkg/lorry/engines/wesql/manager_test.go | 544 -- pkg/lorry/grpcserver/gprc_server.go | 115 - pkg/lorry/grpcserver/grpc_server_test.go | 95 - pkg/lorry/grpcserver/suite_test.go | 64 - pkg/lorry/highavailability/ha.go | 372 -- pkg/lorry/highavailability/util.go | 53 - pkg/lorry/httpserver/apis.go | 180 - pkg/lorry/httpserver/apis_test.go | 48 - pkg/lorry/httpserver/config.go | 43 - pkg/lorry/httpserver/endpoint.go | 37 - pkg/lorry/httpserver/errors.go | 34 - pkg/lorry/httpserver/server.go | 168 - pkg/lorry/httpserver/server_test.go | 161 - .../operations/component/post_provision.go | 97 - .../operations/component/pre_terminate.go | 92 - pkg/lorry/operations/fake.go | 90 - pkg/lorry/operations/interface.go | 60 - pkg/lorry/operations/operations.go | 60 - pkg/lorry/operations/register/register.go | 37 - pkg/lorry/operations/replica/checkrole.go | 259 - pkg/lorry/operations/replica/checkrunning.go | 127 - pkg/lorry/operations/replica/data_dump.go | 83 - pkg/lorry/operations/replica/data_load.go | 71 - pkg/lorry/operations/replica/getlag.go | 92 - pkg/lorry/operations/replica/getrole.go | 104 - pkg/lorry/operations/replica/healthcheck.go | 172 - pkg/lorry/operations/replica/join.go | 98 - pkg/lorry/operations/replica/leave.go | 108 - pkg/lorry/operations/replica/rebuild.go | 130 - pkg/lorry/operations/replica/switchover.go | 135 - pkg/lorry/operations/sql/exec.go | 83 - pkg/lorry/operations/sql/query.go | 80 - pkg/lorry/operations/types.go | 99 - pkg/lorry/operations/user/create.go | 118 - pkg/lorry/operations/user/delete.go | 84 - pkg/lorry/operations/user/describe.go | 100 - pkg/lorry/operations/user/describe_test.go | 41 - pkg/lorry/operations/user/grant_role.go | 85 - pkg/lorry/operations/user/list.go | 75 - .../operations/user/list_system_accounts.go | 75 - pkg/lorry/operations/user/revoke_role.go | 85 - pkg/lorry/operations/volume/lock.go | 83 - pkg/lorry/operations/volume/protect.go | 457 -- pkg/lorry/operations/volume/protect_test.go | 424 -- pkg/lorry/operations/volume/suite_test.go | 83 - pkg/lorry/operations/volume/unlock.go | 83 - pkg/lorry/probe.proto | 58 - pkg/lorry/util/command.go | 72 - pkg/lorry/util/config/decode.go | 198 - pkg/lorry/util/config/deepcopy.go | 176 - pkg/lorry/util/config/deepcopy_test.go | 194 - pkg/lorry/util/event.go | 139 - pkg/lorry/util/kubernetes/client.go | 62 - pkg/lorry/util/ping.go | 93 - pkg/lorry/util/types.go | 145 - pkg/lorry/vault/database.go | 168 - pkg/testutil/apps/constant.go | 42 +- 259 files changed, 5048 insertions(+), 41021 deletions(-) delete mode 100644 cmd/lorry/ctl/main.go delete mode 100644 cmd/lorry/main.go delete mode 100644 cmd/lorry/vault/plugin.go delete mode 100644 hack/docgen/lorryctl/main.go delete mode 100644 pkg/constant/lorry.go delete mode 100644 pkg/controller/component/lorry_utils.go delete mode 100644 pkg/controller/component/lorry_utils_test.go delete mode 100644 pkg/lorry/README.md delete mode 100644 pkg/lorry/client/client.go delete mode 100644 pkg/lorry/client/client_mock.go delete mode 100644 pkg/lorry/client/generate.go delete mode 100644 pkg/lorry/client/httpclient.go delete mode 100644 pkg/lorry/client/httpclient_test.go delete mode 100644 pkg/lorry/client/interface.go delete mode 100644 pkg/lorry/client/k8sexec_client.go delete mode 100644 pkg/lorry/client/suite_test.go delete mode 100644 pkg/lorry/cronjobs/job.go delete mode 100644 pkg/lorry/cronjobs/manager.go delete mode 100644 pkg/lorry/ctl/createuser.go delete mode 100644 pkg/lorry/ctl/ctr.go delete mode 100644 pkg/lorry/ctl/deleteuser.go delete mode 100644 pkg/lorry/ctl/describeuser.go delete mode 100644 pkg/lorry/ctl/grantrole.go delete mode 100644 pkg/lorry/ctl/listsystemaccounts.go delete mode 100644 pkg/lorry/ctl/listusers.go delete mode 100644 pkg/lorry/ctl/options.go delete mode 100644 pkg/lorry/ctl/revokerole.go delete mode 100644 pkg/lorry/ctl/run.go delete mode 100644 pkg/lorry/ctl/switchover.go delete mode 100644 pkg/lorry/ctl/util.go delete mode 100644 pkg/lorry/ctl/vault_plugin.go delete mode 100644 pkg/lorry/dcs/dcs.go delete mode 100644 pkg/lorry/dcs/dcs_mock.go delete mode 100644 pkg/lorry/dcs/generate.go delete mode 100644 pkg/lorry/dcs/k8s.go delete mode 100644 pkg/lorry/dcs/types.go delete mode 100644 pkg/lorry/engines/base.go delete mode 100644 pkg/lorry/engines/cluster_commands.go delete mode 100644 pkg/lorry/engines/cluster_commands_test.go delete mode 100644 pkg/lorry/engines/custom/get_replica_role.go delete mode 100644 pkg/lorry/engines/custom/manager.go delete mode 100644 pkg/lorry/engines/custom/manager_test.go delete mode 100644 pkg/lorry/engines/custom/suite_test.go delete mode 100644 pkg/lorry/engines/dbmanager_mock.go delete mode 100644 pkg/lorry/engines/etcd/get_replica_role.go delete mode 100644 pkg/lorry/engines/etcd/manager.go delete mode 100644 pkg/lorry/engines/etcd/manager_test.go delete mode 100644 pkg/lorry/engines/etcd/suite_test.go delete mode 100644 pkg/lorry/engines/foxlake/commands.go delete mode 100644 pkg/lorry/engines/foxlake/commands_test.go delete mode 100644 pkg/lorry/engines/foxlake/suite_test.go delete mode 100644 pkg/lorry/engines/generate.go delete mode 100644 pkg/lorry/engines/interface.go delete mode 100644 pkg/lorry/engines/mock.go delete mode 100644 pkg/lorry/engines/models/client_types.go delete mode 100644 pkg/lorry/engines/models/engine_types.go delete mode 100644 pkg/lorry/engines/models/errors.go delete mode 100644 pkg/lorry/engines/models/replca_role_types.go delete mode 100644 pkg/lorry/engines/models/role_types.go delete mode 100644 pkg/lorry/engines/models/userinfo.go delete mode 100644 pkg/lorry/engines/mongodb/client.go delete mode 100644 pkg/lorry/engines/mongodb/commands.go delete mode 100644 pkg/lorry/engines/mongodb/commands_test.go delete mode 100644 pkg/lorry/engines/mongodb/config.go delete mode 100644 pkg/lorry/engines/mongodb/config_test.go delete mode 100644 pkg/lorry/engines/mongodb/get_replica_role.go delete mode 100644 pkg/lorry/engines/mongodb/manager.go delete mode 100644 pkg/lorry/engines/mongodb/replset.go delete mode 100644 pkg/lorry/engines/mongodb/roles.go delete mode 100644 pkg/lorry/engines/mongodb/types.go delete mode 100644 pkg/lorry/engines/mongodb/users.go delete mode 100644 pkg/lorry/engines/mysql/commands.go delete mode 100644 pkg/lorry/engines/mysql/commands_test.go delete mode 100644 pkg/lorry/engines/mysql/config.go delete mode 100644 pkg/lorry/engines/mysql/config_test.go delete mode 100644 pkg/lorry/engines/mysql/conn.go delete mode 100644 pkg/lorry/engines/mysql/get_replica_role.go delete mode 100644 pkg/lorry/engines/mysql/get_replica_role_test.go delete mode 100644 pkg/lorry/engines/mysql/gtid.go delete mode 100644 pkg/lorry/engines/mysql/gtid_test.go delete mode 100644 pkg/lorry/engines/mysql/json.go delete mode 100644 pkg/lorry/engines/mysql/json_test.go delete mode 100644 pkg/lorry/engines/mysql/manager.go delete mode 100644 pkg/lorry/engines/mysql/manager_test.go delete mode 100644 pkg/lorry/engines/mysql/query.go delete mode 100644 pkg/lorry/engines/mysql/query_test.go delete mode 100644 pkg/lorry/engines/mysql/semi_sync.go delete mode 100644 pkg/lorry/engines/mysql/semi_sync_test.go delete mode 100644 pkg/lorry/engines/mysql/suite_test.go delete mode 100644 pkg/lorry/engines/mysql/user.go delete mode 100644 pkg/lorry/engines/mysql/util.go delete mode 100644 pkg/lorry/engines/mysql/util_test.go delete mode 100644 pkg/lorry/engines/nebula/commands.go delete mode 100644 pkg/lorry/engines/nebula/commands_test.go delete mode 100644 pkg/lorry/engines/nebula/suite_test.go delete mode 100644 pkg/lorry/engines/oceanbase/commands.go delete mode 100644 pkg/lorry/engines/oceanbase/commands_test.go delete mode 100644 pkg/lorry/engines/oceanbase/config.go delete mode 100644 pkg/lorry/engines/oceanbase/config_test.go delete mode 100644 pkg/lorry/engines/oceanbase/conn.go delete mode 100644 pkg/lorry/engines/oceanbase/get_replica_role.go delete mode 100644 pkg/lorry/engines/oceanbase/get_replica_role_test.go delete mode 100644 pkg/lorry/engines/oceanbase/manager.go delete mode 100644 pkg/lorry/engines/oceanbase/suite_test.go delete mode 100644 pkg/lorry/engines/opengauss/commands.go delete mode 100644 pkg/lorry/engines/opengauss/commands_test.go delete mode 100644 pkg/lorry/engines/opengauss/suite_test.go delete mode 100644 pkg/lorry/engines/oracle/commands.go delete mode 100644 pkg/lorry/engines/oracle/commands_test.go delete mode 100644 pkg/lorry/engines/oracle/suite_test.go delete mode 100644 pkg/lorry/engines/polardbx/config.go delete mode 100644 pkg/lorry/engines/polardbx/get_replica_role.go delete mode 100644 pkg/lorry/engines/polardbx/manager.go delete mode 100644 pkg/lorry/engines/postgres/apecloudpostgres/get_replica_role.go delete mode 100644 pkg/lorry/engines/postgres/apecloudpostgres/manager.go delete mode 100644 pkg/lorry/engines/postgres/apecloudpostgres/manager_test.go delete mode 100644 pkg/lorry/engines/postgres/commands.go delete mode 100644 pkg/lorry/engines/postgres/commands_test.go delete mode 100644 pkg/lorry/engines/postgres/config.go delete mode 100644 pkg/lorry/engines/postgres/config_test.go delete mode 100644 pkg/lorry/engines/postgres/local_command.go delete mode 100644 pkg/lorry/engines/postgres/manager.go delete mode 100644 pkg/lorry/engines/postgres/manager_test.go delete mode 100644 pkg/lorry/engines/postgres/officalpostgres/get_replica_role.go delete mode 100644 pkg/lorry/engines/postgres/officalpostgres/manager.go delete mode 100644 pkg/lorry/engines/postgres/officalpostgres/manager_test.go delete mode 100644 pkg/lorry/engines/postgres/query.go delete mode 100644 pkg/lorry/engines/postgres/query_test.go delete mode 100644 pkg/lorry/engines/postgres/types.go delete mode 100644 pkg/lorry/engines/postgres/types_test.go delete mode 100644 pkg/lorry/engines/postgres/user.go delete mode 100644 pkg/lorry/engines/pulsar/commands.go delete mode 100644 pkg/lorry/engines/pulsar/commands_test.go delete mode 100644 pkg/lorry/engines/pulsar/suite_test.go delete mode 100644 pkg/lorry/engines/redis/commands.go delete mode 100644 pkg/lorry/engines/redis/commands_test.go delete mode 100644 pkg/lorry/engines/redis/get_replica_role.go delete mode 100644 pkg/lorry/engines/redis/manager.go delete mode 100644 pkg/lorry/engines/redis/manager_test.go delete mode 100644 pkg/lorry/engines/redis/metadata.go delete mode 100644 pkg/lorry/engines/redis/query.go delete mode 100644 pkg/lorry/engines/redis/redis.go delete mode 100644 pkg/lorry/engines/redis/settings.go delete mode 100644 pkg/lorry/engines/redis/settings_test.go delete mode 100644 pkg/lorry/engines/redis/suite_test.go delete mode 100644 pkg/lorry/engines/redis/user.go delete mode 100644 pkg/lorry/engines/register/managers.go delete mode 100644 pkg/lorry/engines/register/managers_test.go delete mode 100644 pkg/lorry/engines/suite_test.go delete mode 100644 pkg/lorry/engines/util.go delete mode 100644 pkg/lorry/engines/wesql/config.go delete mode 100644 pkg/lorry/engines/wesql/config_test.go delete mode 100644 pkg/lorry/engines/wesql/conn.go delete mode 100644 pkg/lorry/engines/wesql/conn_test.go delete mode 100644 pkg/lorry/engines/wesql/get_replica_role.go delete mode 100644 pkg/lorry/engines/wesql/get_replica_role_test.go delete mode 100644 pkg/lorry/engines/wesql/manager.go delete mode 100644 pkg/lorry/engines/wesql/manager_test.go delete mode 100644 pkg/lorry/grpcserver/gprc_server.go delete mode 100644 pkg/lorry/grpcserver/grpc_server_test.go delete mode 100644 pkg/lorry/grpcserver/suite_test.go delete mode 100644 pkg/lorry/highavailability/ha.go delete mode 100644 pkg/lorry/highavailability/util.go delete mode 100644 pkg/lorry/httpserver/apis.go delete mode 100644 pkg/lorry/httpserver/apis_test.go delete mode 100644 pkg/lorry/httpserver/config.go delete mode 100644 pkg/lorry/httpserver/endpoint.go delete mode 100644 pkg/lorry/httpserver/errors.go delete mode 100644 pkg/lorry/httpserver/server.go delete mode 100644 pkg/lorry/httpserver/server_test.go delete mode 100644 pkg/lorry/operations/component/post_provision.go delete mode 100644 pkg/lorry/operations/component/pre_terminate.go delete mode 100644 pkg/lorry/operations/fake.go delete mode 100644 pkg/lorry/operations/interface.go delete mode 100644 pkg/lorry/operations/operations.go delete mode 100644 pkg/lorry/operations/register/register.go delete mode 100644 pkg/lorry/operations/replica/checkrole.go delete mode 100644 pkg/lorry/operations/replica/checkrunning.go delete mode 100644 pkg/lorry/operations/replica/data_dump.go delete mode 100644 pkg/lorry/operations/replica/data_load.go delete mode 100644 pkg/lorry/operations/replica/getlag.go delete mode 100644 pkg/lorry/operations/replica/getrole.go delete mode 100644 pkg/lorry/operations/replica/healthcheck.go delete mode 100644 pkg/lorry/operations/replica/join.go delete mode 100644 pkg/lorry/operations/replica/leave.go delete mode 100644 pkg/lorry/operations/replica/rebuild.go delete mode 100644 pkg/lorry/operations/replica/switchover.go delete mode 100644 pkg/lorry/operations/sql/exec.go delete mode 100644 pkg/lorry/operations/sql/query.go delete mode 100644 pkg/lorry/operations/types.go delete mode 100644 pkg/lorry/operations/user/create.go delete mode 100644 pkg/lorry/operations/user/delete.go delete mode 100644 pkg/lorry/operations/user/describe.go delete mode 100644 pkg/lorry/operations/user/describe_test.go delete mode 100644 pkg/lorry/operations/user/grant_role.go delete mode 100644 pkg/lorry/operations/user/list.go delete mode 100644 pkg/lorry/operations/user/list_system_accounts.go delete mode 100644 pkg/lorry/operations/user/revoke_role.go delete mode 100644 pkg/lorry/operations/volume/lock.go delete mode 100644 pkg/lorry/operations/volume/protect.go delete mode 100644 pkg/lorry/operations/volume/protect_test.go delete mode 100644 pkg/lorry/operations/volume/suite_test.go delete mode 100644 pkg/lorry/operations/volume/unlock.go delete mode 100644 pkg/lorry/probe.proto delete mode 100644 pkg/lorry/util/command.go delete mode 100644 pkg/lorry/util/config/decode.go delete mode 100644 pkg/lorry/util/config/deepcopy.go delete mode 100644 pkg/lorry/util/config/deepcopy_test.go delete mode 100644 pkg/lorry/util/event.go delete mode 100644 pkg/lorry/util/kubernetes/client.go delete mode 100644 pkg/lorry/util/ping.go delete mode 100644 pkg/lorry/util/types.go delete mode 100644 pkg/lorry/vault/database.go diff --git a/.github/utils/utils.sh b/.github/utils/utils.sh index 41ac977a6ca..0569c724ba5 100644 --- a/.github/utils/utils.sh +++ b/.github/utils/utils.sh @@ -514,9 +514,6 @@ get_trigger_mode() { apis/*) add_trigger_mode "[apis][test]" ;; - pkg/lorry/ctl/*) - add_trigger_mode "[lorry][test]" - ;; *) add_trigger_mode "[test]" ;; diff --git a/.github/workflows/cicd-push.yml b/.github/workflows/cicd-push.yml index fb4ea0886e5..56d1f359d24 100644 --- a/.github/workflows/cicd-push.yml +++ b/.github/workflows/cicd-push.yml @@ -270,7 +270,7 @@ jobs: apis-doc: needs: [ trigger-mode ] runs-on: ubuntu-latest - if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[apis]') || contains(needs.trigger-mode.outputs.trigger-mode, '[lorry]') }} + if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[apis]') }} steps: - uses: actions/checkout@v4 - name: install lib diff --git a/Makefile b/Makefile index c23640ec1a9..77e82d6d594 100644 --- a/Makefile +++ b/Makefile @@ -255,11 +255,6 @@ endif goimports: goimportstool ## Run goimports against code. $(GOIMPORTS) -local github.com/apecloud/kubeblocks -w $$(git ls-files|grep "\.go$$" | grep -v $(GENERATED_CLIENT_PKG) | grep -v $(GENERATED_DEEP_COPY_FILE)) - -.PHONY: lorryctl-doc -lorryctl-doc: generate test-go-generate ## generate CLI command reference manual. - $(GO) run ./hack/docgen/lorryctl/main.go ./docs/user_docs/lorryctl - .PHONY: api-doc api-doc: ## generate API reference manual. $(GO) run ./hack/docgen/api/main.go -api-dir github.com/apecloud/kubeblocks/apis -config ./hack/docgen/api/gen-api-doc-config.json -template-dir ./hack/docgen/api/template -out-dir ./docs/developer_docs/api-reference/ diff --git a/apis/apps/v1alpha1/clusterdefinition_types.go b/apis/apps/v1alpha1/clusterdefinition_types.go index 5b9fae0b09f..3612f577700 100644 --- a/apis/apps/v1alpha1/clusterdefinition_types.go +++ b/apis/apps/v1alpha1/clusterdefinition_types.go @@ -224,44 +224,6 @@ type LogConfig struct { FilePathPattern string `json:"filePathPattern"` } -// TODO(v1.0): remove this after lorry - -// VolumeProtectionSpec is deprecated since v0.9, replaced with ComponentVolume.HighWatermark. -type VolumeProtectionSpec struct { - // The high watermark threshold for volume space usage. - // If there is any specified volumes who's space usage is over the threshold, the pre-defined "LOCK" action - // will be triggered to degrade the service to protect volume from space exhaustion, such as to set the instance - // as read-only. And after that, if all volumes' space usage drops under the threshold later, the pre-defined - // "UNLOCK" action will be performed to recover the service normally. - // - // +kubebuilder:validation:Maximum=100 - // +kubebuilder:validation:Minimum=0 - // +kubebuilder:default=90 - // +optional - HighWatermark int `json:"highWatermark,omitempty"` - - // The Volumes to be protected. - // - // +optional - Volumes []ProtectedVolume `json:"volumes,omitempty"` -} - -// ProtectedVolume is deprecated since v0.9, replaced with ComponentVolume.HighWatermark. -type ProtectedVolume struct { - // The Name of the volume to protect. - // - // +optional - Name string `json:"name,omitempty"` - - // Defines the high watermark threshold for the volume, it will override the component level threshold. - // If the value is invalid, it will be ignored and the component level threshold will be used. - // - // +kubebuilder:validation:Maximum=100 - // +kubebuilder:validation:Minimum=0 - // +optional - HighWatermark *int `json:"highWatermark,omitempty"` -} - // ServiceRefDeclaration represents a reference to a service that can be either provided by a KubeBlocks Cluster // or an external service. // It acts as a placeholder for the actual service reference, which is determined later when a Cluster is created. diff --git a/apis/apps/v1alpha1/componentdefinition_types.go b/apis/apps/v1alpha1/componentdefinition_types.go index bcb452f7b95..cb75f0b0ff0 100644 --- a/apis/apps/v1alpha1/componentdefinition_types.go +++ b/apis/apps/v1alpha1/componentdefinition_types.go @@ -697,8 +697,6 @@ const ( ) // ExecAction describes an Action that executes a command inside a container. -// Which may run as a K8s job or be executed inside the Lorry sidecar container, depending on the implementation. -// Future implementations will standardize execution within Lorry. type ExecAction struct { // Specifies the container image to be used for running the Action. // @@ -826,8 +824,6 @@ const ( // Actions can be executed in different ways: // // - ExecAction: Executes a command inside a container. -// which may run as a K8s job or be executed inside the Lorry sidecar container, depending on the implementation. -// Future implementations will standardize execution within Lorry. // A set of predefined environment variables are available and can be leveraged within the `exec.command` // to access context information such as details about pods, components, the overall cluster state, // or database connection credentials. @@ -898,11 +894,6 @@ type Action struct { type Probe struct { Action `json:",inline"` - // TODO: remove this later. - // - // +optional - BuiltinHandler *BuiltinActionHandlerType `json:"builtinHandler,omitempty"` - // Specifies the number of seconds to wait after the container has started before the RoleProbe // begins to detect the container's role. // @@ -928,96 +919,6 @@ type Probe struct { FailureThreshold int32 `json:"failureThreshold,omitempty"` } -// BuiltinActionHandlerType defines build-in action handlers provided by Lorry, including: -// -// - `mysql` -// - `wesql` -// - `oceanbase` -// - `redis` -// - `mongodb` -// - `etcd` -// - `postgresql` -// - `official-postgresql` -// - `apecloud-postgresql` -// - `polardbx` -// - `custom` -// - `unknown` -type BuiltinActionHandlerType string - -const ( - MySQLBuiltinActionHandler BuiltinActionHandlerType = "mysql" - WeSQLBuiltinActionHandler BuiltinActionHandlerType = "wesql" - OceanbaseBuiltinActionHandler BuiltinActionHandlerType = "oceanbase" - RedisBuiltinActionHandler BuiltinActionHandlerType = "redis" - MongoDBBuiltinActionHandler BuiltinActionHandlerType = "mongodb" - ETCDBuiltinActionHandler BuiltinActionHandlerType = "etcd" - PostgresqlBuiltinActionHandler BuiltinActionHandlerType = "postgresql" - OfficialPostgresqlBuiltinActionHandler BuiltinActionHandlerType = "official-postgresql" - ApeCloudPostgresqlBuiltinActionHandler BuiltinActionHandlerType = "apecloud-postgresql" - PolarDBXBuiltinActionHandler BuiltinActionHandlerType = "polardbx" - CustomActionHandler BuiltinActionHandlerType = "custom" - UnknownBuiltinActionHandler BuiltinActionHandlerType = "unknown" -) - -// LifecycleActionHandler describes the implementation of a specific lifecycle action. -// -// Each action is deemed successful if it returns an exit code of 0 for command executions, -// or an HTTP 200 status for HTTP(s) actions. -// Any other exit code or HTTP status is considered an indication of failure. -type LifecycleActionHandler struct { - // Specifies the name of the predefined action handler to be invoked for lifecycle actions. - // - // Lorry, as a sidecar agent co-located with the database container in the same Pod, - // includes a suite of built-in action implementations that are tailored to different database engines. - // These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - // `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - // - // If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - // to execute the specified lifecycle actions. - // - // The `builtinHandler` field is of type `BuiltinActionHandlerType`, - // which represents the name of the built-in handler. - // The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - // actions. - // This means that if you specify a built-in handler for one action, you should use the same handler - // for all other actions throughout the entire `ComponentLifecycleActions` collection. - // - // If you need to define lifecycle actions for database engines not covered by the existing built-in support, - // or when the pre-existing built-in handlers do not meet your specific needs, - // you can use the `customHandler` field to define your own action implementation. - // - // Deprecation Notice: - // - // - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - // for configuring all lifecycle actions. - // - Instead of using a name to indicate the built-in action implementations in Lorry, - // the recommended approach will be to explicitly invoke the desired action implementation through - // a gRPC interface exposed by the sidecar agent. - // - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - // or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - // - This change will allow for greater customization and extensibility of lifecycle actions, - // as developers can create their own "builtin" implementations tailored to their specific requirements. - // - // +optional - BuiltinHandler *BuiltinActionHandlerType `json:"builtinHandler,omitempty"` - - // Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - // It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - // tailored actions. - // - // An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - // to support GRPCAction, - // thereby accommodating unique logic for different database systems within the Action's framework. - // - // In future iterations, all built-in handlers are expected to transition to GRPCAction. - // This change means that Lorry or other sidecar agents will expose the implementation of actions - // through a GRPC interface for external invocation. - // Then the controller will interact with these actions via GRPCAction calls. - // - // +optional - CustomHandler *Action `json:"customHandler,omitempty"` -} - // ComponentLifecycleActions defines a collection of Actions for customizing the behavior of a Component. type ComponentLifecycleActions struct { // Specifies the hook to be executed after a component's creation. @@ -1055,7 +956,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - PostProvision *LifecycleActionHandler `json:"postProvision,omitempty"` + PostProvision *Action `json:"postProvision,omitempty"` // Specifies the hook to be executed prior to terminating a component. // @@ -1098,11 +999,11 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - PreTerminate *LifecycleActionHandler `json:"preTerminate,omitempty"` + PreTerminate *Action `json:"preTerminate,omitempty"` // Defines the procedure which is invoked regularly to assess the role of replicas. // - // This action is periodically triggered by Lorry at the specified interval to determine the role of each replica. + // This action is periodically triggered at the specified interval to determine the role of each replica. // Upon successful execution, the action's output designates the role of the replica, // which should match one of the predefined role names within `componentDefinition.spec.roles`. // The output is then compared with the previous successful execution result. @@ -1176,7 +1077,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - MemberJoin *LifecycleActionHandler `json:"memberJoin,omitempty"` + MemberJoin *Action `json:"memberJoin,omitempty"` // Defines the procedure to remove a replica from the replication group. // @@ -1209,7 +1110,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - MemberLeave *LifecycleActionHandler `json:"memberLeave,omitempty"` + MemberLeave *Action `json:"memberLeave,omitempty"` // Defines the procedure to switch a replica into the read-only state. // @@ -1226,7 +1127,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - Readonly *LifecycleActionHandler `json:"readonly,omitempty"` + Readonly *Action `json:"readonly,omitempty"` // Defines the procedure to transition a replica from the read-only state back to the read-write state. // @@ -1245,7 +1146,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - Readwrite *LifecycleActionHandler `json:"readwrite,omitempty"` + Readwrite *Action `json:"readwrite,omitempty"` // Defines the procedure for exporting the data from a replica. // @@ -1264,7 +1165,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - DataDump *LifecycleActionHandler `json:"dataDump,omitempty"` + DataDump *Action `json:"dataDump,omitempty"` // Defines the procedure for importing data into a replica. // @@ -1282,7 +1183,7 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - DataLoad *LifecycleActionHandler `json:"dataLoad,omitempty"` + DataLoad *Action `json:"dataLoad,omitempty"` // Defines the procedure that update a replica with new configuration. // @@ -1291,7 +1192,7 @@ type ComponentLifecycleActions struct { // This Action is reserved for future versions. // // +optional - Reconfigure *LifecycleActionHandler `json:"reconfigure,omitempty"` + Reconfigure *Action `json:"reconfigure,omitempty"` // Defines the procedure to generate a new database account. // @@ -1302,5 +1203,5 @@ type ComponentLifecycleActions struct { // Note: This field is immutable once it has been set. // // +optional - AccountProvision *LifecycleActionHandler `json:"accountProvision,omitempty"` + AccountProvision *Action `json:"accountProvision,omitempty"` } diff --git a/apis/apps/v1alpha1/zz_generated.deepcopy.go b/apis/apps/v1alpha1/zz_generated.deepcopy.go index f18ecc0c2f9..fb60452a065 100644 --- a/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -1347,12 +1347,12 @@ func (in *ComponentLifecycleActions) DeepCopyInto(out *ComponentLifecycleActions *out = *in if in.PostProvision != nil { in, out := &in.PostProvision, &out.PostProvision - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.PreTerminate != nil { in, out := &in.PreTerminate, &out.PreTerminate - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.RoleProbe != nil { @@ -1367,42 +1367,42 @@ func (in *ComponentLifecycleActions) DeepCopyInto(out *ComponentLifecycleActions } if in.MemberJoin != nil { in, out := &in.MemberJoin, &out.MemberJoin - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.MemberLeave != nil { in, out := &in.MemberLeave, &out.MemberLeave - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.Readonly != nil { in, out := &in.Readonly, &out.Readonly - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.Readwrite != nil { in, out := &in.Readwrite, &out.Readwrite - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.DataDump != nil { in, out := &in.DataDump, &out.DataDump - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.DataLoad != nil { in, out := &in.DataLoad, &out.DataLoad - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.Reconfigure != nil { in, out := &in.Reconfigure, &out.Reconfigure - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } if in.AccountProvision != nil { in, out := &in.AccountProvision, &out.AccountProvision - *out = new(LifecycleActionHandler) + *out = new(Action) (*in).DeepCopyInto(*out) } } @@ -3073,31 +3073,6 @@ func (in *LegacyRenderedTemplateSpec) DeepCopy() *LegacyRenderedTemplateSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LifecycleActionHandler) DeepCopyInto(out *LifecycleActionHandler) { - *out = *in - if in.BuiltinHandler != nil { - in, out := &in.BuiltinHandler, &out.BuiltinHandler - *out = new(BuiltinActionHandlerType) - **out = **in - } - if in.CustomHandler != nil { - in, out := &in.CustomHandler, &out.CustomHandler - *out = new(Action) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleActionHandler. -func (in *LifecycleActionHandler) DeepCopy() *LifecycleActionHandler { - if in == nil { - return nil - } - out := new(LifecycleActionHandler) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LogConfig) DeepCopyInto(out *LogConfig) { *out = *in @@ -4027,11 +4002,6 @@ func (in *PreConditionExec) DeepCopy() *PreConditionExec { func (in *Probe) DeepCopyInto(out *Probe) { *out = *in in.Action.DeepCopyInto(&out.Action) - if in.BuiltinHandler != nil { - in, out := &in.BuiltinHandler, &out.BuiltinHandler - *out = new(BuiltinActionHandlerType) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Probe. @@ -4066,26 +4036,6 @@ func (in *ProgressStatusDetail) DeepCopy() *ProgressStatusDetail { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProtectedVolume) DeepCopyInto(out *ProtectedVolume) { - *out = *in - if in.HighWatermark != nil { - in, out := &in.HighWatermark, &out.HighWatermark - *out = new(int) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProtectedVolume. -func (in *ProtectedVolume) DeepCopy() *ProtectedVolume { - if in == nil { - return nil - } - out := new(ProtectedVolume) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProvisionSecretRef) DeepCopyInto(out *ProvisionSecretRef) { *out = *in @@ -5420,25 +5370,3 @@ func (in *VolumeExpansion) DeepCopy() *VolumeExpansion { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *VolumeProtectionSpec) DeepCopyInto(out *VolumeProtectionSpec) { - *out = *in - if in.Volumes != nil { - in, out := &in.Volumes, &out.Volumes - *out = make([]ProtectedVolume, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeProtectionSpec. -func (in *VolumeProtectionSpec) DeepCopy() *VolumeProtectionSpec { - if in == nil { - return nil - } - out := new(VolumeProtectionSpec) - in.DeepCopyInto(out) - return out -} diff --git a/apis/workloads/legacy/replicatedstatemachine_types.go b/apis/workloads/legacy/replicatedstatemachine_types.go index d9bc784d876..fefb3e095cb 100644 --- a/apis/workloads/legacy/replicatedstatemachine_types.go +++ b/apis/workloads/legacy/replicatedstatemachine_types.go @@ -355,15 +355,7 @@ const ( // RoleProbe defines how to observe role type RoleProbe struct { - // Specifies the builtin handler name to use to probe the role of the main container. - // Available handlers include: mysql, postgres, mongodb, redis, etcd, kafka. - // Use CustomHandler to define a custom role probe function if none of the built-in handlers meet the requirement. - // - // +optional - BuiltinHandler *string `json:"builtinHandlerName,omitempty"` - // Defines a custom method for role probing. - // If the BuiltinHandler meets the requirement, use it instead. // Actions defined here are executed in series. // Upon completion of all actions, the final output should be a single string representing the role name defined in spec.Roles. // The latest [BusyBox](https://busybox.net/) image will be used if Image is not configured. diff --git a/apis/workloads/legacy/zz_generated.deepcopy.go b/apis/workloads/legacy/zz_generated.deepcopy.go index ce5d624e353..0e4881f0604 100644 --- a/apis/workloads/legacy/zz_generated.deepcopy.go +++ b/apis/workloads/legacy/zz_generated.deepcopy.go @@ -439,11 +439,6 @@ func (in *ReplicatedStateMachineStatus) DeepCopy() *ReplicatedStateMachineStatus // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoleProbe) DeepCopyInto(out *RoleProbe) { *out = *in - if in.BuiltinHandler != nil { - in, out := &in.BuiltinHandler, &out.BuiltinHandler - *out = new(string) - **out = **in - } if in.CustomHandler != nil { in, out := &in.CustomHandler, &out.CustomHandler *out = make([]Action, len(*in)) diff --git a/apis/workloads/v1alpha1/instanceset_types.go b/apis/workloads/v1alpha1/instanceset_types.go index c10cb08f846..22502344e92 100644 --- a/apis/workloads/v1alpha1/instanceset_types.go +++ b/apis/workloads/v1alpha1/instanceset_types.go @@ -559,15 +559,7 @@ const ( // RoleProbe defines how to observe role type RoleProbe struct { - // Specifies the builtin handler name to use to probe the role of the main container. - // Available handlers include: mysql, postgres, mongodb, redis, etcd, kafka. - // Use CustomHandler to define a custom role probe function if none of the built-in handlers meet the requirement. - // - // +optional - BuiltinHandler *string `json:"builtinHandlerName,omitempty"` - // Defines a custom method for role probing. - // If the BuiltinHandler meets the requirement, use it instead. // Actions defined here are executed in series. // Upon completion of all actions, the final output should be a single string representing the role name defined in spec.Roles. // The latest [BusyBox](https://busybox.net/) image will be used if Image is not configured. diff --git a/apis/workloads/v1alpha1/zz_generated.deepcopy.go b/apis/workloads/v1alpha1/zz_generated.deepcopy.go index e8f6d3babb6..4a67647f9c5 100644 --- a/apis/workloads/v1alpha1/zz_generated.deepcopy.go +++ b/apis/workloads/v1alpha1/zz_generated.deepcopy.go @@ -492,11 +492,6 @@ func (in *ReplicaRole) DeepCopy() *ReplicaRole { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoleProbe) DeepCopyInto(out *RoleProbe) { *out = *in - if in.BuiltinHandler != nil { - in, out := &in.BuiltinHandler, &out.BuiltinHandler - *out = new(string) - **out = **in - } if in.CustomHandler != nil { in, out := &in.CustomHandler, &out.CustomHandler *out = make([]Action, len(*in)) diff --git a/cmd/cmd.mk b/cmd/cmd.mk index 8e7607b55ba..20cf86ccf7b 100644 --- a/cmd/cmd.mk +++ b/cmd/cmd.mk @@ -71,29 +71,4 @@ cue-helper: test-go-generate build-checks ## Build cue-helper related binaries .PHONY: clean-cue-helper clean-cue-helper: ## Clean bin/cue-helper. - rm -f bin/cue-helper - -## lorry cmd - -LORRY_LD_FLAGS = "-s -w" - -bin/lorry.%: ## Cross build bin/lorry.$(OS).$(ARCH) . - GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO) build -ldflags=${LORRY_LD_FLAGS} -o $@ ./cmd/lorry/main.go - -.PHONY: lorry -lorry: OS=$(shell $(GO) env GOOS) -lorry: ARCH=$(shell $(GO) env GOARCH) -lorry: test-go-generate build-checks ## Build lorry related binaries - $(MAKE) bin/lorry.${OS}.${ARCH} - mv bin/lorry.${OS}.${ARCH} bin/lorry - -.PHONY: lorry-fast -lorry-fast: OS=$(shell $(GO) env GOOS) -lorry-fast: ARCH=$(shell $(GO) env GOARCH) -lorry-fast: - $(MAKE) bin/lorry.${OS}.${ARCH} - mv bin/lorry.${OS}.${ARCH} bin/lorry - -.PHONY: clean-lorry -clean-lorry: ## Clean bin/lorry. - rm -f bin/lorry + rm -f bin/cue-helper \ No newline at end of file diff --git a/cmd/lorry/ctl/main.go b/cmd/lorry/ctl/main.go deleted file mode 100644 index 798ad51dd45..00000000000 --- a/cmd/lorry/ctl/main.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package main - -import ( - "github.com/apecloud/kubeblocks/pkg/lorry/ctl" -) - -func main() { - ctl.Execute("", "") -} diff --git a/cmd/lorry/main.go b/cmd/lorry/main.go deleted file mode 100644 index ee972dac0cc..00000000000 --- a/cmd/lorry/main.go +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package main - -import ( - "flag" - "fmt" - "os" - "os/signal" - "strings" - "syscall" - - "github.com/pkg/errors" - "github.com/spf13/pflag" - "go.uber.org/automaxprocs/maxprocs" - "go.uber.org/zap" - "k8s.io/klog/v2" - ctrl "sigs.k8s.io/controller-runtime" - kzap "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/cronjobs" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/grpcserver" - "github.com/apecloud/kubeblocks/pkg/lorry/highavailability" - "github.com/apecloud/kubeblocks/pkg/lorry/httpserver" - opsregister "github.com/apecloud/kubeblocks/pkg/lorry/operations/register" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var configDir string -var disableDNSChecker bool - -func init() { - viper.AutomaticEnv() - pflag.StringVar(&configDir, "config-path", "/config/lorry/components/", "Lorry default config directory for builtin type") - pflag.BoolVar(&disableDNSChecker, "disable-dns-checker", false, "disable dns checker, for test&dev") -} - -func main() { - // Set GOMAXPROCS - _, _ = maxprocs.Set() - - // Initialize flags - opts := kzap.Options{ - Development: true, - } - opts.BindFlags(flag.CommandLine) - klog.InitFlags(nil) - pflag.CommandLine.AddGoFlagSet(flag.CommandLine) - pflag.Parse() - err := viper.BindPFlags(pflag.CommandLine) - if err != nil { - panic(errors.Wrap(err, "fatal error viper bindPFlags")) - } - - // Initialize logger - kopts := []kzap.Opts{kzap.UseFlagOptions(&opts)} - if strings.EqualFold("debug", viper.GetString("zap-log-level")) { - kopts = append(kopts, kzap.RawZapOpts(zap.AddCaller())) - } - ctrl.SetLogger(kzap.New(kopts...)) - - // Initialize DCS (Distributed Control System) - err = dcs.InitStore() - if err != nil { - panic(errors.Wrap(err, "DCS initialize failed")) - } - - // Initialize DB Manager - err = register.InitDBManager(configDir) - if err != nil { - panic(errors.Wrap(err, "DB manager initialize failed")) - } - - // Start HA - characterType := viper.GetString(constant.KBEnvCharacterType) - if viper.IsSet(constant.KBEnvBuiltinHandler) { - characterType = viper.GetString(constant.KBEnvBuiltinHandler) - } - workloadType := viper.GetString(constant.KBEnvWorkloadType) - if highavailability.IsHAAvailable(characterType, workloadType) { - ha := highavailability.NewHa(disableDNSChecker) - if ha != nil { - defer ha.ShutdownWithWait() - go ha.Start() - } - } - - // start grpc server for role probe - grpcServer, err := grpcserver.NewGRPCServer() - if err != nil { - panic(fmt.Errorf("fatal error grpcserver create failed: %v", err)) - } - err = grpcServer.StartNonBlocking() - if err != nil { - panic(fmt.Errorf("fatal error grpcserver serve failed: %v", err)) - } - - // start HTTP Server - ops := opsregister.Operations() - httpServer := httpserver.NewServer(ops) - err = httpServer.StartNonBlocking() - if err != nil { - panic(errors.Wrap(err, "HTTP server initialize failed")) - } - - // start cron jobs - jobManager, err := cronjobs.NewManager() - if err != nil { - panic(errors.Wrap(err, "Cron jobs initialize failed")) - } - jobManager.Start() - - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGTERM, os.Interrupt) - <-stop -} diff --git a/cmd/lorry/vault/plugin.go b/cmd/lorry/vault/plugin.go deleted file mode 100644 index 8aa36f8cd20..00000000000 --- a/cmd/lorry/vault/plugin.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package main - -import ( - "os" - - hclog "github.com/hashicorp/go-hclog" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - - "github.com/apecloud/kubeblocks/pkg/lorry/vault" -) - -func main() { - err := Run() - if err != nil { - logger := hclog.New(&hclog.LoggerOptions{}) - - logger.Error("plugin shutting down", "error", err) - os.Exit(1) - } -} - -// Run instantiates a LorryDB object, and runs the RPC server for the plugin -func Run() error { - db, err := vault.New() - if err != nil { - return err - } - - dbplugin.Serve(db.(dbplugin.Database)) - - return nil -} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index b4a7d571385..5a43ff6169d 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -128,9 +128,6 @@ func init() { viper.SetDefault(constant.EnableRBACManager, true) viper.SetDefault("VOLUMESNAPSHOT_API_BETA", false) viper.SetDefault(constant.KBToolsImage, "apecloud/kubeblocks-tools:latest") - viper.SetDefault(constant.KBEnvLorryHTTPPort, 3501) - viper.SetDefault(constant.KBEnvLorryGRPCPort, 50001) - viper.SetDefault(constant.KBEnvLorryLogLevel, "info") viper.SetDefault("KUBEBLOCKS_SERVICEACCOUNT_NAME", "kubeblocks") viper.SetDefault(constant.ConfigManagerGPRCPortEnv, 9901) viper.SetDefault("CONFIG_MANAGER_LOG_LEVEL", "info") diff --git a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml index c76723b576a..c26085dcdc2 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml @@ -422,331 +422,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - The conditions are as follows: + This field cannot be updated. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + The conditions are as follows: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - If the Action does not complete within this time frame, it will be terminated. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - format: int32 + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 type: integer type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer type: object dataDump: description: |- @@ -770,331 +708,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + Defines the command to run. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. - Deprecation Notice: + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + This field cannot be updated. + type: string + matchingKey: description: |- - Defines the command to run. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This field cannot be updated. - This field cannot be updated. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + The conditions are as follows: - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - The conditions are as follows: + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - type: string - retryPolicy: + This field cannot be updated. + properties: + maxRetries: + default: 0 description: |- - Defines the strategy to be taken when retrying the Action after a failure. - - - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. - - - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object dataLoad: description: |- @@ -1117,331 +993,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: - - - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. - - + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. + + This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 + + + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 type: integer type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer type: object memberJoin: description: "Defines the procedure to add a new replica to the @@ -1463,691 +1277,874 @@ spec: SYSTEM ADD SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: This field is immutable once it has been set." properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + This field cannot be updated. - This field cannot be updated. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: "Defines the procedure to remove a replica from the + replication group.\n\n\nThis action is initiated before remove + a replica from the group.\nThe operator will wait for MemberLeave + to complete successfully before releasing the replica and cleaning + up\nrelated Kubernetes resources.\n\n\nThe process typically + includes updating configurations and informing other group members + about the removal.\nData migration is generally not part of + this action and should be handled separately if needed.\n\n\nThe + container executing this action has access to following variables:\n\n\n- + KB_LEAVE_MEMBER_POD_FQDN: The pod name of the replica being + removed from the group.\n- KB_LEAVE_MEMBER_POD_NAME: The pod + name of the replica being removed from the group.\n\n\nExpected + action output:\n- On Failure: An error message, if applicable, + indicating why the action failed.\n\n\nFor example, to remove + an OBServer from an OceanBase Cluster in 'zone1', the following + command can be executed:\n\n\n```yaml\ncommand:\n- bash\n- -c\n- + |\n CLIENT=\"mysql -u $SERVICE_USER -p$SERVICE_PASSWORD -P + $SERVICE_PORT -h $SERVICE_HOST -e\"\n\t $CLIENT \"ALTER SYSTEM + DELETE SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: + This field is immutable once it has been set." + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. + + + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - memberLeave: - description: "Defines the procedure to remove a replica from the - replication group.\n\n\nThis action is initiated before remove - a replica from the group.\nThe operator will wait for MemberLeave - to complete successfully before releasing the replica and cleaning - up\nrelated Kubernetes resources.\n\n\nThe process typically - includes updating configurations and informing other group members - about the removal.\nData migration is generally not part of - this action and should be handled separately if needed.\n\n\nThe - container executing this action has access to following variables:\n\n\n- - KB_LEAVE_MEMBER_POD_FQDN: The pod name of the replica being - removed from the group.\n- KB_LEAVE_MEMBER_POD_NAME: The pod - name of the replica being removed from the group.\n\n\nExpected - action output:\n- On Failure: An error message, if applicable, - indicating why the action failed.\n\n\nFor example, to remove - an OBServer from an OceanBase Cluster in 'zone1', the following - command can be executed:\n\n\n```yaml\ncommand:\n- bash\n- -c\n- - |\n CLIENT=\"mysql -u $SERVICE_USER -p$SERVICE_PASSWORD -P - $SERVICE_PORT -h $SERVICE_HOST -e\"\n\t $CLIENT \"ALTER SYSTEM - DELETE SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: - This field is immutable once it has been set." - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + postProvision: + description: |- + Specifies the hook to be executed after a component's creation. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + By setting `postProvision.customHandler.preCondition`, you can determine the specific lifecycle stage + at which the action should trigger: `Immediately`, `RuntimeReady`, `ComponentReady`, and `ClusterReady`. + with `ComponentReady` being the default. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + The PostProvision Action is intended to run only once. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + The container executing this action has access to following environment variables: - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + - KB_CLUSTER_POD_IP_LIST: Comma-separated list of the cluster's pod IP addresses (e.g., "podIp1,podIp2"). + - KB_CLUSTER_POD_NAME_LIST: Comma-separated list of the cluster's pod names (e.g., "pod1,pod2"). + - KB_CLUSTER_POD_HOST_NAME_LIST: Comma-separated list of host names, each corresponding to a pod in + KB_CLUSTER_POD_NAME_LIST (e.g., "hostName1,hostName2"). + - KB_CLUSTER_POD_HOST_IP_LIST: Comma-separated list of host IP addresses, each corresponding to a pod in + KB_CLUSTER_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). - Deprecation Notice: + - KB_CLUSTER_COMPONENT_POD_NAME_LIST: Comma-separated list of all pod names within the component + (e.g., "pod1,pod2"). + - KB_CLUSTER_COMPONENT_POD_IP_LIST: Comma-separated list of pod IP addresses, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "podIp1,podIp2"). + - KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST: Comma-separated list of host names for each pod, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostName1,hostName2"). + - KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST: Comma-separated list of host IP addresses for each pod, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + - KB_CLUSTER_COMPONENT_LIST: Comma-separated list of all cluster components (e.g., "comp1,comp2"). + - KB_CLUSTER_COMPONENT_DELETING_LIST: Comma-separated list of components that are currently being deleted + (e.g., "comp1,comp2"). + - KB_CLUSTER_COMPONENT_UNDELETED_LIST: Comma-separated list of components that are not being deleted + (e.g., "comp1,comp2"). - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Note: This field is immutable once it has been set. + properties: + exec: + description: |- + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - postProvision: + preTerminate: description: |- - Specifies the hook to be executed after a component's creation. + Specifies the hook to be executed prior to terminating a component. - By setting `postProvision.customHandler.preCondition`, you can determine the specific lifecycle stage - at which the action should trigger: `Immediately`, `RuntimeReady`, `ComponentReady`, and `ClusterReady`. - with `ComponentReady` being the default. + The PreTerminate Action is intended to run only once. - The PostProvision Action is intended to run only once. + This action is executed immediately when a scale-down operation for the Component is initiated. + The actual termination and cleanup of the Component and its associated resources will not proceed + until the PreTerminate action has completed successfully. The container executing this action has access to following environment variables: @@ -2178,717 +2175,573 @@ spec: (e.g., "comp1,comp2"). - Note: This field is immutable once it has been set. - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. - - - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. - - - Deprecation Notice: + - KB_CLUSTER_COMPONENT_IS_SCALING_IN: Indicates whether the component is currently scaling in. + If this variable is present and set to "true", it denotes that the component is undergoing a scale-in operation. + During scale-in, data rebalancing is necessary to maintain cluster integrity. + Contrast this with a cluster deletion scenario where data rebalancing is not required as the entire cluster + is being cleaned up. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + Note: This field is immutable once it has been set. + properties: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - This field cannot be updated. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - The conditions are as follows: + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 - type: integer - type: object - type: object - preTerminate: - description: |- - Specifies the hook to be executed prior to terminating a component. - - - The PreTerminate Action is intended to run only once. - This action is executed immediately when a scale-down operation for the Component is initiated. - The actual termination and cleanup of the Component and its associated resources will not proceed - until the PreTerminate action has completed successfully. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The container executing this action has access to following environment variables: + The conditions are as follows: - - KB_CLUSTER_POD_IP_LIST: Comma-separated list of the cluster's pod IP addresses (e.g., "podIp1,podIp2"). - - KB_CLUSTER_POD_NAME_LIST: Comma-separated list of the cluster's pod names (e.g., "pod1,pod2"). - - KB_CLUSTER_POD_HOST_NAME_LIST: Comma-separated list of host names, each corresponding to a pod in - KB_CLUSTER_POD_NAME_LIST (e.g., "hostName1,hostName2"). - - KB_CLUSTER_POD_HOST_IP_LIST: Comma-separated list of host IP addresses, each corresponding to a pod in - KB_CLUSTER_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - - KB_CLUSTER_COMPONENT_POD_NAME_LIST: Comma-separated list of all pod names within the component - (e.g., "pod1,pod2"). - - KB_CLUSTER_COMPONENT_POD_IP_LIST: Comma-separated list of pod IP addresses, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "podIp1,podIp2"). - - KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST: Comma-separated list of host names for each pod, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostName1,hostName2"). - - KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST: Comma-separated list of host IP addresses for each pod, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - - KB_CLUSTER_COMPONENT_LIST: Comma-separated list of all cluster components (e.g., "comp1,comp2"). - - KB_CLUSTER_COMPONENT_DELETING_LIST: Comma-separated list of components that are currently being deleted - (e.g., "comp1,comp2"). - - KB_CLUSTER_COMPONENT_UNDELETED_LIST: Comma-separated list of components that are not being deleted - (e.g., "comp1,comp2"). + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - - KB_CLUSTER_COMPONENT_IS_SCALING_IN: Indicates whether the component is currently scaling in. - If this variable is present and set to "true", it denotes that the component is undergoing a scale-in operation. - During scale-in, data rebalancing is necessary to maintain cluster integrity. - Contrast this with a cluster deletion scenario where data rebalancing is not required as the entire cluster - is being cleaned up. + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - Note: This field is immutable once it has been set. - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + If the Action does not complete within this time frame, it will be terminated. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + This field cannot be updated. + format: int32 + type: integer + type: object + readonly: + description: |- + Defines the procedure to switch a replica into the read-only state. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + Use Case: + This action is invoked when the database's volume capacity nears its upper limit and space is about to be exhausted. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + The container executing this action has access to following environment variables: - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + - KB_POD_FQDN: The FQDN of the replica pod whose role is being checked. - Deprecation Notice: + Expected action output: + - On Failure: An error message, if applicable, indicating why the action failed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + Note: This field is immutable once it has been set. + properties: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - readonly: + readwrite: description: |- - Defines the procedure to switch a replica into the read-only state. + Defines the procedure to transition a replica from the read-only state back to the read-write state. Use Case: - This action is invoked when the database's volume capacity nears its upper limit and space is about to be exhausted. + This action is used to bring back a replica that was previously in a read-only state, + which restricted write operations, to its normal operational state where it can handle + both read and write operations. The container executing this action has access to following environment variables: @@ -2903,680 +2756,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + required: + - name + type: object + type: array + image: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: + Specifies the container image to be used for running the Action. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 - type: integer - type: object - type: object - readwrite: - description: |- - Defines the procedure to transition a replica from the read-only state back to the read-write state. - - - Use Case: - This action is used to bring back a replica that was previously in a read-only state, - which restricted write operations, to its normal operational state where it can handle - both read and write operations. - - - The container executing this action has access to following environment variables: - - - - KB_POD_FQDN: The FQDN of the replica pod whose role is being checked. - Expected action output: - - On Failure: An error message, if applicable, indicating why the action failed. - - - Note: This field is immutable once it has been set. - properties: - builtinHandler: + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + The conditions are as follows: - Deprecation Notice: + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. + This field cannot be updated. type: string - customHandler: + retryPolicy: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + Defines the strategy to be taken when retrying the Action after a failure. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: - description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: - - - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. - - - This field cannot be updated. - type: string - retryPolicy: + maxRetries: + default: 0 description: |- - Defines the strategy to be taken when retrying the Action after a failure. - - - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. - - - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object reconfigure: description: |- @@ -3588,338 +3030,276 @@ spec: This Action is reserved for future versions. properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. - - - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. - - - Deprecation Notice: - - - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object roleProbe: description: |- Defines the procedure which is invoked regularly to assess the role of replicas. - This action is periodically triggered by Lorry at the specified interval to determine the role of each replica. + This action is periodically triggered at the specified interval to determine the role of each replica. Upon successful execution, the action's output designates the role of the replica, which should match one of the predefined role names within `componentDefinition.spec.roles`. The output is then compared with the previous successful execution result. @@ -3946,9 +3326,6 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: - description: 'TODO: remove this later.' - type: string exec: description: |- Defines the command to run. diff --git a/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml b/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml index 4107840c448..8f81c4fca90 100644 --- a/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml +++ b/config/crd/bases/workloads.kubeblocks.io_instancesets.yaml @@ -4178,16 +4178,9 @@ spec: roleProbe: description: Provides method to probe role. properties: - builtinHandlerName: - description: |- - Specifies the builtin handler name to use to probe the role of the main container. - Available handlers include: mysql, postgres, mongodb, redis, etcd, kafka. - Use CustomHandler to define a custom role probe function if none of the built-in handlers meet the requirement. - type: string customHandler: description: |- Defines a custom method for role probing. - If the BuiltinHandler meets the requirement, use it instead. Actions defined here are executed in series. Upon completion of all actions, the final output should be a single string representing the role name defined in spec.Roles. The latest [BusyBox](https://busybox.net/) image will be used if Image is not configured. diff --git a/controllers/apps/component_controller_test.go b/controllers/apps/component_controller_test.go index 31c74a07a92..03015985cca 100644 --- a/controllers/apps/component_controller_test.go +++ b/controllers/apps/component_controller_test.go @@ -1583,6 +1583,8 @@ var _ = Describe("Component Controller", func() { compDef.Spec.Volumes[i].HighWatermark = 85 } } + compDef.Spec.LifecycleActions.Readonly = testapps.NewLifecycleAction("readonly") + compDef.Spec.LifecycleActions.Readwrite = testapps.NewLifecycleAction("readwrite") })()).Should(Succeed()) By("creating a component with target service account name") diff --git a/controllers/apps/componentdefinition_controller.go b/controllers/apps/componentdefinition_controller.go index 0f68eac2033..88b1f9caaac 100644 --- a/controllers/apps/componentdefinition_controller.go +++ b/controllers/apps/componentdefinition_controller.go @@ -345,55 +345,6 @@ func (r *ComponentDefinitionReconciler) validateReplicaRoles(cli client.Client, } func (r *ComponentDefinitionReconciler) validateLifecycleActions(cli client.Client, reqCtx intctrlutil.RequestCtx, cmpd *appsv1alpha1.ComponentDefinition) error { - if err := r.validateLifecycleActionBuiltInHandlers(cmpd.Spec.LifecycleActions); err != nil { - return err - } - return nil -} - -func (r *ComponentDefinitionReconciler) validateLifecycleActionBuiltInHandlers(lifecycleActions *appsv1alpha1.ComponentLifecycleActions) error { - if lifecycleActions == nil { - return nil - } - - builtInHandlerMap := make(map[appsv1alpha1.BuiltinActionHandlerType]bool) - supportedBuiltInHandlers := getBuiltinActionHandlers() - - if lifecycleActions.RoleProbe != nil && lifecycleActions.RoleProbe.BuiltinHandler != nil { - if !slices.Contains(supportedBuiltInHandlers, *lifecycleActions.RoleProbe.BuiltinHandler) { - return fmt.Errorf("the builtin handler %s is not supported", *lifecycleActions.RoleProbe.BuiltinHandler) - } - builtInHandlerMap[*lifecycleActions.RoleProbe.BuiltinHandler] = true - } - - actions := []struct { - LifeCycleActionHandlers *appsv1alpha1.LifecycleActionHandler - }{ - {lifecycleActions.PostProvision}, - {lifecycleActions.PreTerminate}, - {lifecycleActions.MemberJoin}, - {lifecycleActions.MemberLeave}, - {lifecycleActions.Readonly}, - {lifecycleActions.Readwrite}, - {lifecycleActions.DataDump}, - {lifecycleActions.DataLoad}, - {lifecycleActions.Reconfigure}, - {lifecycleActions.AccountProvision}, - } - - for _, action := range actions { - if action.LifeCycleActionHandlers != nil && action.LifeCycleActionHandlers.BuiltinHandler != nil { - if !slices.Contains(supportedBuiltInHandlers, *lifecycleActions.RoleProbe.BuiltinHandler) { - return fmt.Errorf("the builtin handler %s is not supported", *lifecycleActions.RoleProbe.BuiltinHandler) - } - builtInHandlerMap[*lifecycleActions.RoleProbe.BuiltinHandler] = true - } - } - - if len(builtInHandlerMap) > 1 { - return fmt.Errorf("the builtin handler within the same lifecycle actions should be consistent") - } - return nil } @@ -515,19 +466,3 @@ func checkUniqueItemWithValue(slice any, fieldName string, val any) bool { } return true } - -func getBuiltinActionHandlers() []appsv1alpha1.BuiltinActionHandlerType { - return []appsv1alpha1.BuiltinActionHandlerType{ - appsv1alpha1.MySQLBuiltinActionHandler, - appsv1alpha1.WeSQLBuiltinActionHandler, - appsv1alpha1.OceanbaseBuiltinActionHandler, - appsv1alpha1.RedisBuiltinActionHandler, - appsv1alpha1.MongoDBBuiltinActionHandler, - appsv1alpha1.ETCDBuiltinActionHandler, - appsv1alpha1.PostgresqlBuiltinActionHandler, - appsv1alpha1.OfficialPostgresqlBuiltinActionHandler, - appsv1alpha1.ApeCloudPostgresqlBuiltinActionHandler, - appsv1alpha1.PolarDBXBuiltinActionHandler, - appsv1alpha1.CustomActionHandler, - } -} diff --git a/controllers/apps/componentdefinition_controller_test.go b/controllers/apps/componentdefinition_controller_test.go index ce4df5a80e4..97916a3000b 100644 --- a/controllers/apps/componentdefinition_controller_test.go +++ b/controllers/apps/componentdefinition_controller_test.go @@ -45,7 +45,7 @@ var _ = Describe("ComponentDefinition Controller", func() { ) var ( - defaultActionHandler = &appsv1alpha1.LifecycleActionHandler{} + defaultActionHandler = &appsv1alpha1.Action{} ) cleanEnv := func() { diff --git a/controllers/apps/operations/datascript.go b/controllers/apps/operations/datascript.go index cf84ba30847..051bbf480cf 100644 --- a/controllers/apps/operations/datascript.go +++ b/controllers/apps/operations/datascript.go @@ -21,25 +21,18 @@ package operations import ( "fmt" - "strings" "time" - "github.com/sethvargo/go-password/password" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/controller/scheduling" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - viper "github.com/apecloud/kubeblocks/pkg/viperx" ) var _ OpsHandler = DataScriptOpsHandler{} @@ -219,177 +212,177 @@ func getScriptContent(reqCtx intctrlutil.RequestCtx, cli client.Client, spec *ap return script, nil } -func getTargetService(reqCtx intctrlutil.RequestCtx, cli client.Client, clusterObjectKey client.ObjectKey, componentName string) (string, error) { - // get svc - service := &corev1.Service{} - serviceName := fmt.Sprintf("%s-%s", clusterObjectKey.Name, componentName) - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: clusterObjectKey.Namespace, Name: serviceName}, service); err != nil { - return "", err - } - return serviceName, nil -} - -func buildDataScriptJobs(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster *appsv1alpha1.Cluster, component *appsv1alpha1.ClusterComponentSpec, - ops *appsv1alpha1.OpsRequest, charType string) ([]*batchv1.Job, error) { - engineForJob, err := register.NewClusterCommands(charType) - if err != nil || engineForJob == nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - - buildJob := func(endpoint string) (*batchv1.Job, error) { - envs := []corev1.EnvVar{} - - envs = append(envs, corev1.EnvVar{ - Name: "KB_HOST", - Value: endpoint, - }) - - // parse username and password - secretFrom := ops.Spec.ScriptSpec.Secret - if secretFrom == nil { - return nil, intctrlutil.NewFatalError("missing secret for user & password") - } - - // verify secrets exist - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: reqCtx.Req.Namespace, Name: secretFrom.Name}, &corev1.Secret{}); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - - envs = append(envs, corev1.EnvVar{ - Name: "KB_USER", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - Key: secretFrom.UsernameKey, - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretFrom.Name, - }, - }, - }, - }) - envs = append(envs, corev1.EnvVar{ - Name: "KB_PASSWD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - Key: secretFrom.PasswordKey, - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretFrom.Name, - }, - }, - }, - }) - - // parse scripts - scripts, err := getScriptContent(reqCtx, cli, ops.Spec.ScriptSpec) - if err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - - envs = append(envs, corev1.EnvVar{ - Name: "KB_SCRIPT", - Value: strings.Join(scripts, "\n"), - }) - - jobCmdTpl, envVars, err := engineForJob.ExecuteCommand(scripts) - if err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - if envVars != nil { - envs = append(envs, envVars...) - } - containerImg := viper.GetString(constant.KBDataScriptClientsImage) - if len(ops.Spec.ScriptSpec.Image) != 0 { - containerImg = ops.Spec.ScriptSpec.Image - } - if len(containerImg) == 0 { - return nil, intctrlutil.NewFatalError("image is empty") - } - - container := corev1.Container{ - Name: "datascript", - Image: containerImg, - ImagePullPolicy: corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)), - Command: jobCmdTpl, - Env: envs, - } - randomStr, _ := password.Generate(4, 0, 0, true, false) - jobName := fmt.Sprintf("%s-%s-%s-%s", cluster.Name, "script", ops.Name, randomStr) - if len(jobName) > 63 { - jobName = strings.TrimSuffix(jobName[:63], "-") - } - - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: jobName, - Namespace: cluster.Namespace, - }, - } - intctrlutil.InjectZeroResourcesLimitsIfEmpty(&container) - // set backoff limit to 0, so that the job will not be restarted - job.Spec.BackoffLimit = pointer.Int32(0) - job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever - job.Spec.Template.Spec.Containers = []corev1.Container{container} - job.Spec.Template.Spec.ImagePullSecrets = intctrlutil.BuildImagePullSecrets() - - // add labels - job.Labels = getDataScriptJobLabels(cluster.Name, component.Name, ops.Name) - // add tolerations - schedulingPolicy, err := scheduling.BuildSchedulingPolicy(cluster, component) - if err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - job.Spec.Template.Spec.Tolerations = schedulingPolicy.Tolerations - // add owner reference - scheme, _ := appsv1alpha1.SchemeBuilder.Build() - if err := controllerutil.SetOwnerReference(ops, job, scheme); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - return job, nil - } - - // parse kb host - var endpoint string - var job *batchv1.Job - - jobs := make([]*batchv1.Job, 0) - if ops.Spec.ScriptSpec.Selector == nil { - if endpoint, err = getTargetService(reqCtx, cli, client.ObjectKeyFromObject(cluster), component.Name); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - if job, err = buildJob(endpoint); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - jobs = append(jobs, job) - return jobs, nil - } - - selector, err := metav1.LabelSelectorAsSelector(ops.Spec.ScriptSpec.Selector) - if err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } - - pods := &corev1.PodList{} - if err = cli.List(reqCtx.Ctx, pods, client.InNamespace(cluster.Namespace), - client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.Name, - constant.KBAppComponentLabelKey: component.Name, - }, - client.MatchingLabelsSelector{Selector: selector}, - ); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } else if len(pods.Items) == 0 { - return nil, intctrlutil.NewFatalError(err.Error()) - } - - for _, pod := range pods.Items { - endpoint = pod.Status.PodIP - if job, err = buildJob(endpoint); err != nil { - return nil, intctrlutil.NewFatalError(err.Error()) - } else { - jobs = append(jobs, job) - } - } - return jobs, nil -} +// func getTargetService(reqCtx intctrlutil.RequestCtx, cli client.Client, clusterObjectKey client.ObjectKey, componentName string) (string, error) { +// // get svc +// service := &corev1.Service{} +// serviceName := fmt.Sprintf("%s-%s", clusterObjectKey.Name, componentName) +// if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: clusterObjectKey.Namespace, Name: serviceName}, service); err != nil { +// return "", err +// } +// return serviceName, nil +// } + +// func buildDataScriptJobs(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster *appsv1alpha1.Cluster, component *appsv1alpha1.ClusterComponentSpec, +// ops *appsv1alpha1.OpsRequest, charType string) ([]*batchv1.Job, error) { +// engineForJob, err := register.NewClusterCommands(charType) +// if err != nil || engineForJob == nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// +// buildJob := func(endpoint string) (*batchv1.Job, error) { +// envs := []corev1.EnvVar{} +// +// envs = append(envs, corev1.EnvVar{ +// Name: "KB_HOST", +// Value: endpoint, +// }) +// +// // parse username and password +// secretFrom := ops.Spec.ScriptSpec.Secret +// if secretFrom == nil { +// return nil, intctrlutil.NewFatalError("missing secret for user & password") +// } +// +// // verify secrets exist +// if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: reqCtx.Req.Namespace, Name: secretFrom.Name}, &corev1.Secret{}); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// +// envs = append(envs, corev1.EnvVar{ +// Name: "KB_USER", +// ValueFrom: &corev1.EnvVarSource{ +// SecretKeyRef: &corev1.SecretKeySelector{ +// Key: secretFrom.UsernameKey, +// LocalObjectReference: corev1.LocalObjectReference{ +// Name: secretFrom.Name, +// }, +// }, +// }, +// }) +// envs = append(envs, corev1.EnvVar{ +// Name: "KB_PASSWD", +// ValueFrom: &corev1.EnvVarSource{ +// SecretKeyRef: &corev1.SecretKeySelector{ +// Key: secretFrom.PasswordKey, +// LocalObjectReference: corev1.LocalObjectReference{ +// Name: secretFrom.Name, +// }, +// }, +// }, +// }) +// +// // parse scripts +// scripts, err := getScriptContent(reqCtx, cli, ops.Spec.ScriptSpec) +// if err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// +// envs = append(envs, corev1.EnvVar{ +// Name: "KB_SCRIPT", +// Value: strings.Join(scripts, "\n"), +// }) +// +// jobCmdTpl, envVars, err := engineForJob.ExecuteCommand(scripts) +// if err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// if envVars != nil { +// envs = append(envs, envVars...) +// } +// containerImg := viper.GetString(constant.KBDataScriptClientsImage) +// if len(ops.Spec.ScriptSpec.Image) != 0 { +// containerImg = ops.Spec.ScriptSpec.Image +// } +// if len(containerImg) == 0 { +// return nil, intctrlutil.NewFatalError("image is empty") +// } +// +// container := corev1.Container{ +// Name: "datascript", +// Image: containerImg, +// ImagePullPolicy: corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)), +// Command: jobCmdTpl, +// Env: envs, +// } +// randomStr, _ := password.Generate(4, 0, 0, true, false) +// jobName := fmt.Sprintf("%s-%s-%s-%s", cluster.Name, "script", ops.Name, randomStr) +// if len(jobName) > 63 { +// jobName = strings.TrimSuffix(jobName[:63], "-") +// } +// +// job := &batchv1.Job{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: jobName, +// Namespace: cluster.Namespace, +// }, +// } +// intctrlutil.InjectZeroResourcesLimitsIfEmpty(&container) +// // set backoff limit to 0, so that the job will not be restarted +// job.Spec.BackoffLimit = pointer.Int32(0) +// job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever +// job.Spec.Template.Spec.Containers = []corev1.Container{container} +// job.Spec.Template.Spec.ImagePullSecrets = intctrlutil.BuildImagePullSecrets() +// +// // add labels +// job.Labels = getDataScriptJobLabels(cluster.Name, component.Name, ops.Name) +// // add tolerations +// schedulingPolicy, err := scheduling.BuildSchedulingPolicy(cluster, component) +// if err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// job.Spec.Template.Spec.Tolerations = schedulingPolicy.Tolerations +// // add owner reference +// scheme, _ := appsv1alpha1.SchemeBuilder.Build() +// if err := controllerutil.SetOwnerReference(ops, job, scheme); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// return job, nil +// } +// +// // parse kb host +// var endpoint string +// var job *batchv1.Job +// +// jobs := make([]*batchv1.Job, 0) +// if ops.Spec.ScriptSpec.Selector == nil { +// if endpoint, err = getTargetService(reqCtx, cli, client.ObjectKeyFromObject(cluster), component.Name); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// if job, err = buildJob(endpoint); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// jobs = append(jobs, job) +// return jobs, nil +// } +// +// selector, err := metav1.LabelSelectorAsSelector(ops.Spec.ScriptSpec.Selector) +// if err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// +// pods := &corev1.PodList{} +// if err = cli.List(reqCtx.Ctx, pods, client.InNamespace(cluster.Namespace), +// client.MatchingLabels{ +// constant.AppInstanceLabelKey: cluster.Name, +// constant.KBAppComponentLabelKey: component.Name, +// }, +// client.MatchingLabelsSelector{Selector: selector}, +// ); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } else if len(pods.Items) == 0 { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } +// +// for _, pod := range pods.Items { +// endpoint = pod.Status.PodIP +// if job, err = buildJob(endpoint); err != nil { +// return nil, intctrlutil.NewFatalError(err.Error()) +// } else { +// jobs = append(jobs, job) +// } +// } +// return jobs, nil +// } func getDataScriptJobLabels(cluster, component, request string) map[string]string { return map[string]string{ diff --git a/controllers/apps/operations/datascript_test.go b/controllers/apps/operations/datascript_test.go index 900943c291b..1218c56ae3b 100644 --- a/controllers/apps/operations/datascript_test.go +++ b/controllers/apps/operations/datascript_test.go @@ -20,12 +20,9 @@ along with this program. If not, see . package operations import ( - "fmt" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,11 +30,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" "github.com/apecloud/kubeblocks/pkg/generics" testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" - viper "github.com/apecloud/kubeblocks/pkg/viperx" ) var _ = Describe("DataScriptOps", func() { @@ -196,194 +191,196 @@ var _ = Describe("DataScriptOps", func() { Expect(ops.Status.Phase).Should(Equal(prevOpsStatus)) }) - // TODO(v1.0): depends on clusterDefinition exist? - PIt("create a datascript ops on running cluster", func() { - By("patch cluster to running") - patchClusterStatus(appsv1alpha1.RunningClusterPhase) - - By("create a datascript ops with ttlSecondsBeforeAbort=0") - ops := createClusterDatascriptOps(defaultCompName, 0) - opsResource.OpsRequest = ops - opsKey := client.ObjectKeyFromObject(ops) - patchOpsPhase(opsKey, appsv1alpha1.OpsCreatingPhase) - Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) - opsResource.OpsRequest = ops - - reqCtx.Req = reconcile.Request{NamespacedName: opsKey} - By("check the opsRequest phase, should fail, cause pod is missing") - _, err := GetOpsManager().Do(reqCtx, k8sClient, opsResource) - Expect(err).ShouldNot(HaveOccurred()) - Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) - }) - - It("reconcile a datascript ops on running cluster, patch job to complete", func() { - By("patch cluster to running") - patchClusterStatus(appsv1alpha1.RunningClusterPhase) - - By("create a datascript ops with ttlSecondsBeforeAbort=0") - ops := createClusterDatascriptOps(defaultCompName, 0) - opsResource.OpsRequest = ops - opsKey := client.ObjectKeyFromObject(ops) - patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) - Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) - opsResource.OpsRequest = ops - - reqCtx.Req = reconcile.Request{NamespacedName: opsKey} - By("mock a job, missing service, should fail") - comp := clusterObj.Spec.GetComponentByName(defaultCompName) - _, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") - Expect(err).Should(HaveOccurred()) - - By("mock a service, should pass") - serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, - Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, - } - err = k8sClient.Create(testCtx.Ctx, service) - Expect(err).Should(Succeed()) - - By("mock a job one more time, fail with missing secret") - _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("missing secret")) - - By("patch a secret name to ops, fail with missing secret") - secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) - patch := client.MergeFrom(ops.DeepCopy()) - ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ - Name: secretName, - PasswordKey: "password", - UsernameKey: "username", - } - Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) - - _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(secretName)) - - By("mock a secret, should pass") - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "password": []byte("123456"), - "username": []byte("hellocoffee"), - }, - } - err = k8sClient.Create(testCtx.Ctx, secret) - Expect(err).Should(Succeed()) - - By("create job, should pass") - viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") - jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") - Expect(err).Should(Succeed()) - job := jobs[0] - Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) - - By("reconcile the opsRequest phase") - _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) - Expect(err).Should(Succeed()) - Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) - - By("patch job to succeed") - Eventually(func(g Gomega) { - g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { - job.Status.Succeeded = 1 - job.Status.Conditions = append(job.Status.Conditions, - batchv1.JobCondition{ - Type: batchv1.JobComplete, - Status: corev1.ConditionTrue, - }) - })) - }).Should(Succeed()) - - _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) - Expect(err).Should(Succeed()) - Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) - - Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) - Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) - Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) - }) - - It("reconcile a datascript ops on running cluster, patch job to failed", func() { - By("patch cluster to running") - patchClusterStatus(appsv1alpha1.RunningClusterPhase) - - By("create a datascript ops with ttlSecondsBeforeAbort=0") - ops := createClusterDatascriptOps(defaultCompName, 0) - opsResource.OpsRequest = ops - opsKey := client.ObjectKeyFromObject(ops) - patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) - Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) - opsResource.OpsRequest = ops - - reqCtx.Req = reconcile.Request{NamespacedName: opsKey} - comp := clusterObj.Spec.GetComponentByName(defaultCompName) - By("mock a service, should pass") - serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, - Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, - } - err := k8sClient.Create(testCtx.Ctx, service) - Expect(err).Should(Succeed()) - - By("patch a secret name to ops") - secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "password": []byte("123456"), - "username": []byte("hellocoffee"), - }, - } - patch := client.MergeFrom(ops.DeepCopy()) - ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ - Name: secretName, - PasswordKey: "password", - UsernameKey: "username", - } - Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) - - By("mock a secret, should pass") - err = k8sClient.Create(testCtx.Ctx, secret) - Expect(err).Should(Succeed()) - - By("create job, should pass") - viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") - jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") - Expect(err).Should(Succeed()) - job := jobs[0] - Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) - - By("reconcile the opsRequest phase") - _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) - Expect(err).Should(Succeed()) - Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) - - By("patch job to failed") - Eventually(func(g Gomega) { - g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { - job.Status.Succeeded = 1 - job.Status.Conditions = append(job.Status.Conditions, - batchv1.JobCondition{ - Type: batchv1.JobFailed, - Status: corev1.ConditionTrue, - }) - })) - }).Should(Succeed()) - - _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) - Expect(err).Should(Succeed()) - Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) - - Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) - Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) - Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) - }) + // TODO(v1.0): depends on clusterDefinition and lorry? + // It("create a datascript ops on running cluster", func() { + // By("patch cluster to running") + // patchClusterStatus(appsv1alpha1.RunningClusterPhase) + // + // By("create a datascript ops with ttlSecondsBeforeAbort=0") + // ops := createClusterDatascriptOps(defaultCompName, 0) + // opsResource.OpsRequest = ops + // opsKey := client.ObjectKeyFromObject(ops) + // patchOpsPhase(opsKey, appsv1alpha1.OpsCreatingPhase) + // Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) + // opsResource.OpsRequest = ops + // + // reqCtx.Req = reconcile.Request{NamespacedName: opsKey} + // By("check the opsRequest phase, should fail, cause pod is missing") + // _, err := GetOpsManager().Do(reqCtx, k8sClient, opsResource) + // Expect(err).ShouldNot(HaveOccurred()) + // Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) + // }) + + // TODO(v1.0): depends on clusterDefinition and lorry? + // It("reconcile a datascript ops on running cluster, patch job to complete", func() { + // By("patch cluster to running") + // patchClusterStatus(appsv1alpha1.RunningClusterPhase) + // + // By("create a datascript ops with ttlSecondsBeforeAbort=0") + // ops := createClusterDatascriptOps(defaultCompName, 0) + // opsResource.OpsRequest = ops + // opsKey := client.ObjectKeyFromObject(ops) + // patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) + // Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) + // opsResource.OpsRequest = ops + // + // reqCtx.Req = reconcile.Request{NamespacedName: opsKey} + // By("mock a job, missing service, should fail") + // comp := clusterObj.Spec.GetComponentByName(defaultCompName) + // _, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") + // Expect(err).Should(HaveOccurred()) + // + // By("mock a service, should pass") + // serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) + // service := &corev1.Service{ + // ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, + // Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, + // } + // err = k8sClient.Create(testCtx.Ctx, service) + // Expect(err).Should(Succeed()) + // + // By("mock a job one more time, fail with missing secret") + // _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") + // Expect(err).Should(HaveOccurred()) + // Expect(err.Error()).Should(ContainSubstring("missing secret")) + // + // By("patch a secret name to ops, fail with missing secret") + // secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) + // patch := client.MergeFrom(ops.DeepCopy()) + // ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ + // Name: secretName, + // PasswordKey: "password", + // UsernameKey: "username", + // } + // Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) + // + // _, err = buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") + // Expect(err).Should(HaveOccurred()) + // Expect(err.Error()).Should(ContainSubstring(secretName)) + // + // By("mock a secret, should pass") + // secret := &corev1.Secret{ + // ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, + // Type: corev1.SecretTypeOpaque, + // Data: map[string][]byte{ + // "password": []byte("123456"), + // "username": []byte("hellocoffee"), + // }, + // } + // err = k8sClient.Create(testCtx.Ctx, secret) + // Expect(err).Should(Succeed()) + // + // By("create job, should pass") + // viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") + // jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") + // Expect(err).Should(Succeed()) + // job := jobs[0] + // Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) + // + // By("reconcile the opsRequest phase") + // _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) + // Expect(err).Should(Succeed()) + // Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) + // + // By("patch job to succeed") + // Eventually(func(g Gomega) { + // g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { + // job.Status.Succeeded = 1 + // job.Status.Conditions = append(job.Status.Conditions, + // batchv1.JobCondition{ + // Type: batchv1.JobComplete, + // Status: corev1.ConditionTrue, + // }) + // })) + // }).Should(Succeed()) + // + // _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) + // Expect(err).Should(Succeed()) + // Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) + // + // Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) + // Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) + // Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) + // }) + + // TODO(v1.0): depends on clusterDefinition and lorry? + // It("reconcile a datascript ops on running cluster, patch job to failed", func() { + // By("patch cluster to running") + // patchClusterStatus(appsv1alpha1.RunningClusterPhase) + // + // By("create a datascript ops with ttlSecondsBeforeAbort=0") + // ops := createClusterDatascriptOps(defaultCompName, 0) + // opsResource.OpsRequest = ops + // opsKey := client.ObjectKeyFromObject(ops) + // patchOpsPhase(opsKey, appsv1alpha1.OpsRunningPhase) + // Expect(k8sClient.Get(testCtx.Ctx, opsKey, ops)).Should(Succeed()) + // opsResource.OpsRequest = ops + // + // reqCtx.Req = reconcile.Request{NamespacedName: opsKey} + // comp := clusterObj.Spec.GetComponentByName(defaultCompName) + // By("mock a service, should pass") + // serviceName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) + // service := &corev1.Service{ + // ObjectMeta: metav1.ObjectMeta{Name: serviceName, Namespace: clusterObj.Namespace}, + // Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Port: 3306}}}, + // } + // err := k8sClient.Create(testCtx.Ctx, service) + // Expect(err).Should(Succeed()) + // + // By("patch a secret name to ops") + // secretName := fmt.Sprintf("%s-%s", clusterObj.Name, comp.Name) + // secret := &corev1.Secret{ + // ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: clusterObj.Namespace}, + // Type: corev1.SecretTypeOpaque, + // Data: map[string][]byte{ + // "password": []byte("123456"), + // "username": []byte("hellocoffee"), + // }, + // } + // patch := client.MergeFrom(ops.DeepCopy()) + // ops.Spec.ScriptSpec.Secret = &appsv1alpha1.ScriptSecret{ + // Name: secretName, + // PasswordKey: "password", + // UsernameKey: "username", + // } + // Expect(k8sClient.Patch(testCtx.Ctx, ops, patch)).Should(Succeed()) + // + // By("mock a secret, should pass") + // err = k8sClient.Create(testCtx.Ctx, secret) + // Expect(err).Should(Succeed()) + // + // By("create job, should pass") + // viper.Set(constant.KBDataScriptClientsImage, "apecloud/kubeblocks-clients:latest") + // jobs, err := buildDataScriptJobs(reqCtx, k8sClient, clusterObj, comp, ops, "mysql") + // Expect(err).Should(Succeed()) + // job := jobs[0] + // Expect(k8sClient.Create(testCtx.Ctx, job)).Should(Succeed()) + // + // By("reconcile the opsRequest phase") + // _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) + // Expect(err).Should(Succeed()) + // Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsRunningPhase)) + // + // By("patch job to failed") + // Eventually(func(g Gomega) { + // g.Expect(testapps.ChangeObjStatus(&testCtx, job, func() { + // job.Status.Succeeded = 1 + // job.Status.Conditions = append(job.Status.Conditions, + // batchv1.JobCondition{ + // Type: batchv1.JobFailed, + // Status: corev1.ConditionTrue, + // }) + // })) + // }).Should(Succeed()) + // + // _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsResource) + // Expect(err).Should(Succeed()) + // Expect(ops.Status.Phase).Should(Equal(appsv1alpha1.OpsFailedPhase)) + // + // Expect(k8sClient.Delete(testCtx.Ctx, service)).Should(Succeed()) + // Expect(k8sClient.Delete(testCtx.Ctx, job)).Should(Succeed()) + // Expect(k8sClient.Delete(testCtx.Ctx, secret)).Should(Succeed()) + // }) It("parse script from spec", func() { cmName := "test-configmap" diff --git a/controllers/k8score/event_controller_test.go b/controllers/k8score/event_controller_test.go index 6d3f1e3acd2..ae713b72717 100644 --- a/controllers/k8score/event_controller_test.go +++ b/controllers/k8score/event_controller_test.go @@ -40,7 +40,6 @@ import ( "github.com/apecloud/kubeblocks/pkg/controller/builder" "github.com/apecloud/kubeblocks/pkg/controller/instanceset" "github.com/apecloud/kubeblocks/pkg/generics" - lorryutil "github.com/apecloud/kubeblocks/pkg/lorry/util" testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" ) @@ -65,6 +64,10 @@ var _ = Describe("Event Controller", func() { const ( // roleChangedAnnotKey is used to mark the role change event has been handled. roleChangedAnnotKey = "role.kubeblocks.io/event-handled" + + // TODO(v1.0): remove this later. + checkRoleOperation = "checkRole" + lorryEventFieldPath = "spec.containers{lorry}" ) var ( @@ -81,13 +84,13 @@ var _ = Describe("Event Controller", func() { Namespace: testCtx.DefaultNamespace, Name: podName, UID: podUid, - FieldPath: lorryutil.LorryEventFieldPath, + FieldPath: lorryEventFieldPath, } eventName := strings.Join([]string{podName, seq}, ".") return builder.NewEventBuilder(testCtx.DefaultNamespace, eventName). SetInvolvedObject(objectRef). SetMessage(fmt.Sprintf("{\"event\":\"roleChanged\",\"originalRole\":\"secondary\",\"role\":\"%s\"}", role)). - SetReason(string(lorryutil.CheckRoleOperation)). + SetReason(checkRoleOperation). SetType(corev1.EventTypeNormal). SetFirstTimestamp(metav1.NewTime(initLastTS)). SetLastTimestamp(metav1.NewTime(initLastTS)). diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml index c76723b576a..c26085dcdc2 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml @@ -422,331 +422,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - The conditions are as follows: + This field cannot be updated. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + The conditions are as follows: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - If the Action does not complete within this time frame, it will be terminated. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - format: int32 + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 type: integer type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer type: object dataDump: description: |- @@ -770,331 +708,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + Defines the command to run. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. - Deprecation Notice: + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + This field cannot be updated. + type: string + matchingKey: description: |- - Defines the command to run. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This field cannot be updated. - This field cannot be updated. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + The conditions are as follows: - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - The conditions are as follows: + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - type: string - retryPolicy: + This field cannot be updated. + properties: + maxRetries: + default: 0 description: |- - Defines the strategy to be taken when retrying the Action after a failure. - - - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. - - - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object dataLoad: description: |- @@ -1117,331 +993,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: - - - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. - - + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. + + This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 + + + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. + + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 type: integer type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer type: object memberJoin: description: "Defines the procedure to add a new replica to the @@ -1463,691 +1277,874 @@ spec: SYSTEM ADD SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: This field is immutable once it has been set." properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + This field cannot be updated. - This field cannot be updated. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + + The conditions are as follows: + + + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. + + + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. + + + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. + + + This field cannot be updated. + properties: + maxRetries: + default: 0 description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. + + + If the Action does not complete within this time frame, it will be terminated. + + + This field cannot be updated. + format: int32 + type: integer + type: object + memberLeave: + description: "Defines the procedure to remove a replica from the + replication group.\n\n\nThis action is initiated before remove + a replica from the group.\nThe operator will wait for MemberLeave + to complete successfully before releasing the replica and cleaning + up\nrelated Kubernetes resources.\n\n\nThe process typically + includes updating configurations and informing other group members + about the removal.\nData migration is generally not part of + this action and should be handled separately if needed.\n\n\nThe + container executing this action has access to following variables:\n\n\n- + KB_LEAVE_MEMBER_POD_FQDN: The pod name of the replica being + removed from the group.\n- KB_LEAVE_MEMBER_POD_NAME: The pod + name of the replica being removed from the group.\n\n\nExpected + action output:\n- On Failure: An error message, if applicable, + indicating why the action failed.\n\n\nFor example, to remove + an OBServer from an OceanBase Cluster in 'zone1', the following + command can be executed:\n\n\n```yaml\ncommand:\n- bash\n- -c\n- + |\n CLIENT=\"mysql -u $SERVICE_USER -p$SERVICE_PASSWORD -P + $SERVICE_PORT -h $SERVICE_HOST -e\"\n\t $CLIENT \"ALTER SYSTEM + DELETE SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: + This field is immutable once it has been set." + properties: + exec: + description: |- + Defines the command to run. + + + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. + + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. + + + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. + + + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. + + + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. + + + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: + + + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. + + + This field cannot be updated. + + + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - memberLeave: - description: "Defines the procedure to remove a replica from the - replication group.\n\n\nThis action is initiated before remove - a replica from the group.\nThe operator will wait for MemberLeave - to complete successfully before releasing the replica and cleaning - up\nrelated Kubernetes resources.\n\n\nThe process typically - includes updating configurations and informing other group members - about the removal.\nData migration is generally not part of - this action and should be handled separately if needed.\n\n\nThe - container executing this action has access to following variables:\n\n\n- - KB_LEAVE_MEMBER_POD_FQDN: The pod name of the replica being - removed from the group.\n- KB_LEAVE_MEMBER_POD_NAME: The pod - name of the replica being removed from the group.\n\n\nExpected - action output:\n- On Failure: An error message, if applicable, - indicating why the action failed.\n\n\nFor example, to remove - an OBServer from an OceanBase Cluster in 'zone1', the following - command can be executed:\n\n\n```yaml\ncommand:\n- bash\n- -c\n- - |\n CLIENT=\"mysql -u $SERVICE_USER -p$SERVICE_PASSWORD -P - $SERVICE_PORT -h $SERVICE_HOST -e\"\n\t $CLIENT \"ALTER SYSTEM - DELETE SERVER '$KB_POD_FQDN:$SERVICE_PORT' ZONE 'zone1'\"\n```\n\n\nNote: - This field is immutable once it has been set." - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + postProvision: + description: |- + Specifies the hook to be executed after a component's creation. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + By setting `postProvision.customHandler.preCondition`, you can determine the specific lifecycle stage + at which the action should trigger: `Immediately`, `RuntimeReady`, `ComponentReady`, and `ClusterReady`. + with `ComponentReady` being the default. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + The PostProvision Action is intended to run only once. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + The container executing this action has access to following environment variables: - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + - KB_CLUSTER_POD_IP_LIST: Comma-separated list of the cluster's pod IP addresses (e.g., "podIp1,podIp2"). + - KB_CLUSTER_POD_NAME_LIST: Comma-separated list of the cluster's pod names (e.g., "pod1,pod2"). + - KB_CLUSTER_POD_HOST_NAME_LIST: Comma-separated list of host names, each corresponding to a pod in + KB_CLUSTER_POD_NAME_LIST (e.g., "hostName1,hostName2"). + - KB_CLUSTER_POD_HOST_IP_LIST: Comma-separated list of host IP addresses, each corresponding to a pod in + KB_CLUSTER_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). - Deprecation Notice: + - KB_CLUSTER_COMPONENT_POD_NAME_LIST: Comma-separated list of all pod names within the component + (e.g., "pod1,pod2"). + - KB_CLUSTER_COMPONENT_POD_IP_LIST: Comma-separated list of pod IP addresses, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "podIp1,podIp2"). + - KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST: Comma-separated list of host names for each pod, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostName1,hostName2"). + - KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST: Comma-separated list of host IP addresses for each pod, + matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + - KB_CLUSTER_COMPONENT_LIST: Comma-separated list of all cluster components (e.g., "comp1,comp2"). + - KB_CLUSTER_COMPONENT_DELETING_LIST: Comma-separated list of components that are currently being deleted + (e.g., "comp1,comp2"). + - KB_CLUSTER_COMPONENT_UNDELETED_LIST: Comma-separated list of components that are not being deleted + (e.g., "comp1,comp2"). - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Note: This field is immutable once it has been set. + properties: + exec: + description: |- + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - postProvision: + preTerminate: description: |- - Specifies the hook to be executed after a component's creation. + Specifies the hook to be executed prior to terminating a component. - By setting `postProvision.customHandler.preCondition`, you can determine the specific lifecycle stage - at which the action should trigger: `Immediately`, `RuntimeReady`, `ComponentReady`, and `ClusterReady`. - with `ComponentReady` being the default. + The PreTerminate Action is intended to run only once. - The PostProvision Action is intended to run only once. + This action is executed immediately when a scale-down operation for the Component is initiated. + The actual termination and cleanup of the Component and its associated resources will not proceed + until the PreTerminate action has completed successfully. The container executing this action has access to following environment variables: @@ -2178,717 +2175,573 @@ spec: (e.g., "comp1,comp2"). - Note: This field is immutable once it has been set. - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. - - - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. - - - Deprecation Notice: + - KB_CLUSTER_COMPONENT_IS_SCALING_IN: Indicates whether the component is currently scaling in. + If this variable is present and set to "true", it denotes that the component is undergoing a scale-in operation. + During scale-in, data rebalancing is necessary to maintain cluster integrity. + Contrast this with a cluster deletion scenario where data rebalancing is not required as the entire cluster + is being cleaned up. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + Note: This field is immutable once it has been set. + properties: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - This field cannot be updated. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - The conditions are as follows: + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 - type: integer - type: object - type: object - preTerminate: - description: |- - Specifies the hook to be executed prior to terminating a component. - - - The PreTerminate Action is intended to run only once. - This action is executed immediately when a scale-down operation for the Component is initiated. - The actual termination and cleanup of the Component and its associated resources will not proceed - until the PreTerminate action has completed successfully. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The container executing this action has access to following environment variables: + The conditions are as follows: - - KB_CLUSTER_POD_IP_LIST: Comma-separated list of the cluster's pod IP addresses (e.g., "podIp1,podIp2"). - - KB_CLUSTER_POD_NAME_LIST: Comma-separated list of the cluster's pod names (e.g., "pod1,pod2"). - - KB_CLUSTER_POD_HOST_NAME_LIST: Comma-separated list of host names, each corresponding to a pod in - KB_CLUSTER_POD_NAME_LIST (e.g., "hostName1,hostName2"). - - KB_CLUSTER_POD_HOST_IP_LIST: Comma-separated list of host IP addresses, each corresponding to a pod in - KB_CLUSTER_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - - KB_CLUSTER_COMPONENT_POD_NAME_LIST: Comma-separated list of all pod names within the component - (e.g., "pod1,pod2"). - - KB_CLUSTER_COMPONENT_POD_IP_LIST: Comma-separated list of pod IP addresses, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "podIp1,podIp2"). - - KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST: Comma-separated list of host names for each pod, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostName1,hostName2"). - - KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST: Comma-separated list of host IP addresses for each pod, - matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., "hostIp1,hostIp2"). + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - - KB_CLUSTER_COMPONENT_LIST: Comma-separated list of all cluster components (e.g., "comp1,comp2"). - - KB_CLUSTER_COMPONENT_DELETING_LIST: Comma-separated list of components that are currently being deleted - (e.g., "comp1,comp2"). - - KB_CLUSTER_COMPONENT_UNDELETED_LIST: Comma-separated list of components that are not being deleted - (e.g., "comp1,comp2"). + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - - KB_CLUSTER_COMPONENT_IS_SCALING_IN: Indicates whether the component is currently scaling in. - If this variable is present and set to "true", it denotes that the component is undergoing a scale-in operation. - During scale-in, data rebalancing is necessary to maintain cluster integrity. - Contrast this with a cluster deletion scenario where data rebalancing is not required as the entire cluster - is being cleaned up. + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: + default: 0 + description: |- + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - Note: This field is immutable once it has been set. - properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. + If the Action does not complete within this time frame, it will be terminated. - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. + This field cannot be updated. + format: int32 + type: integer + type: object + readonly: + description: |- + Defines the procedure to switch a replica into the read-only state. - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. + Use Case: + This action is invoked when the database's volume capacity nears its upper limit and space is about to be exhausted. - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + The container executing this action has access to following environment variables: - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + - KB_POD_FQDN: The FQDN of the replica pod whose role is being checked. - Deprecation Notice: + Expected action output: + - On Failure: An error message, if applicable, indicating why the action failed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + Note: This field is immutable once it has been set. + properties: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object - readonly: + readwrite: description: |- - Defines the procedure to switch a replica into the read-only state. + Defines the procedure to transition a replica from the read-only state back to the read-write state. Use Case: - This action is invoked when the database's volume capacity nears its upper limit and space is about to be exhausted. + This action is used to bring back a replica that was previously in a read-only state, + which restricted write operations, to its normal operational state where it can handle + both read and write operations. The container executing this action has access to following environment variables: @@ -2903,680 +2756,269 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: + exec: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Defines the command to run. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + This field cannot be updated. + properties: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: + description: |- + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - Deprecation Notice: + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: - description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + This field cannot be updated. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. - properties: - exec: + Note: This field is reserved for future use and is not currently active. + type: string + env: description: |- - Defines the command to run. + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: + required: + - name + type: object + type: array + image: description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: + Specifies the container image to be used for running the Action. - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. This field cannot be updated. type: string - retryPolicy: + matchingKey: description: |- - Defines the strategy to be taken when retrying the Action after a failure. + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: - default: 0 - description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. This field cannot be updated. - format: int32 - type: integer - type: object - type: object - readwrite: - description: |- - Defines the procedure to transition a replica from the read-only state back to the read-write state. - - - Use Case: - This action is used to bring back a replica that was previously in a read-only state, - which restricted write operations, to its normal operational state where it can handle - both read and write operations. - - - The container executing this action has access to following environment variables: - - - - KB_POD_FQDN: The FQDN of the replica pod whose role is being checked. - Expected action output: - - On Failure: An error message, if applicable, indicating why the action failed. - - - Note: This field is immutable once it has been set. - properties: - builtinHandler: + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. + The conditions are as follows: - Deprecation Notice: + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. + This field cannot be updated. type: string - customHandler: + retryPolicy: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. + Defines the strategy to be taken when retrying the Action after a failure. - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: - description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - - - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. - - - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. - - - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. - properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. - - - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. - - - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: - - - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. - - - This field cannot be updated. - - - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. - - - The conditions are as follows: - - - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. - - - This field cannot be updated. - type: string - retryPolicy: + maxRetries: + default: 0 description: |- - Defines the strategy to be taken when retrying the Action after a failure. - - - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. - - - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object reconfigure: description: |- @@ -3588,338 +3030,276 @@ spec: This Action is reserved for future versions. properties: - builtinHandler: - description: |- - Specifies the name of the predefined action handler to be invoked for lifecycle actions. - - - Lorry, as a sidecar agent co-located with the database container in the same Pod, - includes a suite of built-in action implementations that are tailored to different database engines. - These are known as "builtin" handlers, includes: `mysql`, `redis`, `mongodb`, `etcd`, - `postgresql`, `official-postgresql`, `apecloud-postgresql`, `wesql`, `oceanbase`, `polardbx`. - - - If the `builtinHandler` field is specified, it instructs Lorry to utilize its internal built-in action handler - to execute the specified lifecycle actions. - - - The `builtinHandler` field is of type `BuiltinActionHandlerType`, - which represents the name of the built-in handler. - The `builtinHandler` specified within the same `ComponentLifecycleActions` should be consistent across all - actions. - This means that if you specify a built-in handler for one action, you should use the same handler - for all other actions throughout the entire `ComponentLifecycleActions` collection. - - - If you need to define lifecycle actions for database engines not covered by the existing built-in support, - or when the pre-existing built-in handlers do not meet your specific needs, - you can use the `customHandler` field to define your own action implementation. - - - Deprecation Notice: - - - - In the future, the `builtinHandler` field will be deprecated in favor of using the `customHandler` field - for configuring all lifecycle actions. - - Instead of using a name to indicate the built-in action implementations in Lorry, - the recommended approach will be to explicitly invoke the desired action implementation through - a gRPC interface exposed by the sidecar agent. - - Developers will have the flexibility to either use the built-in action implementations provided by Lorry - or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces. - - This change will allow for greater customization and extensibility of lifecycle actions, - as developers can create their own "builtin" implementations tailored to their specific requirements. - type: string - customHandler: + exec: description: |- - Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. - It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging - tailored actions. - - - An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning - to support GRPCAction, - thereby accommodating unique logic for different database systems within the Action's framework. + Defines the command to run. - In future iterations, all built-in handlers are expected to transition to GRPCAction. - This change means that Lorry or other sidecar agents will expose the implementation of actions - through a GRPC interface for external invocation. - Then the controller will interact with these actions via GRPCAction calls. + This field cannot be updated. properties: - exec: + args: + description: Args represents the arguments that are passed + to the `command` for execution. + items: + type: string + type: array + command: description: |- - Defines the command to run. - - - This field cannot be updated. - properties: - args: - description: Args represents the arguments that are - passed to the `command` for execution. - items: - type: string - type: array - command: - description: |- - Specifies the command to be executed inside the container. - The working directory for this command is the container's root directory('/'). - Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. - If the shell is required, it must be explicitly invoked in the command. - + Specifies the command to be executed inside the container. + The working directory for this command is the container's root directory('/'). + Commands are executed directly without a shell environment, meaning shell-specific syntax ('|', etc.) is not supported. + If the shell is required, it must be explicitly invoked in the command. - A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. - items: - type: string - type: array - container: - description: |- - Defines the name of the container within the target Pod where the action will be executed. + + A successful execution is indicated by an exit status of 0; any non-zero status signifies a failure. + items: + type: string + type: array + container: + description: |- + Defines the name of the container within the target Pod where the action will be executed. - This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. - If this field is not specified, the default behavior is to use the first container listed in - `componentDefinition.spec.runtime`. + This name must correspond to one of the containers defined in `componentDefinition.spec.runtime`. + If this field is not specified, the default behavior is to use the first container listed in + `componentDefinition.spec.runtime`. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - env: - description: |- - Represents a list of environment variables that will be injected into the container. - These variables enable the container to adapt its behavior based on the environment it's running in. + Note: This field is reserved for future use and is not currently active. + type: string + env: + description: |- + Represents a list of environment variables that will be injected into the container. + These variables enable the container to adapt its behavior based on the environment it's running in. - This field cannot be updated. - items: - description: EnvVar represents an environment variable - present in a Container. + This field cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. properties: - name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's - value. Cannot be used if value is not empty. + configMapKeyRef: + description: Selects a key of a ConfigMap. properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the ConfigMap - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: + key: + description: The key to select. + type: string + name: description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, - defaults to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults - to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to - select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in - the pod's namespace - properties: - key: - description: The key of the secret to - select from. Must be a valid secret - key. - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, uid? - type: string - optional: - description: Specify whether the Secret - or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key type: object - required: - - name + x-kubernetes-map-type: atomic type: object - type: array - image: - description: |- - Specifies the container image to be used for running the Action. + required: + - name + type: object + type: array + image: + description: |- + Specifies the container image to be used for running the Action. - When specified, a dedicated container will be created using this image to execute the Action. - This field is mutually exclusive with the `container` field; only one of them should be provided. + When specified, a dedicated container will be created using this image to execute the Action. + This field is mutually exclusive with the `container` field; only one of them should be provided. - This field cannot be updated. - type: string - matchingKey: - description: |- - Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. - The impact of this field depends on the `targetPodSelector` value: + This field cannot be updated. + type: string + matchingKey: + description: |- + Used in conjunction with the `targetPodSelector` field to refine the selection of target pod(s) for Action execution. + The impact of this field depends on the `targetPodSelector` value: - - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. - - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` - will be selected for the Action. + - When `targetPodSelector` is set to `Any` or `All`, this field will be ignored. + - When `targetPodSelector` is set to `Role`, only those replicas whose role matches the `matchingKey` + will be selected for the Action. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - type: string - targetPodSelector: - description: |- - Defines the criteria used to select the target Pod(s) for executing the Action. - This is useful when there is no default target replica identified. - It allows for precise control over which Pod(s) the Action should run in. + Note: This field is reserved for future use and is not currently active. + type: string + targetPodSelector: + description: |- + Defines the criteria used to select the target Pod(s) for executing the Action. + This is useful when there is no default target replica identified. + It allows for precise control over which Pod(s) the Action should run in. - This field cannot be updated. + This field cannot be updated. - Note: This field is reserved for future use and is not currently active. - enum: - - Any - - All - - Role - - Ordinal - type: string - type: object - preCondition: - description: |- - Specifies the state that the cluster must reach before the Action is executed. - Currently, this is only applicable to the `postProvision` action. + Note: This field is reserved for future use and is not currently active. + enum: + - Any + - All + - Role + - Ordinal + type: string + type: object + preCondition: + description: |- + Specifies the state that the cluster must reach before the Action is executed. + Currently, this is only applicable to the `postProvision` action. - The conditions are as follows: + The conditions are as follows: - - `Immediately`: Executed right after the Component object is created. - The readiness of the Component and its resources is not guaranteed at this stage. - - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated - runtime resources (e.g. Pods) are in a ready state. - - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. - This process does not affect the readiness state of the Component or the Cluster. - - `ClusterReady`: The Action is executed after the Cluster is in a ready state. - This execution does not alter the Component or the Cluster's state of readiness. + - `Immediately`: Executed right after the Component object is created. + The readiness of the Component and its resources is not guaranteed at this stage. + - `RuntimeReady`: The Action is triggered after the Component object has been created and all associated + runtime resources (e.g. Pods) are in a ready state. + - `ComponentReady`: The Action is triggered after the Component itself is in a ready state. + This process does not affect the readiness state of the Component or the Cluster. + - `ClusterReady`: The Action is executed after the Cluster is in a ready state. + This execution does not alter the Component or the Cluster's state of readiness. - This field cannot be updated. - type: string - retryPolicy: - description: |- - Defines the strategy to be taken when retrying the Action after a failure. + This field cannot be updated. + type: string + retryPolicy: + description: |- + Defines the strategy to be taken when retrying the Action after a failure. - It specifies the conditions under which the Action should be retried and the limits to apply, - such as the maximum number of retries and backoff strategy. + It specifies the conditions under which the Action should be retried and the limits to apply, + such as the maximum number of retries and backoff strategy. - This field cannot be updated. - properties: - maxRetries: - default: 0 - description: |- - Defines the maximum number of retry attempts that should be made for a given Action. - This value is set to 0 by default, indicating that no retries will be made. - type: integer - retryInterval: - default: 0 - description: |- - Indicates the duration of time to wait between each retry attempt. - This value is set to 0 by default, indicating that there will be no delay between retry attempts. - format: int64 - type: integer - type: object - timeoutSeconds: + This field cannot be updated. + properties: + maxRetries: + default: 0 + description: |- + Defines the maximum number of retry attempts that should be made for a given Action. + This value is set to 0 by default, indicating that no retries will be made. + type: integer + retryInterval: default: 0 description: |- - Specifies the maximum duration in seconds that the Action is allowed to run. + Indicates the duration of time to wait between each retry attempt. + This value is set to 0 by default, indicating that there will be no delay between retry attempts. + format: int64 + type: integer + type: object + timeoutSeconds: + default: 0 + description: |- + Specifies the maximum duration in seconds that the Action is allowed to run. - If the Action does not complete within this time frame, it will be terminated. + If the Action does not complete within this time frame, it will be terminated. - This field cannot be updated. - format: int32 - type: integer - type: object + This field cannot be updated. + format: int32 + type: integer type: object roleProbe: description: |- Defines the procedure which is invoked regularly to assess the role of replicas. - This action is periodically triggered by Lorry at the specified interval to determine the role of each replica. + This action is periodically triggered at the specified interval to determine the role of each replica. Upon successful execution, the action's output designates the role of the replica, which should match one of the predefined role names within `componentDefinition.spec.roles`. The output is then compared with the previous successful execution result. @@ -3946,9 +3326,6 @@ spec: Note: This field is immutable once it has been set. properties: - builtinHandler: - description: 'TODO: remove this later.' - type: string exec: description: |- Defines the command to run. diff --git a/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml b/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml index 4107840c448..8f81c4fca90 100644 --- a/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml +++ b/deploy/helm/crds/workloads.kubeblocks.io_instancesets.yaml @@ -4178,16 +4178,9 @@ spec: roleProbe: description: Provides method to probe role. properties: - builtinHandlerName: - description: |- - Specifies the builtin handler name to use to probe the role of the main container. - Available handlers include: mysql, postgres, mongodb, redis, etcd, kafka. - Use CustomHandler to define a custom role probe function if none of the built-in handlers meet the requirement. - type: string customHandler: description: |- Defines a custom method for role probing. - If the BuiltinHandler meets the requirement, use it instead. Actions defined here are executed in series. Upon completion of all actions, the final output should be a single string representing the role name defined in spec.Roles. The latest [BusyBox](https://busybox.net/) image will be used if Image is not configured. diff --git a/docker/Dockerfile-tools b/docker/Dockerfile-tools index 071e8186dbd..a60c782af95 100644 --- a/docker/Dockerfile-tools +++ b/docker/Dockerfile-tools @@ -38,7 +38,6 @@ COPY go.sum go.sum #COPY pkg/ pkg/ #COPY controllers/ controllers/ #COPY cmd/reloader/ cmd/reloader/ -#COPY cmd/lorry/ cmd/lorry/ #COPY externalapis/ externalapis/ #COPY version/ version/ #COPY cmd/cli/ cmd/cli/ @@ -71,16 +70,6 @@ RUN --mount=type=bind,target=. \ --mount=type=cache,target=/go/pkg/mod \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/config_render cmd/reloader/template/*.go -# RUN --mount=type=bind,target=. \ -# --mount=type=cache,target=/root/.cache/go-build \ -# --mount=type=cache,target=/go/pkg/mod \ -# CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/lorry cmd/lorry/main.go - -# RUN --mount=type=bind,target=. \ -# --mount=type=cache,target=/root/.cache/go-build \ -# --mount=type=cache,target=/go/pkg/mod \ -# CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/lorryctl cmd/lorry/ctl/main.go - RUN --mount=type=bind,target=. \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ @@ -104,22 +93,15 @@ RUN apk add --no-cache kubectl helm jq --allow-untrusted \ && rm -rf /var/cache/apk/* # copy kubeblocks tools -# COPY config/lorry config/lorry COPY config/crd/bases /kubeblocks/crd COPY --from=builder /out/killer /bin COPY --from=builder /out/reloader /bin COPY --from=builder /out/config_render /bin -# COPY --from=builder /out/lorry /bin -# COPY --from=builder /out/lorryctl /bin COPY --from=builder /out/kbagent /bin COPY --from=builder /bin/grpc_health_probe /bin COPY --from=builder /out/helm_hook /bin COPY --from=binary-downloader /bin/curl /bin/ -# make breaking change compatible -# RUN ln -s /bin/lorry /bin/probe -# RUN ln -s /config/lorry /config/probe - # enable grpc_health_probe binary RUN chmod +x /bin/grpc_health_probe diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index c573cd80d31..2fdc2ae51b9 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -2831,7 +2831,7 @@ The modes can be None, Readonly, or ReadWriteAction

-(Appears on:ComponentLifecycleActions, LifecycleActionHandler, Probe) +(Appears on:ComponentLifecycleActions, Probe)

Action defines a customizable hook or procedure tailored for different database engines, @@ -2859,8 +2859,6 @@ such as during planned maintenance or upgrades on the current leader node.

Actions can be executed in different ways:

  • ExecAction: Executes a command inside a container. -which may run as a K8s job or be executed inside the Lorry sidecar container, depending on the implementation. -Future implementations will standardize execution within Lorry. A set of predefined environment variables are available and can be leveraged within the exec.command to access context information such as details about pods, components, the overall cluster state, or database connection credentials. @@ -3658,61 +3656,6 @@ RefNamespaceName

    BaseBackupType the base backup type, keep synchronized with the BaseBackupType of the data protection API.

    -

    BuiltinActionHandlerType -(string alias)

    -

    -(Appears on:LifecycleActionHandler, Probe) -

    -
    -

    BuiltinActionHandlerType defines build-in action handlers provided by Lorry, including:

    -
      -
    • mysql
    • -
    • wesql
    • -
    • oceanbase
    • -
    • redis
    • -
    • mongodb
    • -
    • etcd
    • -
    • postgresql
    • -
    • official-postgresql
    • -
    • apecloud-postgresql
    • -
    • polardbx
    • -
    • custom
    • -
    • unknown
    • -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ValueDescription

    "apecloud-postgresql"

    "custom"

    "etcd"

    "mongodb"

    "mysql"

    "oceanbase"

    "official-postgresql"

    "polardbx"

    "postgresql"

    "redis"

    "unknown"

    "wesql"

    ClusterBackup

    @@ -6807,8 +6750,8 @@ The portName is transformed by replacing ‘-’ with &lsqu postProvision
    - -LifecycleActionHandler + +Action @@ -6848,8 +6791,8 @@ matching the order of pods in KB_CLUSTER_COMPONENT_POD_NAME_LIST (e.g., “h preTerminate
    - -LifecycleActionHandler + +Action @@ -6902,7 +6845,7 @@ Probe (Optional)

    Defines the procedure which is invoked regularly to assess the role of replicas.

    -

    This action is periodically triggered by Lorry at the specified interval to determine the role of each replica. +

    This action is periodically triggered at the specified interval to determine the role of each replica. Upon successful execution, the action’s output designates the role of the replica, which should match one of the predefined role names within componentDefinition.spec.roles. The output is then compared with the previous successful execution result. @@ -6952,8 +6895,8 @@ involving the current leader node.

    memberJoin
    - -LifecycleActionHandler + +Action @@ -6987,8 +6930,8 @@ during the addition of the new member.

    memberLeave
    - -LifecycleActionHandler + +Action @@ -7022,8 +6965,8 @@ Data migration is generally not part of this action and should be handled separa readonly
    - -LifecycleActionHandler + +Action @@ -7045,8 +6988,8 @@ This action is invoked when the database’s volume capacity nears its upper readwrite
    - -LifecycleActionHandler + +Action @@ -7070,8 +7013,8 @@ both read and write operations.

    dataDump
    - -LifecycleActionHandler + +Action @@ -7094,8 +7037,8 @@ that only the necessary data is exported for import into the new replica.

    dataLoad
    - -LifecycleActionHandler + +Action @@ -7117,8 +7060,8 @@ the action must be able to guarantee idempotence to allow for retries from the b reconfigure
    - -LifecycleActionHandler + +Action @@ -7133,8 +7076,8 @@ LifecycleActionHandler accountProvision
    - -LifecycleActionHandler + +Action @@ -10009,9 +9952,7 @@ This name can originate from an ‘env’ entry or be a data key from an (Appears on:Action)

    -

    ExecAction describes an Action that executes a command inside a container. -Which may run as a K8s job or be executed inside the Lorry sidecar container, depending on the implementation. -Future implementations will standardize execution within Lorry.

    +

    ExecAction describes an Action that executes a command inside a container.

    @@ -11251,91 +11192,6 @@ ConfigTemplateExtension
    -

    LifecycleActionHandler -

    -

    -(Appears on:ComponentLifecycleActions) -

    -
    -

    LifecycleActionHandler describes the implementation of a specific lifecycle action.

    -

    Each action is deemed successful if it returns an exit code of 0 for command executions, -or an HTTP 200 status for HTTP(s) actions. -Any other exit code or HTTP status is considered an indication of failure.

    -
    - - - - - - - - - - - - - - - - - -
    FieldDescription
    -builtinHandler
    - - -BuiltinActionHandlerType - - -
    -(Optional) -

    Specifies the name of the predefined action handler to be invoked for lifecycle actions.

    -

    Lorry, as a sidecar agent co-located with the database container in the same Pod, -includes a suite of built-in action implementations that are tailored to different database engines. -These are known as “builtin” handlers, includes: mysql, redis, mongodb, etcd, -postgresql, official-postgresql, apecloud-postgresql, wesql, oceanbase, polardbx.

    -

    If the builtinHandler field is specified, it instructs Lorry to utilize its internal built-in action handler -to execute the specified lifecycle actions.

    -

    The builtinHandler field is of type BuiltinActionHandlerType, -which represents the name of the built-in handler. -The builtinHandler specified within the same ComponentLifecycleActions should be consistent across all -actions. -This means that if you specify a built-in handler for one action, you should use the same handler -for all other actions throughout the entire ComponentLifecycleActions collection.

    -

    If you need to define lifecycle actions for database engines not covered by the existing built-in support, -or when the pre-existing built-in handlers do not meet your specific needs, -you can use the customHandler field to define your own action implementation.

    -

    Deprecation Notice:

    -
      -
    • In the future, the builtinHandler field will be deprecated in favor of using the customHandler field -for configuring all lifecycle actions.
    • -
    • Instead of using a name to indicate the built-in action implementations in Lorry, -the recommended approach will be to explicitly invoke the desired action implementation through -a gRPC interface exposed by the sidecar agent.
    • -
    • Developers will have the flexibility to either use the built-in action implementations provided by Lorry -or develop their own sidecar agent to implement custom actions and expose them via gRPC interfaces.
    • -
    • This change will allow for greater customization and extensibility of lifecycle actions, -as developers can create their own “builtin” implementations tailored to their specific requirements.
    • -
    -
    -customHandler
    - - -Action - - -
    -(Optional) -

    Specifies a user-defined hook or procedure that is called to perform the specific lifecycle action. -It offers a flexible and expandable approach for customizing the behavior of a Component by leveraging -tailored actions.

    -

    An Action can be implemented as either an ExecAction or an HTTPAction, with future versions planning -to support GRPCAction, -thereby accommodating unique logic for different database systems within the Action’s framework.

    -

    In future iterations, all built-in handlers are expected to transition to GRPCAction. -This change means that Lorry or other sidecar agents will expose the implementation of actions -through a GRPC interface for external invocation. -Then the controller will interact with these actions via GRPCAction calls.

    -

    LogConfig

    @@ -13965,19 +13821,6 @@ Action -builtinHandler
    - - -BuiltinActionHandlerType - - - - -(Optional) - - - - initialDelaySeconds
    int32 @@ -14198,49 +14041,6 @@ Kubernetes meta/v1.Time -

    ProtectedVolume -

    -

    -(Appears on:VolumeProtectionSpec) -

    -
    -

    ProtectedVolume is deprecated since v0.9, replaced with ComponentVolume.HighWatermark.

    -
    - - - - - - - - - - - - - - - - - -
    FieldDescription
    -name
    - -string - -
    -(Optional) -

    The Name of the volume to protect.

    -
    -highWatermark
    - -int - -
    -(Optional) -

    Defines the high watermark threshold for the volume, it will override the component level threshold. -If the value is invalid, it will be ignored and the component level threshold will be used.

    -

    ProvisionSecretRef

    @@ -18396,51 +18196,6 @@ that are used to expand the storage and the desired storage size for each one. -

    VolumeProtectionSpec -

    -
    -

    VolumeProtectionSpec is deprecated since v0.9, replaced with ComponentVolume.HighWatermark.

    -
    - - - - - - - - - - - - - - - - - -
    FieldDescription
    -highWatermark
    - -int - -
    -(Optional) -

    The high watermark threshold for volume space usage. -If there is any specified volumes who’s space usage is over the threshold, the pre-defined “LOCK” action -will be triggered to degrade the service to protect volume from space exhaustion, such as to set the instance -as read-only. And after that, if all volumes’ space usage drops under the threshold later, the pre-defined -“UNLOCK” action will be performed to recover the service normally.

    -
    -volumes
    - - -[]ProtectedVolume - - -
    -(Optional) -

    The Volumes to be protected.

    -

    apps.kubeblocks.io/v1beta1

    @@ -21733,20 +21488,6 @@ bool -builtinHandlerName
    - -string - - - -(Optional) -

    Specifies the builtin handler name to use to probe the role of the main container. -Available handlers include: mysql, postgres, mongodb, redis, etcd, kafka. -Use CustomHandler to define a custom role probe function if none of the built-in handlers meet the requirement.

    - - - - customHandler
    @@ -21757,7 +21498,6 @@ Use CustomHandler to define a custom role probe function if none of the built-in (Optional)

    Defines a custom method for role probing. -If the BuiltinHandler meets the requirement, use it instead. Actions defined here are executed in series. Upon completion of all actions, the final output should be a single string representing the role name defined in spec.Roles. The latest BusyBox image will be used if Image is not configured. diff --git a/go.mod b/go.mod index f5d01f3fdc3..ad3f7c5e4bc 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,6 @@ require ( github.com/bhmj/jsonslice v1.1.2 github.com/clbanning/mxj/v2 v2.5.7 github.com/containers/common v0.55.4 - github.com/deckarep/golang-set/v2 v2.3.1 - github.com/dlclark/regexp2 v1.10.0 github.com/docker/docker v25.0.6+incompatible github.com/evanphx/json-patch v5.6.0+incompatible github.com/fasthttp/router v1.4.20 @@ -23,30 +21,23 @@ require ( github.com/go-sql-driver/mysql v1.7.1 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 - github.com/hashicorp/go-hclog v1.5.0 - github.com/hashicorp/vault/sdk v0.9.2 github.com/imdario/mergo v0.3.14 - github.com/jackc/pgx/v5 v5.5.4 github.com/klauspost/compress v1.17.8 github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0 github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0 github.com/magiconair/properties v1.8.7 - github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 github.com/onsi/ginkgo/v2 v2.15.0 github.com/onsi/gomega v1.31.0 github.com/opencontainers/image-spec v1.1.0 - github.com/pashagolub/pgxmock/v2 v2.11.0 github.com/pkg/errors v0.9.1 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.0 github.com/prometheus/client_golang v1.19.0 - github.com/redis/go-redis/v9 v9.0.5 github.com/replicatedhq/troubleshoot v0.57.0 github.com/rogpeppe/go-internal v1.12.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/sethvargo/go-password v0.2.0 github.com/shirou/gopsutil/v3 v3.23.6 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/afero v1.9.5 github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 @@ -55,9 +46,6 @@ require ( github.com/sykesm/zap-logfmt v0.0.4 github.com/valyala/fasthttp v1.50.0 github.com/vmware-tanzu/velero v1.13.2 - go.etcd.io/etcd/client/v3 v3.5.10 - go.etcd.io/etcd/server/v3 v3.5.10 - go.mongodb.org/mongo-driver v1.11.6 go.uber.org/automaxprocs v1.5.2 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.22.0 @@ -82,7 +70,6 @@ require ( k8s.io/klog/v2 v2.120.1 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 k8s.io/kubectl v0.29.0 - k8s.io/kubelet v0.29.0 k8s.io/utils v0.0.0-20231127182322-b307cd553661 sigs.k8s.io/controller-runtime v0.17.2 sigs.k8s.io/yaml v1.4.0 @@ -100,8 +87,6 @@ require ( github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect - github.com/armon/go-metrics v0.4.1 // indirect - github.com/armon/go-radix v1.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bhmj/xpression v0.9.1 // indirect @@ -109,18 +94,13 @@ require ( github.com/bshuster-repo/logrus-logstash-hook v1.0.2 // indirect github.com/bugsnag/bugsnag-go v2.1.2+incompatible // indirect github.com/bugsnag/panicwrap v1.3.4 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/containerd v1.7.11 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/cli v25.0.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -129,7 +109,6 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.10.0 // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect @@ -137,7 +116,6 @@ require ( github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fvbommel/sortorder v1.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.0.5 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -149,10 +127,8 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/cel-go v0.17.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -164,32 +140,17 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-kms-wrapping/v2 v2.0.8 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect - github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect - github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/jonboulle/clockwork v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect @@ -208,6 +169,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -215,14 +177,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/montanaflynn/stats v0.6.6 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.1 // indirect @@ -231,39 +191,25 @@ require ( github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect github.com/rivo/uniseg v0.4.6 // indirect github.com/rubenv/sql-migrate v1.3.1 // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect - github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect - github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/xlab/treeprint v1.2.0 // indirect - github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 // indirect github.com/yvasiyarov/gorelic v0.0.7 // indirect github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 // indirect - go.etcd.io/bbolt v1.3.8 // indirect - go.etcd.io/etcd/api/v3 v3.5.10 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect - go.etcd.io/etcd/client/v2 v2.305.10 // indirect - go.etcd.io/etcd/pkg/v3 v3.5.10 // indirect - go.etcd.io/etcd/raft/v3 v3.5.10 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.25.0 // indirect @@ -274,7 +220,6 @@ require ( go.opentelemetry.io/otel/trace v1.25.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect @@ -284,11 +229,9 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.19.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.0 // indirect k8s.io/apiserver v0.29.0 // indirect diff --git a/go.sum b/go.sum index b2a13a9bca6..f85e001fec2 100644 --- a/go.sum +++ b/go.sum @@ -64,7 +64,6 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -94,9 +93,7 @@ github.com/StudioSol/set v1.0.0 h1:G27J71la+Da08WidabBkoRrvPLTa4cdCn0RjvyJ5WKQ= github.com/StudioSol/set v1.0.0/go.mod h1:hIUNZPo6rEGF43RlPXHq7Fjmf+HkVJBqAjtK7Z9LoIU= github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -105,10 +102,7 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/g github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= -github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -116,7 +110,6 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -132,10 +125,6 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v1.0.2 h1:JYRWo+QGnQdedgshosug9hxpPYTB9oJ1ZZD3fY31alU= github.com/bshuster-repo/logrus-logstash-hook v1.0.2/go.mod h1:HgYntJprnHSPaF9VPPPLP1L5S1vMWxRfa1J+vzDrDTw= -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= -github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bugsnag/bugsnag-go v2.1.2+incompatible h1:E7dor84qzwUO8KdCM68CZwq9QOSR7HXlLx3Wj5vui2s= @@ -146,7 +135,6 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= @@ -154,8 +142,6 @@ github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHe github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0= github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -164,8 +150,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= -github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= -github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= @@ -182,13 +166,13 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -199,19 +183,13 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= -github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -232,8 +210,6 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -264,8 +240,6 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= @@ -277,8 +251,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -286,8 +258,6 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gorp/gorp/v3 v3.0.5 h1:PUjzYdYu3HBOh8LE+UUmRG2P0IRDak9XMeGNvaeq4Ow= github.com/go-gorp/gorp/v3 v3.0.5/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -329,8 +299,6 @@ github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= @@ -348,8 +316,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -387,9 +353,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -464,8 +427,6 @@ github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= @@ -479,40 +440,21 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/v2 v2.0.8 h1:9Q2lu1YbbmiAgvYZ7Pr31RdlVonUpX+mmDL7Z7qTA2U= -github.com/hashicorp/go-kms-wrapping/v2 v2.0.8/go.mod h1:qTCjxGig/kjuj3hk1z8pOUrzbse/GxB1tGfbrq8tGJg= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -525,8 +467,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/vault/sdk v0.9.2 h1:H1kitfl1rG2SHbeGEyvhEqmIjVKE3E6c2q3ViKOs6HA= -github.com/hashicorp/vault/sdk v0.9.2/go.mod h1:gG0lA7P++KefplzvcD3vrfCmgxVAM7Z/SqX5NeOL/98= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -545,14 +485,6 @@ github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= -github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -562,13 +494,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= -github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -585,7 +514,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -676,7 +604,6 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -706,9 +633,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= -github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -736,12 +660,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pashagolub/pgxmock/v2 v2.11.0 h1:ZUKqZy5Zf/5WJjAXHErjHngJBW5/3fEujGD+Cb0FuDI= -github.com/pashagolub/pgxmock/v2 v2.11.0/go.mod h1:D3YslkN/nJ4+umVqWmbwfSXugJIjPMChkGBG47OJpNw= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= @@ -750,8 +669,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -778,13 +695,11 @@ github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= @@ -792,7 +707,6 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7q github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -800,14 +714,11 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= -github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= -github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/replicatedhq/troubleshoot v0.57.0 h1:m9B31Mhgiz4Lwz+W4RvFkqhfYZLCwAqRPUwiwmSAAps= github.com/replicatedhq/troubleshoot v0.57.0/go.mod h1:R5VdixzaBXfWLbP9mcLuZKs/bDCyGGS4+vFtKGWs9xE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -827,9 +738,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -848,15 +756,12 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -911,17 +816,11 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/sykesm/zap-logfmt v0.0.4 h1:U2WzRvmIWG1wDLCFY3sz8UeEmsdHQjHFNlIdmroVFaI= github.com/sykesm/zap-logfmt v0.0.4/go.mod h1:AuBd9xQjAe3URrWT1BBDk2v2onAZHkZkWRMiYZXiZWA= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -929,14 +828,6 @@ github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/vmware-tanzu/velero v1.13.2 h1:72Rw+11HJB6XUYfH9/M/jle6duSLyGhMisMMYFr/1qs= github.com/vmware-tanzu/velero v1.13.2/go.mod h1:yHFPyr+iwpKRf66xJ88MriAHiX58tTnKmQXY2FQZClM= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -944,13 +835,10 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -967,8 +855,6 @@ github.com/yvasiyarov/gorelic v0.0.7/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96Tg github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 h1:AsFN8kXcCVkUFHyuzp1FtYbzp1nCO/H6+1uPSGEyPzM= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= @@ -976,18 +862,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.10 h1:MrmRktzv/XF8CvtQt+P6wLUlURaNpSDJHFZhe//2QE4= -go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA= go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= -go.etcd.io/etcd/pkg/v3 v3.5.10 h1:WPR8K0e9kWl1gAhB5A7gEa5ZBTNkT9NdNWrR8Qpo1CM= -go.etcd.io/etcd/pkg/v3 v3.5.10/go.mod h1:TKTuCKKcF1zxmfKWDkfz5qqYaE3JncKKZPFf8c1nFUs= -go.etcd.io/etcd/raft/v3 v3.5.10 h1:cgNAYe7xrsrn/5kXMSaH8kM/Ky8mAdMqGOxyYwpP0LA= -go.etcd.io/etcd/raft/v3 v3.5.10/go.mod h1:odD6kr8XQXTy9oQnyMPBOr0TVe+gT0neQhElQ6jbGRc= -go.etcd.io/etcd/server/v3 v3.5.10 h1:4NOGyOwD5sUZ22PiWYKmfxqoeh72z6EhYjNosKGLmZg= -go.etcd.io/etcd/server/v3 v3.5.10/go.mod h1:gBplPHfs6YI0L+RpGkTQO7buDbHv5HJGG/Bst0/zIPo= -go.mongodb.org/mongo-driver v1.11.6 h1:XM7G6PjiGAO5betLF13BIa5TlLUUE3uJ/2Ox3Lz1K+o= -go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1022,11 +898,8 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -1038,7 +911,6 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1056,7 +928,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -1126,7 +997,6 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -1136,7 +1006,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= @@ -1181,7 +1050,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1231,7 +1099,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1262,7 +1129,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -1293,7 +1159,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1395,7 +1260,6 @@ google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1418,8 +1282,6 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= @@ -1475,8 +1337,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= @@ -1484,7 +1344,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -1547,8 +1406,6 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= -k8s.io/kubelet v0.29.0 h1:SX5hlznTBcGIrS1scaf8r8p6m3e475KMifwt9i12iOk= -k8s.io/kubelet v0.29.0/go.mod h1:kvKS2+Bz2tgDOG1S1q0TH2z1DasNuVF+8p6Aw7xvKkI= k8s.io/metrics v0.29.0 h1:a6dWcNM+EEowMzMZ8trka6wZtSRIfEA/9oLjuhBksGc= k8s.io/metrics v0.29.0/go.mod h1:UCuTT4dC/x/x6ODSk87IWIZQnuAfcwxOjb1gjWJdjMA= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/hack/docgen/lorryctl/main.go b/hack/docgen/lorryctl/main.go deleted file mode 100644 index aef6fa505db..00000000000 --- a/hack/docgen/lorryctl/main.go +++ /dev/null @@ -1,165 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "fmt" - "io" - "log" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" - - lorryctl "github.com/apecloud/kubeblocks/pkg/lorry/ctl" -) - -func genMarkdownTreeForOverview(cmd *cobra.Command, dir string) error { - filename := filepath.Join(dir, "cli.md") - f, err := os.Create(filename) - if err != nil { - return err - } - defer f.Close() - - if _, err = io.WriteString(f, `--- -title: KubeBlocks Lorry CLI Overview -description: KubeBlocks Lorry CLI overview -sidebar_position: 1 ---- - -`); err != nil { - return err - } - - for _, c := range cmd.Commands() { - if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { - continue - } - - // write parent command name - link := strings.ReplaceAll(cmd.Name()+" "+c.Name(), " ", "_") - _, err = io.WriteString(f, fmt.Sprintf("## [%s](%s.md)\n\n", c.Name(), link)) - if err != nil { - return err - } - - // write command description - switch { - case c.Long != "": - _, err = io.WriteString(f, fmt.Sprintf("%s\n\n", c.Long)) - case c.Short != "": - _, err = io.WriteString(f, fmt.Sprintf("%s\n\n", c.Short)) - } - if err != nil { - return err - } - - // write subcommands - for _, sub := range c.Commands() { - if !sub.IsAvailableCommand() || sub.IsAdditionalHelpTopicCommand() { - continue - } - subName := cmd.Name() + " " + c.Name() + " " + sub.Name() - link = strings.ReplaceAll(subName, " ", "_") - _, err = io.WriteString(f, fmt.Sprintf("* [%s](%s.md)\t - %s\n", subName, link, sub.Short)) - if err != nil { - return err - } - } - _, err = io.WriteString(f, "\n\n") - if err != nil { - return err - } - } - return nil -} - -func main() { - rootPath := "./docs/user_docs/lorryctl" - if len(os.Args) > 1 { - rootPath = os.Args[1] - } - - fmt.Println("Scanning CLI docs rootPath: ", rootPath) - ctl := lorryctl.RootCmd - ctl.Long = fmt.Sprintf("```\n%s\n```", ctl.Long) - - err := doc.GenMarkdownTree(ctl, rootPath) - if err != nil { - log.Fatal(err) - } - - ctl.Long = fmt.Sprintf("```\n%s\n```", ctl.Long) - err = genMarkdownTreeForOverview(ctl, rootPath) - if err != nil { - log.Fatal("generate docs for cli overview failed", err) - } - - err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - lines := strings.Split(string(data), "\n") - if len(lines) == 0 { - return nil - } - - firstLine := lines[0] - if !strings.HasPrefix(firstLine, "## lorryctl") { - return nil - } - - var lastIdx int - for idx := len(lines) - 1; idx >= 0; idx-- { - if strings.Contains(lines[idx], "Auto generated") { - lastIdx = idx - break - } - } - if lastIdx == 0 { - return nil - } - lines[lastIdx] = "#### Go Back to [LorryCtl Overview](cli.md) Homepage.\n" - - // update the title - lines[0] = "---" - title := strings.TrimPrefix(firstLine, "## ") - newLines := []string{"---", "title: " + title} - for idx, line := range lines { - if strings.Contains(line, "[kbcli](kbcli.md)") { - lines[idx] = "" - continue - } - } - newLines = append(newLines, lines...) - content := strings.Join(newLines, "\n") - return os.WriteFile(path, []byte(content), info.Mode()) - }) - if err != nil { - log.Fatal(err) - } -} diff --git a/pkg/common/doc.go b/pkg/common/doc.go index 913995ca73b..67b49f75ee4 100644 --- a/pkg/common/doc.go +++ b/pkg/common/doc.go @@ -18,7 +18,7 @@ along with this program. If not, see . */ /* -Package common provides types and utils shared by all KubeBlocks components: KubeBlocks Core, KBCLI, Lorry etc. +Package common provides types and utils shared by all KubeBlocks components: KubeBlocks Core, KBCLI etc. will promote to pkg/common when stable. */ package common diff --git a/pkg/common/types.go b/pkg/common/types.go index a82090d6027..9e28095dcb6 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -50,16 +50,3 @@ type Exporter struct { appsv1alpha1.Exporter `json:",inline"` TargetPort *intstr.IntOrString `json:"targetPort,omitempty"` } - -// BuiltinHandler defines builtin role probe handler name. -type BuiltinHandler string - -const ( - MySQLHandler BuiltinHandler = "mysql" - PostgresHandler BuiltinHandler = "postgres" - MongoDBHandler BuiltinHandler = "mongodb" - RedisHandler BuiltinHandler = "redis" - ETCDHandler BuiltinHandler = "etcd" - KafkaHandler BuiltinHandler = "kafka" - WeSQLHandler BuiltinHandler = "wesql" -) diff --git a/pkg/constant/env.go b/pkg/constant/env.go index a754f503bc3..c0ebf7111b1 100644 --- a/pkg/constant/env.go +++ b/pkg/constant/env.go @@ -81,39 +81,6 @@ const ( // Lorry const ( - KBEnvWorkloadType = "KB_WORKLOAD_TYPE" - KBEnvBuiltinHandler = "KB_BUILTIN_HANDLER" - KBEnvActionCommands = "KB_ACTION_COMMANDS" - KBEnvActionHandlers = "KB_ACTION_HANDLERS" - KBEnvCronJobs = "KB_CRON_JOBS" - KBEnvCharacterType = "KB_SERVICE_CHARACTER_TYPE" KBEnvServiceUser = "KB_SERVICE_USER" KBEnvServicePassword = "KB_SERVICE_PASSWORD" - KBEnvLorryHTTPPort = "LORRY_HTTP_PORT" - KBEnvLorryGRPCPort = "LORRY_GRPC_PORT" - KBEnvLorryLogLevel = "LORRY_LOG_LEVEL" - // KBEnvServiceRoles defines the Roles configured in the cluster definition that are visible to users. - KBEnvServiceRoles = "KB_SERVICE_ROLES" - - // KBEnvServicePort defines the port of the DB service - KBEnvServicePort = "KB_SERVICE_PORT" - - // KBEnvDataPath defines the data volume path of the DB service. - KBEnvDataPath = "KB_DATA_PATH" - - // KBEnvTTL controls the lease expiration time in DCS. If the leader fails to renew its lease within the TTL duration, it will lose the leader role, allowing other replicas to take over. - KBEnvTTL = "KB_TTL" - - // KBEnvMaxLag defines maximum replication lag permitted when performing a switchover. - KBEnvMaxLag = "KB_MAX_LAG" - - // KBEnvEnableHA Whether to enable high availability, true by default. - KBEnvEnableHA = "KB_ENABLE_HA" - - // KBEnvRsmRoleUpdateMechanism defines the method to send events: DirectAPIServerEventUpdate(through lorry service), ReadinessProbeEventUpdate(through kubelet service) - KBEnvRsmRoleUpdateMechanism = "KB_RSM_ROLE_UPDATE_MECHANISM" - KBEnvRoleProbeTimeout = "KB_RSM_ROLE_PROBE_TIMEOUT" - KBEnvRoleProbePeriod = "KB_RSM_ROLE_PROBE_PERIOD" - - KBEnvVolumeProtectionSpec = "KB_VOLUME_PROTECTION_SPEC" ) diff --git a/pkg/constant/lorry.go b/pkg/constant/lorry.go deleted file mode 100644 index 834aca32482..00000000000 --- a/pkg/constant/lorry.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package constant - -const ( - LorryHTTPPortName = "lorry-http-port" - LorryGRPCPortName = "lorry-grpc-port" - LorryContainerName = "lorry" - LorryInitContainerName = "init-lorry" - RoleProbeContainerName = "kb-checkrole" - VolumeProtectionProbeContainerName = "kb-volume-protection" - LorryRoleProbePath = "/v1.0/checkrole" - LorryVolumeProtectPath = "/v1.0/volumeprotection" -) - -// action keys -const ( - RoleProbeAction = "roleProbe" - RebuildAction = "rebuild" - HealthyCheckAction = "healthyCheck" - MemberJoinAction = "memberJoin" - MemberLeaveAction = "memberLeave" - ReadonlyAction = "readonly" - ReadWriteAction = "readwrite" - AccountProvisionAction = "accountProvision" - PostProvisionAction = "postProvision" - PreTerminateAction = "preTerminate" - DataDumpAction = "dataDump" - DataLoadAction = "dataLoad" -) diff --git a/pkg/controller/component/action_post_provision.go b/pkg/controller/component/action_post_provision.go index deb9a1974e8..d01d37fd9d1 100644 --- a/pkg/controller/component/action_post_provision.go +++ b/pkg/controller/component/action_post_provision.go @@ -92,7 +92,7 @@ func NeedDoPostProvision(ctx context.Context, cli client.Reader, actionCtx *Acti return false, nil } - actionPreCondition := actionCtx.lifecycleActions.PostProvision.CustomHandler.PreCondition + actionPreCondition := actionCtx.lifecycleActions.PostProvision.PreCondition if actionPreCondition != nil { switch *actionPreCondition { case appsv1alpha1.ImmediatelyPreConditionType: diff --git a/pkg/controller/component/action_post_provision_test.go b/pkg/controller/component/action_post_provision_test.go index 6f555f0b291..8d667cc7dd3 100644 --- a/pkg/controller/component/action_post_provision_test.go +++ b/pkg/controller/component/action_post_provision_test.go @@ -110,7 +110,7 @@ var _ = Describe("Component PostProvision Test", func() { Expect(err).Should(Succeed()) Expect(synthesizeComp).ShouldNot(BeNil()) Expect(synthesizeComp.LifecycleActions).ShouldNot(BeNil()) - Expect(synthesizeComp.LifecycleActions.PostProvision).ShouldNot(BeNil()) + Expect(synthesizeComp.LifecycleActions.PostProvision).Should(BeNil()) dag := graph.NewDAG() dag.AddVertex(&model.ObjectVertex{Obj: cluster, Action: model.ActionUpdatePtr()}) @@ -123,17 +123,15 @@ var _ = Describe("Component PostProvision Test", func() { By("build component with postProvision without PodList, do not need to do PostProvision action") synthesizeComp.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{} defaultPreCondition := appsv1alpha1.ComponentReadyPreConditionType - postProvision := appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &appsv1alpha1.Action{ - Exec: &appsv1alpha1.ExecAction{ - Image: constant.KBToolsImage, - Command: []string{"echo", "mock"}, - Args: []string{}, - }, - PreCondition: &defaultPreCondition, + postProvision := &appsv1alpha1.Action{ + Exec: &appsv1alpha1.ExecAction{ + Image: constant.KBToolsImage, + Command: []string{"echo", "mock"}, + Args: []string{}, }, + PreCondition: &defaultPreCondition, } - synthesizeComp.LifecycleActions.PostProvision = &postProvision + synthesizeComp.LifecycleActions.PostProvision = postProvision actionCtx, err = NewActionContext(cluster, comp, nil, synthesizeComp.LifecycleActions, synthesizeComp.ScriptTemplates, PostProvisionAction) Expect(err).Should(Succeed()) need, err := NeedDoPostProvision(testCtx.Ctx, k8sClient, actionCtx) diff --git a/pkg/controller/component/action_pre_terminate_test.go b/pkg/controller/component/action_pre_terminate_test.go index eb3db743b48..504281927ce 100644 --- a/pkg/controller/component/action_pre_terminate_test.go +++ b/pkg/controller/component/action_pre_terminate_test.go @@ -111,7 +111,7 @@ var _ = Describe("Component PreTerminate Test", func() { Expect(err).Should(Succeed()) Expect(synthesizeComp).ShouldNot(BeNil()) Expect(synthesizeComp.LifecycleActions).ShouldNot(BeNil()) - Expect(synthesizeComp.LifecycleActions.PreTerminate).ShouldNot(BeNil()) + Expect(synthesizeComp.LifecycleActions.PreTerminate).Should(BeNil()) By("test component without preTerminate action and no need to do PreTerminate action") dag := graph.NewDAG() @@ -135,16 +135,14 @@ var _ = Describe("Component PreTerminate Test", func() { Expect(k8sClient.Status().Update(ctx, &pod)).Should(Succeed()) } synthesizeComp.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{} - PreTerminate := appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &appsv1alpha1.Action{ - Exec: &appsv1alpha1.ExecAction{ - Image: constant.KBToolsImage, - Command: []string{"echo", "mock"}, - Args: []string{}, - }, + PreTerminate := &appsv1alpha1.Action{ + Exec: &appsv1alpha1.ExecAction{ + Image: constant.KBToolsImage, + Command: []string{"echo", "mock"}, + Args: []string{}, }, } - synthesizeComp.LifecycleActions.PreTerminate = &PreTerminate + synthesizeComp.LifecycleActions.PreTerminate = PreTerminate actionCtx, err = NewActionContext(cluster, comp, nil, synthesizeComp.LifecycleActions, synthesizeComp.ScriptTemplates, PreTerminateAction) Expect(err).Should(Succeed()) need, err = needDoPreTerminate(testCtx.Ctx, k8sClient, actionCtx) diff --git a/pkg/controller/component/action_utils.go b/pkg/controller/component/action_utils.go index e31564484a4..52ae67a61d1 100644 --- a/pkg/controller/component/action_utils.go +++ b/pkg/controller/component/action_utils.go @@ -526,13 +526,9 @@ func checkLifeCycleAction(actionCtx *ActionContext) (bool, *appsv1alpha1.Action) var action *appsv1alpha1.Action switch actionCtx.actionType { case PostProvisionAction: - if actions := actionCtx.lifecycleActions.PostProvision; actions != nil { - action = actions.CustomHandler - } + action = actionCtx.lifecycleActions.PostProvision case PreTerminateAction: - if actions := actionCtx.lifecycleActions.PreTerminate; actions != nil { - action = actions.CustomHandler - } + action = actionCtx.lifecycleActions.PreTerminate default: return false, nil } diff --git a/pkg/controller/component/action_utils_test.go b/pkg/controller/component/action_utils_test.go index 9bf6718ae0c..7657961b547 100644 --- a/pkg/controller/component/action_utils_test.go +++ b/pkg/controller/component/action_utils_test.go @@ -112,7 +112,7 @@ var _ = Describe("Component LifeCycle Action Utils Test", func() { Expect(err).Should(Succeed()) Expect(synthesizeComp).ShouldNot(BeNil()) Expect(synthesizeComp.LifecycleActions).ShouldNot(BeNil()) - Expect(synthesizeComp.LifecycleActions.PostProvision).ShouldNot(BeNil()) + Expect(synthesizeComp.LifecycleActions.PostProvision).Should(BeNil()) dag := graph.NewDAG() dag.AddVertex(&model.ObjectVertex{Obj: cluster, Action: model.ActionUpdatePtr()}) @@ -123,16 +123,14 @@ var _ = Describe("Component LifeCycle Action Utils Test", func() { By("build component with preTerminate without PodList, check the built-in envs of cluster component available in action job") synthesizeComp.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{} - preTerminate := appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &appsv1alpha1.Action{ - Exec: &appsv1alpha1.ExecAction{ - Image: constant.KBToolsImage, - Command: []string{"echo", "mock"}, - Args: []string{}, - }, + preTerminate := &appsv1alpha1.Action{ + Exec: &appsv1alpha1.ExecAction{ + Image: constant.KBToolsImage, + Command: []string{"echo", "mock"}, + Args: []string{}, }, } - synthesizeComp.LifecycleActions.PreTerminate = &preTerminate + synthesizeComp.LifecycleActions.PreTerminate = preTerminate By("check built-in envs of cluster component available in action job") pods := mockPodsForTest(cluster, 1) diff --git a/pkg/controller/component/kbagent.go b/pkg/controller/component/kbagent.go index 4925a46ab3c..1b225c8ed78 100644 --- a/pkg/controller/component/kbagent.go +++ b/pkg/controller/component/kbagent.go @@ -157,10 +157,8 @@ func buildKBAgentStartupEnvs(synthesizedComp *SynthesizedComponent) ([]corev1.En if a := buildAction4KBAgent(synthesizedComp.LifecycleActions.PreTerminate, "preTerminate"); a != nil { actions = append(actions, *a) } - if synthesizedComp.LifecycleActions.Switchover != nil { - if a := buildAction4KBAgentLow(synthesizedComp.LifecycleActions.Switchover, "switchover"); a != nil { - actions = append(actions, *a) - } + if a := buildAction4KBAgent(synthesizedComp.LifecycleActions.Switchover, "switchover"); a != nil { + actions = append(actions, *a) } if a := buildAction4KBAgent(synthesizedComp.LifecycleActions.MemberJoin, "memberJoin"); a != nil { actions = append(actions, *a) @@ -195,14 +193,7 @@ func buildKBAgentStartupEnvs(synthesizedComp *SynthesizedComponent) ([]corev1.En return kbagent.BuildStartupEnvs(actions, probes) } -func buildAction4KBAgent(handler *appsv1alpha1.LifecycleActionHandler, name string) *proto.Action { - if handler == nil { - return nil - } - return buildAction4KBAgentLow(handler.CustomHandler, name) -} - -func buildAction4KBAgentLow(action *appsv1alpha1.Action, name string) *proto.Action { +func buildAction4KBAgent(action *appsv1alpha1.Action, name string) *proto.Action { if action == nil || action.Exec == nil { return nil } @@ -228,7 +219,7 @@ func buildProbe4KBAgent(probe *appsv1alpha1.Probe, name string) (*proto.Action, if probe == nil || probe.Exec == nil { return nil, nil } - a := buildAction4KBAgentLow(&probe.Action, name) + a := buildAction4KBAgent(&probe.Action, name) p := &proto.Probe{ Action: name, InitialDelaySeconds: probe.InitialDelaySeconds, @@ -270,9 +261,10 @@ func customExecActionImageNContainer(synthesizedComp *SynthesizedComponent) (str return "", nil, nil } - handlers := []*appsv1alpha1.LifecycleActionHandler{ + actions := []*appsv1alpha1.Action{ synthesizedComp.LifecycleActions.PostProvision, synthesizedComp.LifecycleActions.PreTerminate, + synthesizedComp.LifecycleActions.Switchover, synthesizedComp.LifecycleActions.MemberJoin, synthesizedComp.LifecycleActions.MemberLeave, synthesizedComp.LifecycleActions.Readonly, @@ -283,27 +275,25 @@ func customExecActionImageNContainer(synthesizedComp *SynthesizedComponent) (str synthesizedComp.LifecycleActions.AccountProvision, } if synthesizedComp.LifecycleActions.RoleProbe != nil && synthesizedComp.LifecycleActions.RoleProbe.Exec != nil { - handlers = append(handlers, &appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &synthesizedComp.LifecycleActions.RoleProbe.Action, - }) + actions = append(actions, &synthesizedComp.LifecycleActions.RoleProbe.Action) } var image, container string - for _, handler := range handlers { - if handler == nil || handler.CustomHandler == nil || handler.CustomHandler.Exec == nil { + for _, action := range actions { + if action == nil || action.Exec == nil { continue } - if handler.CustomHandler.Exec.Image != "" { - if len(image) > 0 && image != handler.CustomHandler.Exec.Image { + if action.Exec.Image != "" { + if len(image) > 0 && image != action.Exec.Image { return "", nil, fmt.Errorf("only one exec image is allowed in lifecycle actions") } - image = handler.CustomHandler.Exec.Image + image = action.Exec.Image } - if handler.CustomHandler.Exec.Container != "" { - if len(container) > 0 && container != handler.CustomHandler.Exec.Container { + if action.Exec.Container != "" { + if len(container) > 0 && container != action.Exec.Container { return "", nil, fmt.Errorf("only one exec container is allowed in lifecycle actions") } - container = handler.CustomHandler.Exec.Container + container = action.Exec.Container } } diff --git a/pkg/controller/component/lifecycle/kbagent.go b/pkg/controller/component/lifecycle/kbagent.go index 1223f10d415..8a89fd9398c 100644 --- a/pkg/controller/component/lifecycle/kbagent.go +++ b/pkg/controller/component/lifecycle/kbagent.go @@ -62,10 +62,7 @@ func (a *kbagent) PreTerminate(ctx context.Context, cli client.Reader, opts *Opt func (a *kbagent) Switchover(ctx context.Context, cli client.Reader, opts *Options) error { la := &switchover{} - if a.lifecycleActions.Switchover == nil { - return errors.Wrap(ErrActionNotDefined, la.name()) - } - return a.callAction(ctx, cli, a.lifecycleActions.Switchover, la, opts) + return a.checkedCallAction(ctx, cli, a.lifecycleActions.Switchover, la, opts) } func (a *kbagent) MemberJoin(ctx context.Context, cli client.Reader, opts *Options) error { @@ -99,12 +96,11 @@ func (a *kbagent) AccountProvision(ctx context.Context, cli client.Reader, opts return a.checkedCallAction(ctx, cli, a.lifecycleActions.AccountProvision, la, opts) } -func (a *kbagent) checkedCallAction(ctx context.Context, cli client.Reader, - handler *appsv1alpha1.LifecycleActionHandler, la lifecycleAction, opts *Options) error { - if handler == nil || handler.CustomHandler == nil { +func (a *kbagent) checkedCallAction(ctx context.Context, cli client.Reader, action *appsv1alpha1.Action, la lifecycleAction, opts *Options) error { + if action == nil { return errors.Wrap(ErrActionNotDefined, la.name()) } - return a.callAction(ctx, cli, handler.CustomHandler, la, opts) + return a.callAction(ctx, cli, action, la, opts) } func (a *kbagent) callAction(ctx context.Context, cli client.Reader, spec *appsv1alpha1.Action, la lifecycleAction, opts *Options) error { diff --git a/pkg/controller/component/lorry_utils.go b/pkg/controller/component/lorry_utils.go deleted file mode 100644 index f0e05b8328d..00000000000 --- a/pkg/controller/component/lorry_utils.go +++ /dev/null @@ -1,571 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package component - -import ( - "encoding/json" - "errors" - "fmt" - "strconv" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/sets" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/controller/builder" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const ( - dataVolume = "data" - minAvailPort = 1 - maxAvailPort = 65535 -) - -var ( - // default probe setting for volume protection. - defaultVolumeProtectionProbeTimeout int32 = 5 - defaultVolumeProtectionProbePeriod int32 = 60 -) - -// buildLorryContainers builds lorry containers for component. -// In the new ComponentDefinition API, StatusProbe and RunningProbe have been removed. -func buildLorryContainers(reqCtx intctrlutil.RequestCtx, synthesizeComp *SynthesizedComponent, clusterCompSpec *appsv1alpha1.ClusterComponentSpec) error { - // If it's not a built-in handler supported by Lorry, LorryContainers are not injected by default. - builtinHandler := getBuiltinActionHandler(synthesizeComp) - if builtinHandler == appsv1alpha1.UnknownBuiltinActionHandler { - return nil - } - - var lorryContainers []corev1.Container - lorryHTTPPort := viper.GetInt32(constant.KBEnvLorryHTTPPort) - lorryGRPCPort := viper.GetInt32(constant.KBEnvLorryGRPCPort) - if synthesizeComp.PodSpec.HostNetwork { - lorryHTTPPort = 51 - lorryGRPCPort = 61 - } - availablePorts, err := getAvailableContainerPorts(synthesizeComp.PodSpec.Containers, []int32{lorryHTTPPort, lorryGRPCPort}) - if err != nil { - reqCtx.Log.Info("get lorry container port failed", "error", err) - return err - } - lorryHTTPPort = availablePorts[0] - lorryGRPCPort = availablePorts[1] - if synthesizeComp.PodSpec.HostNetwork { - if lorryGRPCPort >= 100 || lorryHTTPPort >= 100 { - return fmt.Errorf("port numbers need to be less than 100 when using the host network! "+ - "lorry http port: %d, lorry grpc port: %d", lorryHTTPPort, lorryGRPCPort) - } - } - - container := buildBasicContainer(int(lorryHTTPPort)) - // inject role probe container - var compRoleProbe *appsv1alpha1.Probe - if synthesizeComp.LifecycleActions != nil { - compRoleProbe = synthesizeComp.LifecycleActions.RoleProbe - } - if compRoleProbe != nil { - reqCtx.Log.V(3).Info("lorry", "role probe settings", compRoleProbe) - roleChangedContainer := container.DeepCopy() - buildRoleProbeContainer(roleChangedContainer, compRoleProbe, int(lorryHTTPPort)) - lorryContainers = append(lorryContainers, *roleChangedContainer) - } - - // inject volume protection probe container - if volumeProtectionEnabled(synthesizeComp) { - c := container.DeepCopy() - buildVolumeProtectionProbeContainer(c, int(lorryHTTPPort)) - lorryContainers = append(lorryContainers, *c) - } - - if len(lorryContainers) == 0 { - // need by other action handlers - lorryContainer := container.DeepCopy() - lorryContainers = append(lorryContainers, *lorryContainer) - } - - buildLorryServiceContainer(synthesizeComp, &lorryContainers[0], int(lorryHTTPPort), int(lorryGRPCPort), clusterCompSpec) - adaptLorryIfCustomHandlerDefined(synthesizeComp, &lorryContainers[0], int(lorryHTTPPort), int(lorryGRPCPort)) - - reqCtx.Log.V(1).Info("lorry", "containers", lorryContainers) - synthesizeComp.PodSpec.Containers = append(synthesizeComp.PodSpec.Containers, lorryContainers...) - - return nil -} - -func adaptLorryIfCustomHandlerDefined(synthesizeComp *SynthesizedComponent, lorryContainer *corev1.Container, - lorryHTTPPort, lorryGRPCPort int) { - actionCommands, execImage, containerName := getActionCommandsWithExecImageOrContainerName(synthesizeComp) - if len(actionCommands) == 0 { - return - } - initContainer := buildLorryInitContainer() - synthesizeComp.PodSpec.InitContainers = append(synthesizeComp.PodSpec.InitContainers, *initContainer) - execContainer := getExecContainer(synthesizeComp.PodSpec.Containers, containerName) - if execImage == "" { - if execContainer == nil { - return - } - execImage = execContainer.Image - } - - lorryContainer.Image = execImage - lorryContainer.VolumeMounts = append(lorryContainer.VolumeMounts, corev1.VolumeMount{Name: "kubeblocks", MountPath: "/kubeblocks"}) - - lorryContainer.Command = []string{"/kubeblocks/lorry", - "--port", strconv.Itoa(lorryHTTPPort), - "--grpcport", strconv.Itoa(lorryGRPCPort), - "--config-path", "/kubeblocks/config/lorry/components/", - } - actionJSON, _ := json.Marshal(actionCommands) - lorryContainer.Env = append(lorryContainer.Env, corev1.EnvVar{ - Name: constant.KBEnvActionCommands, - Value: string(actionJSON), - }) - - if execContainer == nil { - return - } - - envSet := sets.New([]string{}...) - for _, env := range lorryContainer.Env { - envSet.Insert(env.Name) - } - - for _, env := range execContainer.Env { - if envSet.Has(env.Name) { - continue - } - lorryContainer.Env = append(lorryContainer.Env, env) - } -} - -func buildBasicContainer(lorryHTTPPort int) *corev1.Container { - return builder.NewContainerBuilder("string"). - SetImage("apecloud-registry.cn-zhangjiakou.cr.aliyuncs.com/apecloud/pause:3.6"). - SetImagePullPolicy(corev1.PullIfNotPresent). - AddCommands("/pause"). - SetStartupProbe(corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt(lorryHTTPPort)}, - }}). - GetObject() -} - -func buildLorryServiceContainer(synthesizeComp *SynthesizedComponent, container *corev1.Container, - lorryHTTPPort, lorryGRPCPort int, clusterCompSpec *appsv1alpha1.ClusterComponentSpec) { - container.Name = constant.LorryContainerName - container.Image = viper.GetString(constant.KBToolsImage) - container.ImagePullPolicy = corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)) - container.Command = []string{"lorry", - "--port", strconv.Itoa(lorryHTTPPort), - "--grpcport", strconv.Itoa(lorryGRPCPort), - } - - container.Ports = []corev1.ContainerPort{ - { - ContainerPort: int32(lorryHTTPPort), - Name: constant.LorryHTTPPortName, - Protocol: "TCP", - }, - { - ContainerPort: int32(lorryGRPCPort), - Name: constant.LorryGRPCPortName, - Protocol: "TCP", - }, - } - - buildLorryEnvs(container, synthesizeComp, clusterCompSpec) - - // set lorry container ports to host network - if synthesizeComp.HostNetwork != nil { - if synthesizeComp.HostNetwork.ContainerPorts == nil { - synthesizeComp.HostNetwork.ContainerPorts = make([]appsv1alpha1.HostNetworkContainerPort, 0) - } - synthesizeComp.HostNetwork.ContainerPorts = append( - synthesizeComp.HostNetwork.ContainerPorts, - appsv1alpha1.HostNetworkContainerPort{ - Container: container.Name, - Ports: []string{constant.LorryHTTPPortName, constant.LorryGRPCPortName}, - }) - } -} - -func buildLorryInitContainer() *corev1.Container { - container := &corev1.Container{} - container.Image = viper.GetString(constant.KBToolsImage) - container.Name = constant.LorryInitContainerName - container.ImagePullPolicy = corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)) - container.Command = []string{"cp", "-r", "/bin/lorry", "/config", "/bin/curl", "/kubeblocks/"} - container.StartupProbe = nil - container.ReadinessProbe = nil - volumeMount := corev1.VolumeMount{Name: "kubeblocks", MountPath: "/kubeblocks"} - container.VolumeMounts = []corev1.VolumeMount{volumeMount} - return container -} - -func buildLorryEnvs(container *corev1.Container, synthesizeComp *SynthesizedComponent, clusterCompSpec *appsv1alpha1.ClusterComponentSpec) { - envs := []corev1.EnvVar{ - // inject the default built-in handler env to lorry container. - { - Name: constant.KBEnvBuiltinHandler, - Value: string(getBuiltinActionHandler(synthesizeComp)), - ValueFrom: nil, - }, - } - - envs = append(envs, buildEnv4DBAccount(synthesizeComp, clusterCompSpec)...) - - mainContainer := getMainContainer(synthesizeComp.PodSpec.Containers) - if mainContainer != nil { - if len(mainContainer.Ports) > 0 { - port := mainContainer.Ports[0] - dbPort := port.ContainerPort - envs = append(envs, corev1.EnvVar{ - Name: constant.KBEnvServicePort, - Value: strconv.Itoa(int(dbPort)), - ValueFrom: nil, - }) - } - - dataVolumeName := dataVolume - for _, v := range synthesizeComp.Volumes { - // TODO(xingran): how to convert needSnapshot to original volumeTypeData ? - if v.NeedSnapshot { - dataVolumeName = v.Name - } - } - for _, volumeMount := range mainContainer.VolumeMounts { - if volumeMount.Name != dataVolumeName { - continue - } - vm := volumeMount.DeepCopy() - container.VolumeMounts = []corev1.VolumeMount{*vm} - envs = append(envs, corev1.EnvVar{ - Name: constant.KBEnvDataPath, - Value: vm.MountPath, - ValueFrom: nil, - }) - } - } - - if volumeProtectionEnabled(synthesizeComp) { - envs = append(envs, buildEnv4VolumeProtection(synthesizeComp)) - } - envs = append(envs, buildEnv4CronJobs(synthesizeComp)...) - - container.Env = append(container.Env, envs...) -} - -func buildRoleProbeContainer(roleChangedContainer *corev1.Container, roleProbe *appsv1alpha1.Probe, probeSvcHTTPPort int) { - roleChangedContainer.Name = constant.RoleProbeContainerName - httpGet := &corev1.HTTPGetAction{} - httpGet.Path = constant.LorryRoleProbePath - httpGet.Port = intstr.FromInt(probeSvcHTTPPort) - probe := &corev1.Probe{} - probe.Exec = nil - probe.HTTPGet = httpGet - probe.PeriodSeconds = roleProbe.PeriodSeconds - probe.TimeoutSeconds = roleProbe.TimeoutSeconds - probe.FailureThreshold = 3 - roleChangedContainer.ReadinessProbe = probe - roleChangedContainer.Env = append(roleChangedContainer.Env, corev1.EnvVar{ - Name: constant.KBEnvRoleProbePeriod, - Value: strconv.Itoa(int(roleProbe.PeriodSeconds)), - }) -} - -func volumeProtectionEnabled(component *SynthesizedComponent) bool { - for _, v := range component.Volumes { - if v.HighWatermark > 0 { - return true - } - } - return false -} - -func buildVolumeProtectionProbeContainer(c *corev1.Container, probeSvcHTTPPort int) { - c.Name = constant.VolumeProtectionProbeContainerName - probe := &corev1.Probe{} - httpGet := &corev1.HTTPGetAction{} - httpGet.Path = constant.LorryVolumeProtectPath - httpGet.Port = intstr.FromInt(probeSvcHTTPPort) - probe.HTTPGet = httpGet - probe.PeriodSeconds = defaultVolumeProtectionProbePeriod - probe.TimeoutSeconds = defaultVolumeProtectionProbeTimeout - probe.FailureThreshold = 3 - c.ReadinessProbe = probe -} - -func buildEnv4DBAccount(synthesizeComp *SynthesizedComponent, clusterCompSpec *appsv1alpha1.ClusterComponentSpec) []corev1.EnvVar { - var ( - secretName string - sysInitAccount *appsv1alpha1.SystemAccount - ) - - for index, sysAccount := range synthesizeComp.SystemAccounts { - // use first init account - if sysAccount.InitAccount { - sysInitAccount = &synthesizeComp.SystemAccounts[index] - break - } - } - - if clusterCompSpec == nil || clusterCompSpec.ComponentDef != "" { - if sysInitAccount != nil { - secretName = constant.GenerateAccountSecretName(synthesizeComp.ClusterName, synthesizeComp.Name, sysInitAccount.Name) - } - } - envs := []corev1.EnvVar{} - if secretName == "" { - return envs - } - - envs = append(envs, - corev1.EnvVar{ - Name: constant.KBEnvServiceUser, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: constant.AccountNameForSecret, - }, - }, - }, - corev1.EnvVar{ - Name: constant.KBEnvServicePassword, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: secretName, - }, - Key: constant.AccountPasswdForSecret, - }, - }, - }) - return envs -} - -func buildEnv4VolumeProtection(synthesizedComp *SynthesizedComponent) corev1.EnvVar { - spec := &appsv1alpha1.VolumeProtectionSpec{} - for i, v := range synthesizedComp.Volumes { - if v.HighWatermark > 0 { - spec.Volumes = append(spec.Volumes, appsv1alpha1.ProtectedVolume{ - Name: v.Name, - HighWatermark: &synthesizedComp.Volumes[i].HighWatermark, - }) - } - } - - value, err := json.Marshal(spec) - if err != nil { - panic(fmt.Sprintf("marshal volume protection spec error: %s", err.Error())) - } - return corev1.EnvVar{ - Name: constant.KBEnvVolumeProtectionSpec, - Value: string(value), - } -} - -func buildEnv4CronJobs(_ *SynthesizedComponent) []corev1.EnvVar { - return nil - // if synthesizeComp.LifecycleActions == nil || synthesizeComp.LifecycleActions.HealthyCheck == nil { - // return nil - // } - // healthyCheck := synthesizeComp.LifecycleActions.HealthyCheck - // healthCheckSetting := make(map[string]string) - // healthCheckSetting["periodSeconds"] = strconv.Itoa(int(healthyCheck.PeriodSeconds)) - // healthCheckSetting["timeoutSeconds"] = strconv.Itoa(int(healthyCheck.TimeoutSeconds)) - // healthCheckSetting["failureThreshold"] = strconv.Itoa(int(healthyCheck.FailureThreshold)) - // healthCheckSetting["successThreshold"] = strconv.Itoa(int(healthyCheck.SuccessThreshold)) - // cronJobs := make(map[string]map[string]string) - // cronJobs["healthyCheck"] = healthCheckSetting - - // jsonStr, _ := json.Marshal(cronJobs) - // return []corev1.EnvVar{ - // { - // Name: constant.KBEnvCronJobs, - // Value: string(jsonStr), - // }, - // } -} - -// getBuiltinActionHandler gets the built-in handler. -// The BuiltinActionHandler within the same synthesizeComp LifecycleActions should be consistent, we can take any one of them. -func getBuiltinActionHandler(synthesizeComp *SynthesizedComponent) appsv1alpha1.BuiltinActionHandlerType { - if synthesizeComp.LifecycleActions == nil { - return appsv1alpha1.UnknownBuiltinActionHandler - } - - if synthesizeComp.LifecycleActions.RoleProbe != nil { - if synthesizeComp.LifecycleActions.RoleProbe.BuiltinHandler != nil && - *synthesizeComp.LifecycleActions.RoleProbe.BuiltinHandler != appsv1alpha1.UnknownBuiltinActionHandler { - return *synthesizeComp.LifecycleActions.RoleProbe.BuiltinHandler - } else { - return appsv1alpha1.CustomActionHandler - } - } - - actions := []*appsv1alpha1.LifecycleActionHandler{ - synthesizeComp.LifecycleActions.PostProvision, - synthesizeComp.LifecycleActions.PreTerminate, - synthesizeComp.LifecycleActions.MemberJoin, - synthesizeComp.LifecycleActions.MemberLeave, - synthesizeComp.LifecycleActions.Readonly, - synthesizeComp.LifecycleActions.Readwrite, - synthesizeComp.LifecycleActions.DataDump, - synthesizeComp.LifecycleActions.DataLoad, - synthesizeComp.LifecycleActions.Reconfigure, - synthesizeComp.LifecycleActions.AccountProvision, - } - - hasAction := false - for _, action := range actions { - if action != nil { - hasAction = true - if action.BuiltinHandler != nil { - return *action.BuiltinHandler - } - } - } - if hasAction { - return appsv1alpha1.CustomActionHandler - } - return appsv1alpha1.UnknownBuiltinActionHandler -} - -func getActionCommandsWithExecImageOrContainerName(synthesizeComp *SynthesizedComponent) (map[string][]string, string, string) { - if synthesizeComp.LifecycleActions == nil { - return nil, "", "" - } - - actions := map[string]*appsv1alpha1.LifecycleActionHandler{ - constant.PostProvisionAction: synthesizeComp.LifecycleActions.PostProvision, - constant.PreTerminateAction: synthesizeComp.LifecycleActions.PreTerminate, - constant.MemberJoinAction: synthesizeComp.LifecycleActions.MemberJoin, - constant.MemberLeaveAction: synthesizeComp.LifecycleActions.MemberLeave, - constant.ReadonlyAction: synthesizeComp.LifecycleActions.Readonly, - constant.ReadWriteAction: synthesizeComp.LifecycleActions.Readwrite, - constant.DataDumpAction: synthesizeComp.LifecycleActions.DataDump, - constant.DataLoadAction: synthesizeComp.LifecycleActions.DataLoad, - constant.AccountProvisionAction: synthesizeComp.LifecycleActions.AccountProvision, - // "reconfigure": synthesizeComp.LifecycleActions.Reconfigure, - } - - if synthesizeComp.LifecycleActions.RoleProbe != nil && synthesizeComp.LifecycleActions.RoleProbe.Exec != nil { - actions[constant.RoleProbeAction] = &appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &synthesizeComp.LifecycleActions.RoleProbe.Action, - } - } - - var toolImage string - var containerName string - actionCommands := map[string][]string{} - for action, handler := range actions { - if handler != nil && handler.CustomHandler != nil && handler.CustomHandler.Exec != nil { - actionCommands[action] = append(handler.CustomHandler.Exec.Command, handler.CustomHandler.Exec.Args...) - if handler.CustomHandler.Exec.Image != "" { - toolImage = handler.CustomHandler.Exec.Image - } - if handler.CustomHandler.Exec.Container != "" { - containerName = handler.CustomHandler.Exec.Container - } - } - } - - return actionCommands, toolImage, containerName -} - -func getExecContainer(containers []corev1.Container, name string) *corev1.Container { - if name == "" { - return getMainContainer(containers) - } - - for i := range containers { - if containers[i].Name == name { - return &containers[i] - } - } - return nil -} - -func getMainContainer(containers []corev1.Container) *corev1.Container { - if len(containers) > 0 { - return &containers[0] - } - return nil -} - -// get available container ports, increased by one if conflict with exist ports -// util no conflicts. -func getAvailableContainerPorts(containers []corev1.Container, containerPorts []int32) ([]int32, error) { - set, err := getAllContainerPorts(containers) - if err != nil { - return nil, err - } - - iterAvailPort := func(p int32) (int32, error) { - // The TCP/IP port numbers below 1024 are privileged ports, which are special - // in that normal users are not allowed to run servers on them. - // Ports below 1024 can be allocated, as the port manager will automatically reallocate ports under 100. - if p < minAvailPort || p > maxAvailPort { - p = minAvailPort - } - sentinel := p - for { - if _, ok := set[p]; !ok { - set[p] = true - return p, nil - } - p++ - if p == sentinel { - return -1, errors.New("no available port for container") - } - if p > maxAvailPort { - p = minAvailPort - } - } - } - - for i, p := range containerPorts { - if containerPorts[i], err = iterAvailPort(p); err != nil { - return []int32{}, err - } - } - return containerPorts, nil -} - -func getAllContainerPorts(containers []corev1.Container) (map[int32]bool, error) { - set := map[int32]bool{} - for _, container := range containers { - for _, v := range container.Ports { - _, ok := set[v.ContainerPort] - if ok { - return nil, fmt.Errorf("containerPorts conflict: [%+v]", v.ContainerPort) - } - set[v.ContainerPort] = true - } - } - return set, nil -} diff --git a/pkg/controller/component/lorry_utils_test.go b/pkg/controller/component/lorry_utils_test.go deleted file mode 100644 index ab6686f35da..00000000000 --- a/pkg/controller/component/lorry_utils_test.go +++ /dev/null @@ -1,279 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package component - -import ( - "encoding/json" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - corev1 "k8s.io/api/core/v1" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var _ = Describe("Lorry Utils", func() { - - Context("build probe containers", func() { - var container *corev1.Container - var component *SynthesizedComponent - var lorryHTTPPort int - var lorryGRPCPort int - - BeforeEach(func() { - lorryHTTPPort = 3501 - lorryGRPCPort = 50001 - - component = &SynthesizedComponent{} - component.ComponentServices = append(component.ComponentServices, appsv1alpha1.ComponentService{ - Service: appsv1alpha1.Service{ - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{{ - Protocol: corev1.ProtocolTCP, - Port: 3306, - }}, - }, - }, - }) - component.Roles = []appsv1alpha1.ReplicaRole{ - { - Name: "leader", - Serviceable: true, - Writable: true, - Votable: true, - }, - { - Name: "follower", - Serviceable: true, - Writable: false, - Votable: true, - }, - { - Name: "learner", - Serviceable: true, - Writable: false, - Votable: false, - }, - } - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - RoleProbe: &appsv1alpha1.Probe{}, - } - component.PodSpec = &corev1.PodSpec{ - Containers: []corev1.Container{}, - } - - container = buildBasicContainer(lorryHTTPPort) - }) - - It("build role probe containers", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - defaultBuiltInHandler := appsv1alpha1.MySQLBuiltinActionHandler - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - RoleProbe: &appsv1alpha1.Probe{ - BuiltinHandler: &defaultBuiltInHandler, - }, - } - Expect(buildLorryContainers(reqCtx, component, nil)).Should(Succeed()) - Expect(component.PodSpec.Containers).Should(HaveLen(1)) - Expect(component.PodSpec.InitContainers).Should(HaveLen(0)) - Expect(component.PodSpec.Containers[0].Name).Should(Equal(constant.LorryContainerName)) - }) - - It("should build role service container", func() { - buildLorryServiceContainer(component, container, lorryHTTPPort, lorryGRPCPort, nil) - Expect(container.Command).ShouldNot(BeEmpty()) - Expect(container.Name).Should(Equal(constant.LorryContainerName)) - Expect(len(container.Ports)).Should(Equal(2)) - }) - - It("build lorry container if any builtinhandler specified", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - // all other services are disabled - defaultBuiltInHandler := appsv1alpha1.MySQLBuiltinActionHandler - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - MemberJoin: &appsv1alpha1.LifecycleActionHandler{ - BuiltinHandler: &defaultBuiltInHandler, - }, - } - Expect(buildLorryContainers(reqCtx, component, nil)).Should(Succeed()) - Expect(component.PodSpec.Containers).Should(HaveLen(1)) - Expect(component.PodSpec.InitContainers).Should(HaveLen(0)) - Expect(component.PodSpec.Containers[0].Name).Should(Equal(constant.LorryContainerName)) - }) - - It("build lorry container if any exec specified", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - image := "testimage" - // all other services are disabled - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - MemberJoin: &appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &appsv1alpha1.Action{ - Exec: &appsv1alpha1.ExecAction{ - Image: image, - Command: []string{"test"}, - }, - }, - }, - } - Expect(buildLorryContainers(reqCtx, component, nil)).Should(Succeed()) - Expect(component.PodSpec.Containers).Should(HaveLen(1)) - Expect(component.PodSpec.InitContainers).Should(HaveLen(1)) - Expect(component.PodSpec.Containers[0].Image).Should(Equal(image)) - Expect(component.PodSpec.Containers[0].Name).Should(Equal(constant.LorryContainerName)) - }) - - It("build volume protection probe container without RBAC", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - component.Volumes = []appsv1alpha1.ComponentVolume{ - { - Name: "volume-001", - HighWatermark: 90, - }, - { - Name: "volume-002", - HighWatermark: 0, - }, - } - defaultBuiltInHandler := appsv1alpha1.MySQLBuiltinActionHandler - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - RoleProbe: &appsv1alpha1.Probe{ - BuiltinHandler: &defaultBuiltInHandler, - }, - } - Expect(buildLorryContainers(reqCtx, component, nil)).Should(Succeed()) - Expect(component.PodSpec.Containers).Should(HaveLen(2)) - Expect(component.PodSpec.Containers[0].Name).Should(Equal(constant.LorryContainerName)) - Expect(component.PodSpec.Containers[1].Name).Should(Equal(constant.VolumeProtectionProbeContainerName)) - }) - - It("build volume protection probe container with RBAC", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - component.Volumes = []appsv1alpha1.ComponentVolume{ - { - Name: "volume-001", - HighWatermark: 90, - }, - { - Name: "volume-002", - HighWatermark: 0, - }, - } - defaultBuiltInHandler := appsv1alpha1.MySQLBuiltinActionHandler - component.LifecycleActions = &appsv1alpha1.ComponentLifecycleActions{ - RoleProbe: &appsv1alpha1.Probe{ - BuiltinHandler: &defaultBuiltInHandler, - }, - } - viper.SetDefault(constant.EnableRBACManager, true) - Expect(buildLorryContainers(reqCtx, component, nil)).Should(Succeed()) - Expect(component.PodSpec.Containers).Should(HaveLen(2)) - spec := &appsv1alpha1.VolumeProtectionSpec{} - for _, e := range component.PodSpec.Containers[0].Env { - if e.Name == constant.KBEnvVolumeProtectionSpec { - Expect(json.Unmarshal([]byte(e.Value), spec)).Should(Succeed()) - break - } - } - Expect(spec.Volumes).Should(HaveLen(1)) - Expect(*spec.Volumes[0].HighWatermark).Should(Equal(90)) - }) - }) -}) - -func TestGetAvailableContainerPorts(t *testing.T) { - var containers []corev1.Container - - tests := []struct { - inputPort int32 - outputPort int32 - }{{ - inputPort: 80, // 80 is a privileged port - outputPort: 80, - }, { - inputPort: 65536, // 65536 is an invalid port - outputPort: minAvailPort, - }, { - inputPort: 3306, // 3306 is a qualified port - outputPort: 3306, - }} - - for _, test := range tests { - containerPorts := []int32{test.inputPort} - foundPorts, err := getAvailableContainerPorts(containers, containerPorts) - if err != nil { - t.Error("expect getAvailableContainerPorts success") - } - if len(foundPorts) != 1 || foundPorts[0] != test.outputPort { - t.Error("expect getAvailableContainerPorts returns", test.outputPort) - } - } -} - -func TestGetAvailableContainerPortsPartlyOccupied(t *testing.T) { - var containers []corev1.Container - - destPort := 3306 - for p := minAvailPort; p < destPort; p++ { - containers = append(containers, corev1.Container{Ports: []corev1.ContainerPort{{ContainerPort: int32(p)}}}) - } - - containerPorts := []int32{minAvailPort + 1} - foundPorts, err := getAvailableContainerPorts(containers, containerPorts) - if err != nil { - t.Error("expect getAvailableContainerPorts success") - } - if len(foundPorts) != 1 || foundPorts[0] != int32(destPort) { - t.Error("expect getAvailableContainerPorts returns 3306") - } -} - -func TestGetAvailableContainerPortsFullyOccupied(t *testing.T) { - var containers []corev1.Container - - for p := minAvailPort; p <= maxAvailPort; p++ { - containers = append(containers, corev1.Container{Ports: []corev1.ContainerPort{{ContainerPort: int32(p)}}}) - } - - containerPorts := []int32{3306} - _, err := getAvailableContainerPorts(containers, containerPorts) - if err == nil { - t.Error("expect getAvailableContainerPorts return error") - } -} diff --git a/pkg/controller/component/synthesize_component.go b/pkg/controller/component/synthesize_component.go index 01aad7d4f6e..a067feca4be 100644 --- a/pkg/controller/component/synthesize_component.go +++ b/pkg/controller/component/synthesize_component.go @@ -554,8 +554,6 @@ func overrideConfigTemplates(synthesizedComp *SynthesizedComponent, comp *appsv1 // buildServiceAccountName builds serviceAccountName for component and podSpec. func buildServiceAccountName(synthesizeComp *SynthesizedComponent) { - // lorry container requires a service account with adequate privileges. - // If lorry required and the serviceAccountName is not set, a default serviceAccountName will be assigned. if synthesizeComp.ServiceAccountName != "" { synthesizeComp.PodSpec.ServiceAccountName = synthesizeComp.ServiceAccountName return diff --git a/pkg/controller/instanceset/object_builder.go b/pkg/controller/instanceset/object_builder.go index 6ba798462a6..ee827ce8292 100644 --- a/pkg/controller/instanceset/object_builder.go +++ b/pkg/controller/instanceset/object_builder.go @@ -25,7 +25,6 @@ import ( "strconv" "strings" - "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -189,6 +188,12 @@ func injectRoleProbeBaseContainer(its *workloads.InstanceSet, template *corev1.P if roleProbe == nil { return } + + // already has role probe container, for test purpose + if _, c := controllerutil.GetContainerByName(template.Spec.Containers, roleProbeContainerName); c != nil { + return + } + credential := its.Spec.Credential image := viper.GetString(constant.KBToolsImage) probeHTTPPort := viper.GetInt("ROLE_SERVICE_HTTP_PORT") @@ -300,15 +305,6 @@ func injectRoleProbeBaseContainer(its *workloads.InstanceSet, template *corev1.P }, ) - characterType := "custom" - if roleProbe.BuiltinHandler != nil { - characterType = *roleProbe.BuiltinHandler - } - env = append(env, corev1.EnvVar{ - Name: constant.KBEnvCharacterType, - Value: characterType, - }) - readinessProbe := &corev1.Probe{ InitialDelaySeconds: roleProbe.InitialDelaySeconds, TimeoutSeconds: roleProbe.TimeoutSeconds, @@ -326,43 +322,6 @@ func injectRoleProbeBaseContainer(its *workloads.InstanceSet, template *corev1.P }, } - tryToGetLorryGrpcPort := func(container *corev1.Container) *corev1.ContainerPort { - for i, port := range container.Ports { - if port.Name == constant.LorryGRPCPortName { - return &container.Ports[i] - } - } - return nil - } - - // if role probe container exists, update the readiness probe, env and serving container port - if container := controllerutil.GetLorryContainer(template.Spec.Containers); container != nil { - if roleProbe.RoleUpdateMechanism == workloads.ReadinessProbeEventUpdate || - // for compatibility when upgrading from 0.7 to 0.8 - (container.ReadinessProbe != nil && container.ReadinessProbe.HTTPGet != nil && - strings.HasPrefix(container.ReadinessProbe.HTTPGet.Path, "/v1.0/bindings")) { - port := tryToGetLorryGrpcPort(container) - if port != nil && port.ContainerPort != int32(probeGRPCPort) { - readinessProbe.Exec.Command = []string{ - grpcHealthProbeBinaryPath, - fmt.Sprintf(grpcHealthProbeArgsFormat, port.ContainerPort), - } - } - container.ReadinessProbe = readinessProbe - } - - for _, e := range env { - if slices.IndexFunc(container.Env, func(v corev1.EnvVar) bool { - return v.Name == e.Name || e.Name == constant.KBEnvServiceUser || - e.Name == constant.KBEnvServicePassword || e.Name == usernameCredentialVarName || e.Name == passwordCredentialVarName - }) >= 0 { - continue - } - container.Env = append(container.Env, e) - } - return - } - // if role probe container doesn't exist, create a new one // build container container := builder.NewContainerBuilder(roleProbeContainerName). @@ -546,11 +505,5 @@ func buildEnvConfigData(its workloads.InstanceSet) (map[string]string, error) { generateMemberEnv(prefixWithCompDefName) envData[prefixWithCompDefName+"CLUSTER_UID"] = uid - lorryHTTPPort, err := controllerutil.GetLorryHTTPPortFromContainers(its.Spec.Template.Spec.Containers) - if err == nil { - envData[constant.KBEnvLorryHTTPPort] = strconv.Itoa(int(lorryHTTPPort)) - - } - return envData, nil } diff --git a/pkg/controller/instanceset/object_builder_test.go b/pkg/controller/instanceset/object_builder_test.go index 4787405af50..8953c57ed97 100644 --- a/pkg/controller/instanceset/object_builder_test.go +++ b/pkg/controller/instanceset/object_builder_test.go @@ -128,18 +128,18 @@ var _ = Describe("object generation transformer test.", func() { }) Context("injectRoleProbeBaseContainer function", func() { - It("should reuse container 'kb-checkrole' if exists", func() { + It("should reuse container 'kb-role-probe' if exists", func() { templateCopy := template.DeepCopy() templateCopy.Spec.Containers = append(templateCopy.Spec.Containers, corev1.Container{ - Name: constant.RoleProbeContainerName, + Name: roleProbeContainerName, Image: "bar", Ports: []corev1.ContainerPort{ { - Name: constant.LorryGRPCPortName, + Name: roleProbeGRPCPortName, ContainerPort: defaultRoleProbeGRPCPort, }, { - Name: constant.LorryHTTPPortName, + Name: roleProbeDaemonPortName, ContainerPort: defaultRoleProbeDaemonPort, }, }, @@ -151,19 +151,19 @@ var _ = Describe("object generation transformer test.", func() { Expect(probeContainer.Ports[0].ContainerPort).Should(BeElementOf([]int32{int32(defaultRoleProbeGRPCPort), int32(defaultRoleProbeDaemonPort)})) }) - It("should not use default grpcPort in case of 'lorry-grpc-port' existence", func() { + It("should not use default grpcPort in case of 'probe-grpc-port' existence", func() { its.Spec.RoleProbe.RoleUpdateMechanism = workloads.ReadinessProbeEventUpdate templateCopy := template.DeepCopy() templateCopy.Spec.Containers = append(templateCopy.Spec.Containers, corev1.Container{ - Name: constant.RoleProbeContainerName, + Name: roleProbeContainerName, Image: "bar", Ports: []corev1.ContainerPort{ { - Name: constant.LorryGRPCPortName, + Name: roleProbeGRPCPortName, ContainerPort: 9555, }, { - Name: constant.LorryHTTPPortName, + Name: roleProbeDaemonPortName, ContainerPort: defaultRoleProbeDaemonPort, }, }, @@ -179,15 +179,15 @@ var _ = Describe("object generation transformer test.", func() { its.Spec.RoleProbe.RoleUpdateMechanism = workloads.ReadinessProbeEventUpdate templateCopy := template.DeepCopy() templateCopy.Spec.Containers = append(templateCopy.Spec.Containers, corev1.Container{ - Name: constant.RoleProbeContainerName, + Name: roleProbeContainerName, Image: "bar", Ports: []corev1.ContainerPort{ { - Name: constant.LorryGRPCPortName, + Name: roleProbeGRPCPortName, ContainerPort: defaultRoleProbeGRPCPort, }, { - Name: constant.LorryHTTPPortName, + Name: roleProbeDaemonPortName, ContainerPort: defaultRoleProbeDaemonPort, }, }, @@ -203,15 +203,15 @@ var _ = Describe("object generation transformer test.", func() { its.Spec.RoleProbe.RoleUpdateMechanism = workloads.ReadinessProbeEventUpdate templateCopy := template.DeepCopy() templateCopy.Spec.Containers = append(templateCopy.Spec.Containers, corev1.Container{ - Name: constant.RoleProbeContainerName, + Name: roleProbeContainerName, Image: "bar", Ports: []corev1.ContainerPort{ { - Name: constant.LorryGRPCPortName, + Name: roleProbeGRPCPortName, ContainerPort: defaultRoleProbeGRPCPort, }, { - Name: constant.LorryHTTPPortName, + Name: roleProbeDaemonPortName, ContainerPort: defaultRoleProbeDaemonPort, }, }, diff --git a/pkg/controller/instanceset/pod_role_event_handler.go b/pkg/controller/instanceset/pod_role_event_handler.go index 5f32e03cf46..28df2bbd9c0 100644 --- a/pkg/controller/instanceset/pod_role_event_handler.go +++ b/pkg/controller/instanceset/pod_role_event_handler.go @@ -39,7 +39,6 @@ import ( "github.com/apecloud/kubeblocks/pkg/controller/multicluster" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" "github.com/apecloud/kubeblocks/pkg/kbagent/proto" - "github.com/apecloud/kubeblocks/pkg/lorry/util" ) type PodRoleEventHandler struct{} @@ -62,6 +61,11 @@ type probeMessage struct { const ( // roleChangedAnnotKey is used to mark the role change event has been handled. roleChangedAnnotKey = "role.kubeblocks.io/event-handled" + + // TODO(v1.0): remove this later. + checkRoleOperation = "checkRole" + legacyEventFieldPath = "spec.containers{kb-checkrole}" + lorryEventFieldPath = "spec.containers{lorry}" ) var roleMessageRegex = regexp.MustCompile(`Readiness probe failed: .*({.*})`) @@ -70,8 +74,8 @@ func (h *PodRoleEventHandler) Handle(cli client.Client, reqCtx intctrlutil.Reque // HACK: to support kb-agent probe event event = h.transformKBAgentProbeEvent(reqCtx.Log, event) - filePaths := []string{readinessProbeEventFieldPath, util.LegacyEventFieldPath, util.LorryEventFieldPath} - if !slices.Contains(filePaths, event.InvolvedObject.FieldPath) || event.Reason != string(util.CheckRoleOperation) { + filePaths := []string{readinessProbeEventFieldPath, legacyEventFieldPath, lorryEventFieldPath} + if !slices.Contains(filePaths, event.InvolvedObject.FieldPath) || event.Reason != checkRoleOperation { return nil } var ( @@ -117,8 +121,8 @@ func (h *PodRoleEventHandler) transformKBAgentProbeEvent(logger logr.Logger, eve } data, _ := json.Marshal(message) - event.InvolvedObject.FieldPath = util.LorryEventFieldPath - event.Reason = string(util.CheckRoleOperation) + event.InvolvedObject.FieldPath = lorryEventFieldPath + event.Reason = checkRoleOperation event.Message = string(data) return event } diff --git a/pkg/controller/instanceset/pod_role_event_handler_test.go b/pkg/controller/instanceset/pod_role_event_handler_test.go index 42e3539fe05..369b9f4394e 100644 --- a/pkg/controller/instanceset/pod_role_event_handler_test.go +++ b/pkg/controller/instanceset/pod_role_event_handler_test.go @@ -35,7 +35,6 @@ import ( "github.com/apecloud/kubeblocks/pkg/constant" "github.com/apecloud/kubeblocks/pkg/controller/builder" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" - "github.com/apecloud/kubeblocks/pkg/lorry/util" ) var _ = Describe("pod role label event handler test", func() { @@ -66,7 +65,7 @@ var _ = Describe("pod role label event handler test", func() { message := fmt.Sprintf("Readiness probe failed: error: health rpc failed: rpc error: code = Unknown desc = {\"event\":\"Success\",\"originalRole\":\"\",\"role\":\"%s\"}", role.Name) event := builder.NewEventBuilder(namespace, "foo"). SetInvolvedObject(objectRef). - SetReason(string(util.CheckRoleOperation)). + SetReason(checkRoleOperation). SetMessage(message). GetObject() @@ -115,7 +114,7 @@ var _ = Describe("pod role label event handler test", func() { event = builder.NewEventBuilder(namespace, "foo"). SetInvolvedObject(objectRef). SetMessage(message). - SetReason(string(util.CheckRoleOperation)). + SetReason(checkRoleOperation). GetObject() k8sMock.EXPECT(). Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). diff --git a/pkg/controller/instanceset/revision_util_test.go b/pkg/controller/instanceset/revision_util_test.go index 07b8545b7b3..93906594371 100644 --- a/pkg/controller/instanceset/revision_util_test.go +++ b/pkg/controller/instanceset/revision_util_test.go @@ -92,7 +92,6 @@ var _ = Describe("revision util test", func() { "podManagementPolicy": "Parallel", "replicas": 1, "roleProbe": { - "builtinHandlerName": "redis", "failureThreshold": 2, "initialDelaySeconds": 0, "periodSeconds": 2, @@ -586,7 +585,7 @@ var _ = Describe("revision util test", func() { }, { "command": [ - "lorry", + "role-probe", "--port", "3501", "--grpcport", @@ -781,12 +780,12 @@ var _ = Describe("revision util test", func() { "ports": [ { "containerPort": 3501, - "name": "lorry-http-port", + "name": "http-port", "protocol": "TCP" }, { "containerPort": 50001, - "name": "lorry-grpc-port", + "name": "grpc-port", "protocol": "TCP" } ], @@ -962,7 +961,7 @@ var _ = Describe("revision util test", func() { Expect(err).Should(Succeed()) cr, err := NewRevision(its) Expect(err).Should(Succeed()) - Expect(cr.Name).Should(Equal("redis-test-redis-59996f5569")) + Expect(cr.Name).Should(Equal("redis-test-redis-56666f656d")) }) }) diff --git a/pkg/controller/instanceset/types.go b/pkg/controller/instanceset/types.go index 00fe3a48fc9..f47adac5e3a 100644 --- a/pkg/controller/instanceset/types.go +++ b/pkg/controller/instanceset/types.go @@ -41,8 +41,8 @@ const ( shell2httpServePath = "/role" defaultRoleProbeDaemonPort = 7373 defaultRoleProbeGRPCPort = 50101 + roleProbeDaemonPortName = "probe-port" roleProbeGRPCPortName = "probe-grpc-port" - httpRoleProbePath = "/v1.0/checkrole" grpcHealthProbeBinaryPath = "/bin/grpc_health_probe" grpcHealthProbeArgsFormat = "-addr=:%d" defaultActionImage = "busybox:1.35" @@ -50,8 +50,6 @@ const ( passwordCredentialVarName = "KB_RSM_PASSWORD" servicePortVarName = "KB_RSM_SERVICE_PORT" actionSvcListVarName = "KB_RSM_ACTION_SVC_LIST" - leaderHostVarName = "KB_RSM_LEADER_HOST" - targetHostVarName = "KB_RSM_TARGET_HOST" RoleUpdateMechanismVarName = "KB_RSM_ROLE_UPDATE_MECHANISM" roleProbeTimeoutVarName = "KB_RSM_ROLE_PROBE_TIMEOUT" readinessProbeEventFieldPath = "spec.containers{" + roleProbeContainerName + "}" diff --git a/pkg/controllerutil/pod_utils.go b/pkg/controllerutil/pod_utils.go index ec94ce55269..9d14d8cc64e 100644 --- a/pkg/controllerutil/pod_utils.go +++ b/pkg/controllerutil/pod_utils.go @@ -346,44 +346,6 @@ func GetPortByName(pod corev1.Pod, cname, pname string) (int32, error) { return 0, fmt.Errorf("port %s not found", pname) } -func GetLorryGRPCPort(pod *corev1.Pod) (int32, error) { - return GetLorryGRPCPortFromContainers(pod.Spec.Containers) -} - -func GetLorryGRPCPortFromContainers(containers []corev1.Container) (int32, error) { - return GetPortByPortName(containers, constant.LorryGRPCPortName) -} - -func GetLorryHTTPPort(pod *corev1.Pod) (int32, error) { - return GetLorryHTTPPortFromContainers(pod.Spec.Containers) -} - -func GetLorryHTTPPortFromContainers(containers []corev1.Container) (int32, error) { - return GetPortByPortName(containers, constant.LorryHTTPPortName) -} - -// GetLorryContainerName gets the lorry container from pod -func GetLorryContainerName(pod *corev1.Pod) (string, error) { - container := GetLorryContainer(pod.Spec.Containers) - if container != nil { - return container.Name, nil - } - return "", fmt.Errorf("lorry container not found") -} - -func GetLorryContainer(containers []corev1.Container) *corev1.Container { - var container *corev1.Container - for i := range containers { - container = &containers[i] - for _, port := range container.Ports { - if port.Name == constant.LorryHTTPPortName { - return container - } - } - } - return nil -} - // PodIsReadyWithLabel checks if pod is ready for ConsensusSet/ReplicationSet component, // it will be available when the pod is ready and labeled with role. func PodIsReadyWithLabel(pod corev1.Pod) bool { diff --git a/pkg/kbagent/util/event.go b/pkg/kbagent/util/event.go index 9a8b78d89a9..7107e55b290 100644 --- a/pkg/kbagent/util/event.go +++ b/pkg/kbagent/util/event.go @@ -53,6 +53,7 @@ func SendEventWithMessage(logger *logr.Logger, reason string, message string) { } func createEvent(reason string, message string) *corev1.Event { + // TODO(v1.0): pod variables podName := os.Getenv(constant.KBEnvPodName) podUID := os.Getenv(constant.KBEnvPodUID) nodeName := os.Getenv(constant.KBEnvNodeName) diff --git a/pkg/lorry/README.md b/pkg/lorry/README.md deleted file mode 100644 index d4a6dec2eb5..00000000000 --- a/pkg/lorry/README.md +++ /dev/null @@ -1,49 +0,0 @@ -

    Probe

    - -Probe is a health check service that can do health checking for multi DB engines, running as a sidecar in cluster pods. - - -# 1. Introduction - -Probe is capable of doing multi checks, include running check/status check/role changed check, serviced through HTTP API. we use kubelet readinessprobe to config and enable probe in kubeblocks. - - -# 2. Getting Started - -You can get started with Probe, by any of the following methods: -* Download the release for your platform from github.com/apecloud/kubeblocks/release -* Use the available Probe docker image `docker run -it apecloud/kubeblocks` -* Build `probe` from sources - -## 2.1 Build - -Compiler `Go 1.20+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. - -Use `go build` to build and produce the `probe` binary file. The executable is produced under current directory. - -```shell -$ cd kubeblocks/cmd/probe -$ go build -o probe main.go -``` -## 2.2 Configure - -Probe read some configurations from env variables, Now there are: -* KB_CHECK_FAILED_THRESHOLD: The number of failed continuous checks aggregated in one event, for preventing events flooding. minimum 10, maximum 60 and default 10. -* KB_ROLE_UNCHANGED_THRESHOLD: The count of consecutive role unchanged checks to trigger one event. minimum 10, maximum 60 and default 60. -* KB_SERVICE_PORT: The port of service to probe, eg. 3306. -* KB_SERVICE_USER: The user name of service used to connect, eg. root. -* KB_SERVICE_PASSWORD: The user password of service used to connect. -* KB_SERVICE_ROLES: The mapping of roles and accessmode, eg. {"follower":"Readonly","leader":"ReadWrite"}. - -## 2.3 Run - -You can run the following command to start Probe once built - -```shell -$ probe --config-path config/probe --port 7373 -``` - - -# 3. License - -Probe is under the AGPL 3.0 license. See the [LICENSE](../LICENSE) file for details. diff --git a/pkg/lorry/client/client.go b/pkg/lorry/client/client.go deleted file mode 100644 index 1308dabaa4b..00000000000 --- a/pkg/lorry/client/client.go +++ /dev/null @@ -1,271 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import ( - "context" - "errors" - "net/http" - - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/rest" - - . "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -// HACK: for unit test only. -var mockClient Client -var mockClientError error - -func SetMockClient(cli Client, err error) { - mockClient = cli - mockClientError = err -} - -func UnsetMockClient() { - mockClient = nil - mockClientError = nil -} - -func GetMockClient() Client { - return mockClient -} - -func NewClient(pod corev1.Pod) (Client, error) { - if mockClient != nil || mockClientError != nil { - return mockClient, mockClientError - } - - _, err := rest.InClusterConfig() - if err != nil { - // As the service does not run as a pod in the Kubernetes cluster, - // it is unable to call the lorry service running as a pod using the pod's IP address. - // In this scenario, it is recommended to use an k8s exec client instead. - execClient, err := NewK8sExecClientWithPod(nil, &pod) - if err != nil { - return nil, err - } - if execClient != nil { - return execClient, nil - } - return nil, nil - } - - httpClient, err := NewHTTPClientWithPod(&pod) - if err != nil { - return nil, err - } - if httpClient != nil { - return httpClient, nil - } - - // return Client as nil explicitly to indicate that Client interface is nil, - // or Client will be a non-nil interface value even newclient returns nil. - return nil, nil -} - -type Requester interface { - Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) -} -type lorryClient struct { - requester Requester -} - -func (cli *lorryClient) GetRole(ctx context.Context) (string, error) { - resp, err := cli.Request(ctx, string(GetRoleOperation), http.MethodGet, nil) - if err != nil { - return "", err - } - - role, ok := resp["role"] - if !ok { - return "", nil - } - - return role.(string), nil -} - -func (cli *lorryClient) CreateUser(ctx context.Context, userName, password, roleName, statement string) error { - parameters := map[string]any{ - "userName": userName, - "password": password, - } - if roleName != "" { - parameters["roleName"] = roleName - } - - if statement != "" { - parameters["statement"] = statement - } - - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(CreateUserOp), http.MethodPost, req) - return err -} - -func (cli *lorryClient) DeleteUser(ctx context.Context, userName string) error { - parameters := map[string]any{ - "userName": userName, - } - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(DeleteUserOp), http.MethodPost, req) - return err -} - -func (cli *lorryClient) DescribeUser(ctx context.Context, userName string) (map[string]any, error) { - parameters := map[string]any{ - "userName": userName, - } - req := map[string]any{"parameters": parameters} - resp, err := cli.Request(ctx, string(DescribeUserOp), http.MethodGet, req) - if err != nil { - return nil, err - } - user, ok := resp["user"] - if !ok || user == nil { - return nil, nil - } - - return user.(map[string]any), nil -} - -func (cli *lorryClient) GrantUserRole(ctx context.Context, userName, roleName string) error { - parameters := map[string]any{ - "userName": userName, - "roleName": roleName, - } - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(GrantUserRoleOp), http.MethodPost, req) - return err -} - -func (cli *lorryClient) RevokeUserRole(ctx context.Context, userName, roleName string) error { - parameters := map[string]any{ - "userName": userName, - "roleName": roleName, - } - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(RevokeUserRoleOp), http.MethodPost, req) - return err -} - -func (cli *lorryClient) Switchover(ctx context.Context, primary, candidate string, force bool) error { - parameters := map[string]any{ - "primary": primary, - "candidate": candidate, - "force": force, - } - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(SwitchoverOperation), http.MethodPost, req) - return err -} - -// ListUsers lists all normal users created -func (cli *lorryClient) ListUsers(ctx context.Context) ([]map[string]any, error) { - resp, err := cli.Request(ctx, string(ListUsersOp), http.MethodGet, nil) - if err != nil { - return nil, err - } - users, ok := resp["users"] - if !ok { - return nil, nil - } - return convertToArrayOfMap(users) -} - -// ListSystemAccounts lists all system accounts created -func (cli *lorryClient) ListSystemAccounts(ctx context.Context) ([]map[string]any, error) { - resp, err := cli.Request(ctx, string(ListSystemAccountsOp), http.MethodGet, nil) - if err != nil { - return nil, err - } - systemAccounts, ok := resp["systemAccounts"] - if !ok { - return nil, nil - } - return convertToArrayOfMap(systemAccounts) -} - -// JoinMember sends a join member operation request to Lorry, located on the target pod that is about to join. -func (cli *lorryClient) JoinMember(ctx context.Context) error { - _, err := cli.Request(ctx, string(JoinMemberOperation), http.MethodPost, nil) - return err -} - -// LeaveMember sends a Leave member operation request to Lorry, located on the target pod that is about to leave. -func (cli *lorryClient) LeaveMember(ctx context.Context) error { - _, err := cli.Request(ctx, string(LeaveMemberOperation), http.MethodPost, nil) - return err -} - -// Lock sends a set readonly request to Lorry. -func (cli *lorryClient) Lock(ctx context.Context) error { - _, err := cli.Request(ctx, string(LockOperation), http.MethodPost, nil) - return err -} - -// Unlock sends a set readwrite request to Lorry. -func (cli *lorryClient) Unlock(ctx context.Context) error { - _, err := cli.Request(ctx, string(UnlockOperation), http.MethodPost, nil) - return err -} - -// PostProvision sends a component post provision request to Lorry. -func (cli *lorryClient) PostProvision(ctx context.Context, componentNames, podNames, podIPs, podHostNames, podHostIPs string) error { - parameters := map[string]any{ - "componentNames": componentNames, - "podNames": podNames, - "podIPs": podIPs, - "podHostNames": podHostNames, - "podHostIPs": podHostIPs, - } - req := map[string]any{"parameters": parameters} - _, err := cli.Request(ctx, string(PostProvisionOperation), http.MethodPost, req) - return err -} - -// PreTerminate sends a pre terminate request to Lorry. -func (cli *lorryClient) PreTerminate(ctx context.Context) error { - _, err := cli.Request(ctx, string(PreTerminateOperation), http.MethodPost, nil) - return err -} - -// Rebuild sends a slave rebuild request to Lorry. -func (cli *lorryClient) Rebuild(ctx context.Context) error { - _, err := cli.Request(ctx, "rebuild", http.MethodPost, nil) - return err -} - -func (cli *lorryClient) DataDump(ctx context.Context) error { - _, err := cli.Request(ctx, string(DataDumpOperation), http.MethodPost, nil) - return err -} - -func (cli *lorryClient) DataLoad(ctx context.Context) error { - _, err := cli.Request(ctx, string(DataLoadOperation), http.MethodPost, nil) - return err -} - -func (cli *lorryClient) Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) { - if cli.requester == nil { - return nil, errors.New("lorry client's requester must be set") - } - return cli.requester.Request(ctx, operation, method, req) -} diff --git a/pkg/lorry/client/client_mock.go b/pkg/lorry/client/client_mock.go deleted file mode 100644 index a607c0ba5c3..00000000000 --- a/pkg/lorry/client/client_mock.go +++ /dev/null @@ -1,312 +0,0 @@ -// /* -// Copyright (C) 2022-2024 ApeCloud Co., Ltd -// -// This file is part of KubeBlocks project -// -// 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 . -// */ -// -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/apecloud/kubeblocks/pkg/lorry/client (interfaces: Client) - -// Package client is a generated GoMock package. -package client - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockClient is a mock of Client interface. -type MockClient struct { - ctrl *gomock.Controller - recorder *MockClientMockRecorder -} - -// MockClientMockRecorder is the mock recorder for MockClient. -type MockClientMockRecorder struct { - mock *MockClient -} - -// NewMockClient creates a new mock instance. -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockClient) EXPECT() *MockClientMockRecorder { - return m.recorder -} - -// CreateUser mocks base method. -func (m *MockClient) CreateUser(arg0 context.Context, arg1, arg2, arg3, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateUser indicates an expected call of CreateUser. -func (mr *MockClientMockRecorder) CreateUser(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockClient)(nil).CreateUser), arg0, arg1, arg2, arg3, arg4) -} - -// DataDump mocks base method. -func (m *MockClient) DataDump(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DataDump", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DataDump indicates an expected call of DataDump. -func (mr *MockClientMockRecorder) DataDump(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DataDump", reflect.TypeOf((*MockClient)(nil).DataDump), arg0) -} - -// DataLoad mocks base method. -func (m *MockClient) DataLoad(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DataLoad", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// DataLoad indicates an expected call of DataLoad. -func (mr *MockClientMockRecorder) DataLoad(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DataLoad", reflect.TypeOf((*MockClient)(nil).DataLoad), arg0) -} - -// DeleteUser mocks base method. -func (m *MockClient) DeleteUser(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteUser", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteUser indicates an expected call of DeleteUser. -func (mr *MockClientMockRecorder) DeleteUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockClient)(nil).DeleteUser), arg0, arg1) -} - -// DescribeUser mocks base method. -func (m *MockClient) DescribeUser(arg0 context.Context, arg1 string) (map[string]interface{}, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeUser", arg0, arg1) - ret0, _ := ret[0].(map[string]interface{}) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DescribeUser indicates an expected call of DescribeUser. -func (mr *MockClientMockRecorder) DescribeUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeUser", reflect.TypeOf((*MockClient)(nil).DescribeUser), arg0, arg1) -} - -// GetRole mocks base method. -func (m *MockClient) GetRole(arg0 context.Context) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRole", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRole indicates an expected call of GetRole. -func (mr *MockClientMockRecorder) GetRole(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRole", reflect.TypeOf((*MockClient)(nil).GetRole), arg0) -} - -// GrantUserRole mocks base method. -func (m *MockClient) GrantUserRole(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GrantUserRole", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// GrantUserRole indicates an expected call of GrantUserRole. -func (mr *MockClientMockRecorder) GrantUserRole(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantUserRole", reflect.TypeOf((*MockClient)(nil).GrantUserRole), arg0, arg1, arg2) -} - -// JoinMember mocks base method. -func (m *MockClient) JoinMember(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "JoinMember", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// JoinMember indicates an expected call of JoinMember. -func (mr *MockClientMockRecorder) JoinMember(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JoinMember", reflect.TypeOf((*MockClient)(nil).JoinMember), arg0) -} - -// LeaveMember mocks base method. -func (m *MockClient) LeaveMember(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LeaveMember", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// LeaveMember indicates an expected call of LeaveMember. -func (mr *MockClientMockRecorder) LeaveMember(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LeaveMember", reflect.TypeOf((*MockClient)(nil).LeaveMember), arg0) -} - -// ListSystemAccounts mocks base method. -func (m *MockClient) ListSystemAccounts(arg0 context.Context) ([]map[string]interface{}, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSystemAccounts", arg0) - ret0, _ := ret[0].([]map[string]interface{}) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListSystemAccounts indicates an expected call of ListSystemAccounts. -func (mr *MockClientMockRecorder) ListSystemAccounts(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSystemAccounts", reflect.TypeOf((*MockClient)(nil).ListSystemAccounts), arg0) -} - -// ListUsers mocks base method. -func (m *MockClient) ListUsers(arg0 context.Context) ([]map[string]interface{}, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListUsers", arg0) - ret0, _ := ret[0].([]map[string]interface{}) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListUsers indicates an expected call of ListUsers. -func (mr *MockClientMockRecorder) ListUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockClient)(nil).ListUsers), arg0) -} - -// Lock mocks base method. -func (m *MockClient) Lock(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Lock", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Lock indicates an expected call of Lock. -func (mr *MockClientMockRecorder) Lock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockClient)(nil).Lock), arg0) -} - -// PostProvision mocks base method. -func (m *MockClient) PostProvision(arg0 context.Context, arg1, arg2, arg3, arg4, arg5 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PostProvision", arg0, arg1, arg2, arg3, arg4, arg5) - ret0, _ := ret[0].(error) - return ret0 -} - -// PostProvision indicates an expected call of PostProvision. -func (mr *MockClientMockRecorder) PostProvision(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostProvision", reflect.TypeOf((*MockClient)(nil).PostProvision), arg0, arg1, arg2, arg3, arg4, arg5) -} - -// PreTerminate mocks base method. -func (m *MockClient) PreTerminate(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PreTerminate", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// PreTerminate indicates an expected call of PreTerminate. -func (mr *MockClientMockRecorder) PreTerminate(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreTerminate", reflect.TypeOf((*MockClient)(nil).PreTerminate), arg0) -} - -// Rebuild mocks base method. -func (m *MockClient) Rebuild(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Rebuild", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Rebuild indicates an expected call of Rebuild. -func (mr *MockClientMockRecorder) Rebuild(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rebuild", reflect.TypeOf((*MockClient)(nil).Rebuild), arg0) -} - -// RevokeUserRole mocks base method. -func (m *MockClient) RevokeUserRole(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeUserRole", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// RevokeUserRole indicates an expected call of RevokeUserRole. -func (mr *MockClientMockRecorder) RevokeUserRole(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserRole", reflect.TypeOf((*MockClient)(nil).RevokeUserRole), arg0, arg1, arg2) -} - -// Switchover mocks base method. -func (m *MockClient) Switchover(arg0 context.Context, arg1, arg2 string, arg3 bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Switchover", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// Switchover indicates an expected call of Switchover. -func (mr *MockClientMockRecorder) Switchover(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Switchover", reflect.TypeOf((*MockClient)(nil).Switchover), arg0, arg1, arg2, arg3) -} - -// Unlock mocks base method. -func (m *MockClient) Unlock(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Unlock", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Unlock indicates an expected call of Unlock. -func (mr *MockClientMockRecorder) Unlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockClient)(nil).Unlock), arg0) -} diff --git a/pkg/lorry/client/generate.go b/pkg/lorry/client/generate.go deleted file mode 100644 index 723668b8199..00000000000 --- a/pkg/lorry/client/generate.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination client_mock.go github.com/apecloud/kubeblocks/pkg/lorry/client Client diff --git a/pkg/lorry/client/httpclient.go b/pkg/lorry/client/httpclient.go deleted file mode 100644 index 3fc8e13c733..00000000000 --- a/pkg/lorry/client/httpclient.go +++ /dev/null @@ -1,304 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "sort" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - ctrl "sigs.k8s.io/controller-runtime" - - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" -) - -const ( - urlTemplate = "http://%s:%d/v1.0/" -) - -var NotImplemented = errors.New("NotImplemented") - -type HTTPClient struct { - lorryClient - Client *http.Client - URL string - CacheTTL time.Duration - ReconcileTimeout time.Duration - RequestTimeout time.Duration - logger logr.Logger -} - -var _ Client = &HTTPClient{} - -type OperationResult struct { - response *http.Response - err error - reqTime time.Time - respTime time.Time -} - -var cache map[string]*OperationResult = make(map[string]*OperationResult) - -func NewHTTPClientWithPod(pod *corev1.Pod) (*HTTPClient, error) { - logger := ctrl.Log.WithName("Lorry HTTP client") - port, err := intctrlutil.GetLorryHTTPPort(pod) - if err != nil { - logger.Info("not lorry in the pod, just return nil without error") - return nil, nil - } - - ip := pod.Status.PodIP - if ip == "" { - return nil, fmt.Errorf("pod %v has no ip", pod.Name) - } - - // don't use default http-client - dialer := &net.Dialer{ - Timeout: 5 * time.Second, - } - netTransport := &http.Transport{ - Dial: dialer.Dial, - TLSHandshakeTimeout: 5 * time.Second, - } - client := &http.Client{ - Timeout: time.Second * 30, - Transport: netTransport, - } - - operationClient := &HTTPClient{ - Client: client, - URL: fmt.Sprintf(urlTemplate, ip, port), - CacheTTL: 1800 * time.Second, - RequestTimeout: 300 * time.Second, - ReconcileTimeout: 500 * time.Millisecond, - logger: ctrl.Log.WithName("Lorry HTTP client"), - } - operationClient.lorryClient = lorryClient{requester: operationClient} - return operationClient, nil -} - -func NewHTTPClientWithURL(url string) (*HTTPClient, error) { - if url == "" { - return nil, fmt.Errorf("no url") - } - - // don't use default http-client - dialer := &net.Dialer{ - Timeout: 5 * time.Second, - } - netTransport := &http.Transport{ - Dial: dialer.Dial, - TLSHandshakeTimeout: 5 * time.Second, - } - client := &http.Client{ - Timeout: time.Second * 30, - Transport: netTransport, - } - - operationClient := &HTTPClient{ - Client: client, - URL: url, - CacheTTL: 1800 * time.Second, - RequestTimeout: 300 * time.Second, - ReconcileTimeout: 500 * time.Millisecond, - } - operationClient.lorryClient = lorryClient{requester: operationClient} - return operationClient, nil -} - -func (cli *HTTPClient) Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) { - ctxWithReconcileTimeout, cancel := context.WithTimeout(ctx, cli.ReconcileTimeout) - defer cancel() - - // Request sql channel via http request - url := fmt.Sprintf("%s%s", cli.URL, strings.ToLower(operation)) - - var reader io.Reader = nil - if req != nil { - body, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(err, "request encode failed") - } - reader = bytes.NewReader(body) - } - - resp, err := cli.InvokeComponentInRoutine(ctxWithReconcileTimeout, url, method, reader) - if err != nil { - return nil, err - } - - switch resp.StatusCode { - case http.StatusOK, http.StatusUnavailableForLegalReasons: - result, err := parseBody(resp.Body) - if err != nil { - return nil, err - } - if event, ok := result["event"]; ok && event.(string) == "NotImplemented" { - return nil, NotImplemented - } - return result, nil - - case http.StatusNoContent: - return nil, nil - case http.StatusNotImplemented, http.StatusInternalServerError: - fallthrough - default: - msg, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return nil, fmt.Errorf("%s", string(msg)) - } -} - -func (cli *HTTPClient) InvokeComponentInRoutine(ctxWithReconcileTimeout context.Context, url, method string, body io.Reader) (*http.Response, error) { - ch := make(chan *OperationResult, 1) - go cli.InvokeComponent(ctxWithReconcileTimeout, url, method, body, ch) - var resp *http.Response - var err error - select { - case <-ctxWithReconcileTimeout.Done(): - err = fmt.Errorf("request timeout: %v", ctxWithReconcileTimeout.Err()) - case result := <-ch: - resp = result.response - err = result.err - } - return resp, err -} - -func (cli *HTTPClient) InvokeComponent(ctxWithReconcileTimeout context.Context, url, method string, body io.Reader, ch chan *OperationResult) { - ctxWithRequestTimeout, cancel := context.WithTimeout(context.Background(), cli.RequestTimeout) - defer cancel() - req, err := http.NewRequestWithContext(ctxWithRequestTimeout, method, url, body) - if err != nil { - operationRes := &OperationResult{ - response: nil, - err: err, - respTime: time.Now(), - } - ch <- operationRes - return - } - - mapKey := GetMapKeyFromRequest(req) - operationRes, ok := cache[mapKey] - if ok { - if operationRes.response != nil { - // if the response is not nil, it means the request has been sent and the response is cached - delete(cache, mapKey) - if time.Since(operationRes.respTime) <= cli.CacheTTL { - ch <- operationRes - return - } - cli.logger.Info("cache expired", "url", url, "method", method, "cacheTTL", cli.CacheTTL, "since", time.Since(operationRes.respTime)) - } else { - // if the response is nil, it means the request has been sent and not finished yet - if time.Since(operationRes.reqTime) >= 2*cli.RequestTimeout { - // if the request has been sent for less than 2 times of cacheTTL, and there is no the response, clean the cache and send the request again - delete(cache, mapKey) - cli.logger.Info("request timeout, and try again", "url", url, "method", method, "timeout", 2*cli.RequestTimeout, "since", time.Since(operationRes.reqTime)) - } else { - ch <- operationRes - return - } - } - } - operationRes = &OperationResult{ - err: fmt.Errorf("request not finished"), - reqTime: time.Now(), - } - - cache[mapKey] = operationRes - resp, err := cli.Client.Do(req) - operationRes.response = resp - operationRes.err = err - operationRes.respTime = time.Now() - select { - case <-ctxWithReconcileTimeout.Done(): - default: - delete(cache, mapKey) - ch <- operationRes - } -} - -func GetMapKeyFromRequest(req *http.Request) string { - var buf bytes.Buffer - buf.WriteString(req.URL.String()) - - if req.Body != nil { - all, err := io.ReadAll(req.Body) - if err != nil { - return "" - } - req.Body = io.NopCloser(bytes.NewReader(all)) - buf.Write(all) - } - keys := make([]string, 0, len(req.Header)) - for k := range req.Header { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - buf.WriteString(fmt.Sprintf("%s:%s", k, req.Header[k])) - } - - return buf.String() -} - -func parseBody(body io.Reader) (map[string]any, error) { - result := map[string]any{} - data, err := io.ReadAll(body) - if err != nil { - return nil, errors.Wrap(err, "read response body failed") - } - err = json.Unmarshal(data, &result) - if err != nil { - return nil, errors.Wrap(err, "decode body failed") - } - - return result, nil -} - -func convertToArrayOfMap(value any) ([]map[string]any, error) { - array, ok := value.([]any) - if !ok { - return nil, fmt.Errorf("resp errors: %v", value) - } - - result := make([]map[string]any, 0, len(array)) - for _, v := range array { - m, ok := v.(map[string]any) - if !ok { - return nil, fmt.Errorf("resp errors: %v", value) - } - result = append(result, m) - } - return result, nil -} diff --git a/pkg/lorry/client/httpclient_test.go b/pkg/lorry/client/httpclient_test.go deleted file mode 100644 index c1a4d1f91dd..00000000000 --- a/pkg/lorry/client/httpclient_test.go +++ /dev/null @@ -1,872 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "strconv" - "strings" - "time" - - "github.com/golang/mock/gomock" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/custom" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - opsregister "github.com/apecloud/kubeblocks/pkg/lorry/operations/register" - "github.com/apecloud/kubeblocks/pkg/lorry/util" - testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" - "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const ( - msg = "not implemented for test" -) - -var _ = Describe("Lorry HTTP Client", func() { - var pod *corev1.Pod - - BeforeEach(func() { - podName := "pod-for-lorry-test" - pod = testapps.NewPodFactory("default", podName). - AddContainer(corev1.Container{ - Name: testapps.DefaultNginxContainerName, - Command: []string{"lorry", "--port", strconv.Itoa(lorryHTTPPort)}, - Image: testapps.NginxImage}). - GetObject() - pod.Spec.Containers[0].Ports = []corev1.ContainerPort{ - { - ContainerPort: int32(lorryHTTPPort), - Name: constant.LorryHTTPPortName, - Protocol: "TCP", - }, - { - ContainerPort: int32(50001), - Name: constant.LorryGRPCPortName, - Protocol: "TCP", - }, - } - pod.Status.PodIP = "127.0.0.1" - }) - - Context("new HTTPClient", func() { - It("without lorry service, return nil", func() { - podWithoutLorry := pod.DeepCopy() - podWithoutLorry.Spec.Containers[0].Ports = nil - lorryClient, err := NewHTTPClientWithPod(podWithoutLorry) - Expect(err).ShouldNot(HaveOccurred()) - Expect(lorryClient).Should(BeNil()) - }) - - It("without pod ip, failed", func() { - podWithoutPodIP := pod.DeepCopy() - podWithoutPodIP.Status.PodIP = "" - _, err := NewHTTPClientWithPod(podWithoutPodIP) - Expect(err).Should(HaveOccurred()) - }) - - It("success", func() { - lorryClient, err := NewHTTPClientWithPod(pod) - Expect(err).ShouldNot(HaveOccurred()) - Expect(lorryClient).ShouldNot(BeNil()) - }) - }) - - Context("request with timeout", func() { - var httpServer *httptest.Server - var port int - var lorryClient *HTTPClient - - BeforeEach(func() { - pod1 := pod.DeepCopy() - body := []byte("{\"role\": \"leader\"}") - httpServer, port = newHTTPServer(body) - pod1.Spec.Containers[0].Ports[0].ContainerPort = int32(port) - lorryClient, _ = NewHTTPClientWithPod(pod1) - Expect(lorryClient).ShouldNot(BeNil()) - cache = make(map[string]*OperationResult) - }) - - AfterEach(func() { - httpServer.Close() - }) - - It("response in time", func() { - lorryClient.ReconcileTimeout = 1 * time.Second - _, err := lorryClient.GetRole(context.TODO()) - Expect(err).ShouldNot(HaveOccurred()) - Expect(cache).Should(BeEmpty()) - }) - - It("response timeout", func() { - lorryClient.ReconcileTimeout = 50 * time.Millisecond - _, err := lorryClient.GetRole(context.TODO()) - Expect(err).Should(HaveOccurred()) - // wait client to get response and cache it - time.Sleep(200 * time.Millisecond) - Expect(cache).Should(HaveLen(1)) - }) - - It("response by cache", func() { - lorryClient.ReconcileTimeout = 50 * time.Millisecond - // get response from server, and timeout - _, err := lorryClient.GetRole(context.TODO()) - Expect(err).Should(HaveOccurred()) - // wait client to get response and cache it - time.Sleep(200 * time.Millisecond) - // get response from cache - _, err = lorryClient.GetRole(context.TODO()) - Expect(err).ShouldNot(HaveOccurred()) - Expect(cache).Should(BeEmpty()) - }) - }) - - Context("get replica role", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("success", func() { - role := "leader" - mockDBManager.EXPECT().GetReplicaRole(gomock.Any(), gomock.Any()).Return(role, nil) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}) - Expect(lorryClient.GetRole(context.TODO())).Should(Equal(role)) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().GetReplicaRole(gomock.Any(), gomock.Any()).Return(string(""), fmt.Errorf("%s", msg)) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}) - role, err := lorryClient.GetRole(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - Expect(role).Should(BeEmpty()) - }) - }) - - Context("list system accounts", func() { - var lorryClient *HTTPClient - var systemAccounts []models.UserInfo - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - systemAccounts = []models.UserInfo{ - { - UserName: "kb-admin1", - }, - { - UserName: "kb-admin2", - }, - } - }) - - It("success", func() { - mockDBManager.EXPECT().ListSystemAccounts(gomock.Any()).Return(systemAccounts, nil) - Expect(lorryClient.ListSystemAccounts(context.TODO())).Should(HaveLen(2)) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().ListSystemAccounts(gomock.Any()).Return(nil, fmt.Errorf("%s", msg)) - accounts, err := lorryClient.ListSystemAccounts(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - Expect(accounts).Should(BeEmpty()) - }) - }) - - Context("create user", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("success", func() { - mockDBManager.EXPECT().DescribeUser(gomock.Any(), gomock.Any()).Return(nil, nil) - mockDBManager.EXPECT().CreateUser(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - Expect(lorryClient.CreateUser(context.TODO(), "user-test", "password-test", "", "")).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().DescribeUser(gomock.Any(), gomock.Any()).Return(nil, nil) - mockDBManager.EXPECT().CreateUser(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.CreateUser(context.TODO(), "user-test", "password-test", "", "") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - }) - - Context("delete user", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("success", func() { - mockDBManager.EXPECT().DeleteUser(gomock.Any(), gomock.Any()).Return(nil) - Expect(lorryClient.DeleteUser(context.TODO(), "user-test")).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().DeleteUser(gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.DeleteUser(context.TODO(), "user-test") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - }) - - Context("describe user", func() { - var lorryClient *HTTPClient - var userInfo *models.UserInfo - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - userInfo = &models.UserInfo{ - UserName: "kb-admin1", - } - }) - - It("success", func() { - mockDBManager.EXPECT().DescribeUser(gomock.Any(), gomock.Any()).Return(userInfo, nil) - user, err := lorryClient.DescribeUser(context.TODO(), "user-test") - Expect(err).Should(Succeed()) - Expect(user).ShouldNot(BeZero()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().DescribeUser(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("%s", msg)) - _, err := lorryClient.DescribeUser(context.TODO(), "user-test") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - }) - - Context("grant user role", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("success", func() { - mockDBManager.EXPECT().GrantUserRole(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - Expect(lorryClient.GrantUserRole(context.TODO(), "user-test", "readwrite")).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().GrantUserRole(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.GrantUserRole(context.TODO(), "user-test", "readwrite") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - }) - - Context("revoke user role", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("success", func() { - mockDBManager.EXPECT().RevokeUserRole(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - Expect(lorryClient.RevokeUserRole(context.TODO(), "user-test", "readwrite")).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().RevokeUserRole(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.RevokeUserRole(context.TODO(), "user-test", "readwrite") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - }) - - Context("list users", func() { - var lorryClient *HTTPClient - var users []models.UserInfo - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - users = []models.UserInfo{ - { - UserName: "user1", - }, - { - UserName: "user2", - }, - } - }) - - It("success", func() { - mockDBManager.EXPECT().ListUsers(gomock.Any()).Return(users, nil) - Expect(lorryClient.ListUsers(context.TODO())).Should(HaveLen(2)) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().ListUsers(gomock.Any()).Return(nil, fmt.Errorf("%s", msg)) - users, err := lorryClient.ListUsers(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - Expect(users).Should(BeEmpty()) - }) - }) - - Context("join member", func() { - var lorryClient *HTTPClient - var cluster *dcs.Cluster - var podName = "pod-test" - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - cluster = &dcs.Cluster{ - Members: []dcs.Member{{Name: podName}}, - Leader: &dcs.Leader{Name: podName}, - } - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - }) - - It("success if join once", func() { - mockDBManager.EXPECT().JoinCurrentMemberToCluster(gomock.Any(), gomock.Any()).Return(nil) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - Expect(lorryClient.JoinMember(context.TODO())).Should(Succeed()) - }) - - It("success if join twice", func() { - mockDBManager.EXPECT().JoinCurrentMemberToCluster(gomock.Any(), gomock.Any()).Return(nil).Times(2) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil).Times(2) - // first join - Expect(lorryClient.JoinMember(context.TODO())).Should(Succeed()) - // second join - Expect(lorryClient.JoinMember(context.TODO())).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().JoinCurrentMemberToCluster(gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - err := lorryClient.JoinMember(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - - // It("return nil if envs is unset", func() { - // mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - // actions := map[string][]string{} - // actions[constant.MemberJoinAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.JoinMemberOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.JoinMember(context.TODO()) - // Expect(err).Should(BeNil()) - // }) - - It("execute command failed cased by executable file is not found", func() { - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - actions := map[string][]string{} - actions[constant.MemberJoinAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.JoinMemberOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.JoinMember(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("leave member", func() { - var lorryClient *HTTPClient - var cluster *dcs.Cluster - var podName string - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - podName = "pod-test" - - cluster = &dcs.Cluster{ - HaConfig: &dcs.HaConfig{DeleteMembers: make(map[string]dcs.MemberToDelete)}, - Members: []dcs.Member{{Name: podName}}, - Leader: &dcs.Leader{Name: podName}, - } - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - viperx.SetDefault(constant.KBEnvPodName, podName) - }) - - It("success if leave once", func() { - mockDBManager.EXPECT().GetCurrentMemberName().Return(podName).Times(2) - mockDBManager.EXPECT().LeaveMemberFromCluster(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - mockDCSStore.EXPECT().UpdateHaConfig().Return(nil).Times(2) - Expect(lorryClient.LeaveMember(context.TODO())).Should(Succeed()) - Expect(cluster.HaConfig.DeleteMembers).Should(HaveLen(1)) - }) - - It("success if leave twice", func() { - mockDBManager.EXPECT().GetCurrentMemberName().Return(podName).Times(4) - mockDBManager.EXPECT().LeaveMemberFromCluster(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil).Times(2) - mockDCSStore.EXPECT().UpdateHaConfig().Return(nil).Times(3) - // first leave - Expect(lorryClient.LeaveMember(context.TODO())).Should(Succeed()) - Expect(cluster.HaConfig.DeleteMembers).Should(HaveLen(1)) - // second leave - Expect(lorryClient.LeaveMember(context.TODO())).Should(Succeed()) - Expect(cluster.HaConfig.DeleteMembers).Should(HaveLen(1)) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().GetCurrentMemberName().Return(podName).Times(2) - mockDBManager.EXPECT().LeaveMemberFromCluster(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - mockDCSStore.EXPECT().UpdateHaConfig().Return(nil) - err := lorryClient.LeaveMember(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - - // It("execute command failed cased by envs is unset", func() { - // mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - // mockDCSStore.EXPECT().UpdateHaConfig().Return(nil) - // actions := map[string][]string{} - // actions[constant.MemberLeaveAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.LeaveMemberOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.LeaveMember(context.TODO()) - // Expect(err).Should(HaveOccurred()) - // Expect(err.Error()).Should(ContainSubstring("envs is unset")) - // }) - - It("execute command failed cased by executable file is not found", func() { - mockDCSStore.EXPECT().GetCluster().Return(cluster, nil) - mockDCSStore.EXPECT().UpdateHaConfig().Return(nil) - actions := map[string][]string{} - actions[constant.MemberLeaveAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.LeaveMemberOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.LeaveMember(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("switchover", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - }) - - It("fail if ha is disabled", func() { - clusterTemp := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - Members: []dcs.Member{{Name: "pod-0"}, {Name: "pod-1"}}, - } - clusterTemp.HaConfig.SetEnable(false) - mockDCSStore.EXPECT().GetCluster().Return(clusterTemp, nil) - err := lorryClient.Switchover(context.TODO(), "pod-0", "pod-1", false) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("cluster's ha is disabled")) - }) - - It("fail if the specified member has a wrong role", func() { - clusterTemp := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - Members: []dcs.Member{{Name: "pod-0"}, {Name: "pod-1"}}, - } - clusterTemp.HaConfig.SetEnable(true) - mockDCSStore.EXPECT().GetCluster().Return(clusterTemp, nil).AnyTimes() - err := lorryClient.Switchover(context.TODO(), "pod-a", "pod-b", false) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("primary pod-a not exists")) - - mockDBManager.EXPECT().IsLeaderMember(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(1) - err = lorryClient.Switchover(context.TODO(), "pod-0", "pod-1", false) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("pod-0 is not the primary")) - - mockDBManager.EXPECT().IsLeaderMember(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) - err = lorryClient.Switchover(context.TODO(), "pod-0", "pod-b", false) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("candidate pod-b not exists")) - - mockDBManager.EXPECT().IsLeaderMember(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) - mockDBManager.EXPECT().IsMemberHealthy(gomock.Any(), gomock.Any(), gomock.Any()).Return(false).Times(1) - err = lorryClient.Switchover(context.TODO(), "pod-0", "pod-1", false) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("candidate pod-1 is unhealthy")) - }) - - It("success if the primary specified only", func() { - clusterTemp := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - Members: []dcs.Member{{Name: "pod-0"}, {Name: "pod-1"}}, - } - clusterTemp.HaConfig.SetEnable(true) - mockDCSStore.EXPECT().GetCluster().Return(clusterTemp, nil).AnyTimes() - mockDCSStore.EXPECT().CreateSwitchover(gomock.Any(), gomock.Any()).Return(nil) - mockDBManager.EXPECT().IsLeaderMember(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) - mockDBManager.EXPECT().HasOtherHealthyMembers(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*dcs.Member{{}, {}}).Times(1) - err := lorryClient.Switchover(context.TODO(), "pod-0", "", false) - Expect(err).Should(BeNil()) - }) - - It("success if the candidate specified only", func() { - clusterTemp := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - Members: []dcs.Member{{Name: "pod-0"}, {Name: "pod-1"}}, - } - clusterTemp.HaConfig.SetEnable(true) - mockDCSStore.EXPECT().GetCluster().Return(clusterTemp, nil).AnyTimes() - mockDCSStore.EXPECT().CreateSwitchover(gomock.Any(), gomock.Any()).Return(nil) - mockDBManager.EXPECT().IsMemberHealthy(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).Times(1) - err := lorryClient.Switchover(context.TODO(), "", "pod-1", false) - Expect(err).Should(BeNil()) - }) - - It("success if the primary specified only", func() { - clusterTemp := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - Members: []dcs.Member{{Name: "pod-0"}, {Name: "pod-1"}}, - } - clusterTemp.HaConfig.SetEnable(true) - mockDCSStore.EXPECT().GetCluster().Return(clusterTemp, nil).AnyTimes() - mockDCSStore.EXPECT().CreateSwitchover(gomock.Any(), gomock.Any()).Return(nil) - mockDBManager.EXPECT().IsLeaderMember(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).Times(1) - mockDBManager.EXPECT().IsMemberHealthy(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).Times(1) - err := lorryClient.Switchover(context.TODO(), "pod-0", "pod-1", false) - Expect(err).Should(BeNil()) - }) - }) - - Context("lock member", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - - }) - - It("success", func() { - mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil) - Expect(lorryClient.Lock(context.TODO())).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.Lock(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - - // It("execute command failed cased by envs is unset", func() { - // actions := map[string][]string{} - // actions[constant.ReadonlyAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.LockOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.Lock(context.TODO()) - // Expect(err).Should(HaveOccurred()) - // Expect(err.Error()).Should(ContainSubstring("envs is unset")) - // }) - - It("execute command failed cased by executable file is not found", func() { - actions := map[string][]string{} - actions[constant.ReadonlyAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.LockOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.Lock(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("unlock member", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - }) - - It("success", func() { - mockDBManager.EXPECT().Unlock(gomock.Any()).Return(nil) - Expect(lorryClient.Unlock(context.TODO())).Should(Succeed()) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().Unlock(gomock.Any()).Return(fmt.Errorf("%s", msg)) - err := lorryClient.Unlock(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring(msg)) - }) - - // It("execute command failed cased by envs is unset", func() { - // actions := map[string][]string{} - // actions[constant.ReadWriteAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.UnlockOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.Unlock(context.TODO()) - // Expect(err).Should(HaveOccurred()) - // Expect(err.Error()).Should(ContainSubstring("envs is unset")) - // }) - - It("execute command failed cased by executable file is not found", func() { - actions := map[string][]string{} - actions[constant.ReadWriteAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.UnlockOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.Unlock(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("post provision", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - }) - - It("not implemented", func() { - err := lorryClient.PostProvision(context.TODO(), "", "", "", "", "") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("operation exec failed: not implemented")) - }) - - // It("execute command failed cased by envs is unset", func() { - // actions := map[string][]string{} - // actions[constant.PostProvisionAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.PostProvisionOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.PostProvision(context.TODO(), "", "", "", "", "") - // Expect(err).Should(HaveOccurred()) - // Expect(err.Error()).Should(ContainSubstring("envs is unset")) - // }) - - It("execute command failed cased by executable file is not found", func() { - actions := map[string][]string{} - actions[constant.PostProvisionAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.PostProvisionOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.PostProvision(context.TODO(), "", "", "", "", "") - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("pre terminate", func() { - var lorryClient *HTTPClient - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - }) - - It("not implemented", func() { - err := lorryClient.PreTerminate(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("operation exec failed: not implemented")) - }) - - // It("execute command failed cased by envs is unset", func() { - // actions := map[string][]string{} - // actions[constant.PreTerminateAction] = []string{"ls"} - // jsonStr, _ := json.Marshal(actions) - // viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - // ops := opsregister.Operations() - // _ = ops[strings.ToLower(string(util.PreTerminateOperation))].Init(context.TODO()) - // customManager, _ := custom.NewManager(engines.Properties{}) - // register.SetCustomManager(customManager) - // err := lorryClient.PreTerminate(context.TODO()) - // Expect(err).Should(HaveOccurred()) - // Expect(err.Error()).Should(ContainSubstring("envs is unset")) - // }) - - It("execute command failed cased by executable file is not found", func() { - actions := map[string][]string{} - actions[constant.PreTerminateAction] = []string{"binary_not_exist"} - jsonStr, _ := json.Marshal(actions) - viperx.SetDefault(constant.KBEnvActionCommands, jsonStr) - os.Setenv(constant.KBEnvPodFQDN, "test") - os.Setenv(constant.KBEnvServicePort, "test") - os.Setenv(constant.KBEnvServiceUser, "test") - os.Setenv(constant.KBEnvServicePassword, "test") - ops := opsregister.Operations() - _ = ops[strings.ToLower(string(util.PreTerminateOperation))].Init(context.TODO()) - customManager, _ := custom.NewManager(engines.Properties{}) - register.SetCustomManager(customManager) - err := lorryClient.PreTerminate(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("executable file not found")) - }) - }) - - Context("rebuild", func() { - var lorryClient *HTTPClient - var cluster *dcs.Cluster - podName := "pod-test" - - BeforeEach(func() { - lorryClient, _ = NewHTTPClientWithPod(pod) - Expect(lorryClient).ShouldNot(BeNil()) - cluster = &dcs.Cluster{ - Members: []dcs.Member{{Name: podName}}, - } - os.Unsetenv(constant.KBEnvPodFQDN) - os.Unsetenv(constant.KBEnvServicePort) - os.Unsetenv(constant.KBEnvServiceUser) - os.Unsetenv(constant.KBEnvServicePassword) - }) - - It("not implemented", func() { - mockDBManager.EXPECT().GetCurrentMemberName().Return(podName) - mockDCSStore.EXPECT().GetClusterFromCache().Return(cluster) - err := lorryClient.Rebuild(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("there is no ha service yet")) - }) - - It("request failed", func() { - cluster.Members[0].HAPort = "63601" - mockDBManager.EXPECT().GetCurrentMemberName().Return(podName) - mockDCSStore.EXPECT().GetClusterFromCache().Return(cluster) - err := lorryClient.Rebuild(context.TODO()) - Expect(err).Should(HaveOccurred()) - Expect(err.Error()).Should(ContainSubstring("request ha service failed")) - }) - }) -}) - -func newHTTPServer(resp []byte) (*httptest.Server, int) { - s := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - time.Sleep(100 * time.Millisecond) - writer.WriteHeader(200) - _, _ = writer.Write(resp) - })) - addr := s.Listener.Addr().String() - index := strings.LastIndex(addr, ":") - portStr := addr[index+1:] - port, _ := strconv.Atoi(portStr) - return s, port -} diff --git a/pkg/lorry/client/interface.go b/pkg/lorry/client/interface.go deleted file mode 100644 index 0ec73cf5cd0..00000000000 --- a/pkg/lorry/client/interface.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import "context" - -type Client interface { - // GetRole return the replication role(like primary/secondary) of the target replica - GetRole(ctx context.Context) (string, error) - - // user management funcs - CreateUser(ctx context.Context, userName, password, roleName, statement string) error - DeleteUser(ctx context.Context, userName string) error - DescribeUser(ctx context.Context, userName string) (map[string]any, error) - GrantUserRole(ctx context.Context, userName, roleName string) error - RevokeUserRole(ctx context.Context, userName, roleName string) error - ListUsers(ctx context.Context) ([]map[string]any, error) - ListSystemAccounts(ctx context.Context) ([]map[string]any, error) - - // JoinMember sends a join member operation request to Lorry, located on the target pod that is about to join. - JoinMember(ctx context.Context) error - - // LeaveMember sends a Leave member operation request to Lorry, located on the target pod that is about to leave. - LeaveMember(ctx context.Context) error - - Switchover(ctx context.Context, primary, candidate string, force bool) error - Lock(ctx context.Context) error - Unlock(ctx context.Context) error - PostProvision(ctx context.Context, componentNames, podNames, podIPs, podHostNames, podHostIPs string) error - PreTerminate(ctx context.Context) error - - // local rebuild slave - Rebuild(ctx context.Context) error - DataDump(ctx context.Context) error - DataLoad(ctx context.Context) error -} diff --git a/pkg/lorry/client/k8sexec_client.go b/pkg/lorry/client/k8sexec_client.go deleted file mode 100644 index 33f8d897e84..00000000000 --- a/pkg/lorry/client/k8sexec_client.go +++ /dev/null @@ -1,203 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "os" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericiooptions" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/remotecommand" - cmdexec "k8s.io/kubectl/pkg/cmd/exec" - ctrl "sigs.k8s.io/controller-runtime" - - intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" -) - -// K8sExecClient is a mock client for operation, mainly used to hide curl command details. -type K8sExecClient struct { - lorryClient - cmdexec.StreamOptions - Executor cmdexec.RemoteExecutor - restConfig *rest.Config - restClient *rest.RESTClient - lorryPort int32 - RequestTimeout time.Duration - logger logr.Logger -} - -// NewK8sExecClientWithPod create a new OperationHTTPClient with lorry container -func NewK8sExecClientWithPod(restConfig *rest.Config, pod *corev1.Pod) (*K8sExecClient, error) { - var ( - err error - ) - logger := ctrl.Log.WithName("Lorry K8S Exec client") - - containerName, err := intctrlutil.GetLorryContainerName(pod) - if err != nil { - logger.Info("not lorry in the pod, just return nil without error") - return nil, nil - } - - port, err := intctrlutil.GetLorryHTTPPort(pod) - if err != nil { - return nil, err - } - - streamOptions := cmdexec.StreamOptions{ - IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, - Stdin: true, - TTY: true, - PodName: pod.Name, - ContainerName: containerName, - Namespace: pod.Namespace, - } - - if restConfig == nil { - restConfig, err = ctrl.GetConfig() - if err != nil { - return nil, errors.Wrap(err, "get k8s config failed") - } - } - - restConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} - restConfig.APIPath = "/api" - restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() - restClient, err := rest.RESTClientFor(restConfig) - if err != nil { - return nil, errors.Wrap(err, "create k8s client failed") - } - - client := &K8sExecClient{ - StreamOptions: streamOptions, - lorryPort: port, - restConfig: restConfig, - restClient: restClient, - RequestTimeout: 10 * time.Second, - logger: logger, - Executor: &cmdexec.DefaultRemoteExecutor{}, - } - client.lorryClient = lorryClient{requester: client} - return client, nil -} - -// Request execs lorry operation, this is a blocking operation and use pod EXEC subresource to send a http request to the lorry pod -func (cli *K8sExecClient) Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) { - var ( - strBuffer bytes.Buffer - errBuffer bytes.Buffer - err error - ) - curlCmd := fmt.Sprintf("curl --fail-with-body --silent -X %s -H 'Content-Type: application/json' http://localhost:%d/v1.0/%s", - strings.ToUpper(method), cli.lorryPort, strings.ToLower(operation)) - - if len(req) != 0 { - jsonData, err := json.Marshal(req) - if err != nil { - return nil, err - } - // escape single quote - body := strings.ReplaceAll(string(jsonData), "'", "\\'") - curlCmd += fmt.Sprintf(" -d '%s'", body) - } - cmd := []string{"sh", "-c", "PATH=/kubeblocks:$PATH " + curlCmd} - - // redirect output to strBuffer to be parsed later - if err = cli.k8sExec(cmd, &strBuffer, &errBuffer); err != nil { - data := strBuffer.Bytes() - if len(data) != 0 { - // curl emits result message to output - return nil, errors.Wrap(err, string(data)) - } - - errData := errBuffer.Bytes() - if len(errData) != 0 { - return nil, errors.Wrap(err, string(errData)) - } - return nil, err - } - - data := strBuffer.Bytes() - if len(data) == 0 { - errData := errBuffer.Bytes() - if len(errData) != 0 { - cli.logger.Info("k8s exec error output", "message", string(errData)) - return nil, errors.New(string(errData)) - } - - return nil, nil - } - - result := map[string]any{} - if err := json.Unmarshal(data, &result); err != nil { - return nil, errors.Wrap(err, "decode result failed") - } - return result, nil -} - -func (cli *K8sExecClient) k8sExec(cmd []string, outWriter io.Writer, errWriter io.Writer) error { - // ensure we can recover the terminal while attached - t := cli.SetupTTY() - - var sizeQueue remotecommand.TerminalSizeQueue - if t.Raw { - // this call spawns a goroutine to monitor/update the terminal size - sizeQueue = t.MonitorSize(t.GetSize()) - - // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is - // true - cli.ErrOut = nil - } - - fn := func() error { - req := cli.restClient.Post(). - Resource("pods"). - Name(cli.PodName). - Namespace(cli.Namespace). - SubResource("exec") - req.VersionedParams(&corev1.PodExecOptions{ - Container: cli.ContainerName, - Command: cmd, - Stdin: cli.Stdin, - Stdout: outWriter != nil, - Stderr: errWriter != nil, - TTY: t.Raw, - }, scheme.ParameterCodec) - - return cli.Executor.Execute(req.URL(), cli.restConfig, cli.In, outWriter, errWriter, t.Raw, sizeQueue) - } - - if err := t.Safe(fn); err != nil { - return err - } - return nil -} diff --git a/pkg/lorry/client/suite_test.go b/pkg/lorry/client/suite_test.go deleted file mode 100644 index 6bde03d6fde..00000000000 --- a/pkg/lorry/client/suite_test.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package client - -import ( - "fmt" - "net" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - "github.com/valyala/fasthttp" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/httpserver" - opsregister "github.com/apecloud/kubeblocks/pkg/lorry/operations/register" -) - -var ( - lorryHTTPPort = 3501 - tcpListener net.Listener - dbManager engines.DBManager - mockDBManager *engines.MockDBManager - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") -} - -func TestLorryClient(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Lorry Client. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock db manager - InitMockDBManager() - - // Init mock dcs store - InitMockDCSStore() - - // Start lorry HTTP server - StartHTTPServer() -}) - -var _ = AfterSuite(func() { - StopHTTPServer() -}) - -func newTCPServer(port int) (net.Listener, int) { - var err error - for i := 0; i < 3; i++ { - tcpListener, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", port)) - if err == nil { - break - } - port++ - } - Expect(err).Should(BeNil()) - return tcpListener, port -} - -func StartHTTPServer() { - ops := opsregister.Operations() - s := httpserver.NewServer(ops) - handler := s.Router() - - listener, port := newTCPServer(lorryHTTPPort) - lorryHTTPPort = port - - go func() { - Expect(fasthttp.Serve(listener, handler)).Should(Succeed()) - }() -} - -func StopHTTPServer() { - if tcpListener == nil { - return - } - _ = tcpListener.Close() -} - -func InitMockDBManager() { - ctrl := gomock.NewController(GinkgoT()) - mockDBManager = engines.NewMockDBManager(ctrl) - register.SetDBManager(mockDBManager) - dbManager = mockDBManager -} - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} diff --git a/pkg/lorry/cronjobs/job.go b/pkg/lorry/cronjobs/job.go deleted file mode 100644 index fda7ccc9c3e..00000000000 --- a/pkg/lorry/cronjobs/job.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package cronjobs - -import ( - "context" - "strconv" - "time" - - "github.com/apecloud/kubeblocks/pkg/lorry/operations" -) - -type Job struct { - Name string - Ticker *time.Ticker - Operation operations.Operation - TimeoutSeconds int - PeriodSeconds int - SuccessThreshold int - FailureThreshold int -} - -func NewJob(name string, settings map[string]string) (*Job, error) { - operations := operations.Operations() - ops, ok := operations[name] - if !ok { - logger.Info("Operation not found", "name", name) - return nil, nil - } - job := &Job{ - Name: name, - Operation: ops, - TimeoutSeconds: 60, - PeriodSeconds: 60, - SuccessThreshold: 1, - FailureThreshold: 3, - } - - if v, ok := settings["timeoutSeconds"]; ok { - timeoutSeconds, err := strconv.Atoi(v) - if err != nil { - logger.Info("Failed to parse timeoutSeconds", "error", err.Error(), "job", name, "value", v) - return nil, err - } - job.TimeoutSeconds = timeoutSeconds - } - - if settings["periodSeconds"] != "" { - periodSeconds, err := strconv.Atoi(settings["periodSeconds"]) - if err != nil { - logger.Info("Failed to parse periodSeconds", "error", err.Error(), "job", name, "value", settings["periodSeconds"]) - return nil, err - } - job.PeriodSeconds = periodSeconds - } - - if settings["successThreshold"] != "" { - successThreshold, err := strconv.Atoi(settings["successThreshold"]) - if err != nil { - logger.Info("Failed to parse successThreshold", "error", err.Error(), "job", name, "value", settings["successThreshold"]) - return nil, err - } - job.SuccessThreshold = successThreshold - } - - if settings["failureThreshold"] != "" { - failureThreshold, err := strconv.Atoi(settings["failureThreshold"]) - if err != nil { - logger.Info("Failed to parse failureThreshold", "error", err.Error(), "job", name, "value", settings["failureThreshold"]) - return nil, err - } - job.FailureThreshold = failureThreshold - } - // operation is initialized in httpserver/apis.go - job.Operation.SetTimeout(time.Duration(job.TimeoutSeconds) * time.Second) - return job, nil -} - -func (job *Job) Start() { - job.Ticker = time.NewTicker(time.Duration(job.PeriodSeconds) * time.Second) - defer job.Ticker.Stop() - for range job.Ticker.C { - _, err := job.Operation.Do(context.Background(), nil) - if err != nil { - logger.Info("Failed to run job", "name", job.Name, "error", err.Error()) - // Handle error, e.g., increase failure count, stop job if failureThreshold is reached, etc. - } - } -} diff --git a/pkg/lorry/cronjobs/manager.go b/pkg/lorry/cronjobs/manager.go deleted file mode 100644 index 71113db3a74..00000000000 --- a/pkg/lorry/cronjobs/manager.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package cronjobs - -import ( - "encoding/json" - - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" -) - -type Manager struct { - Jobs map[string]*Job -} - -var logger = ctrl.Log.WithName("cronjobs") - -func NewManager() (*Manager, error) { - cronSettings := make(map[string]map[string]string) - jsonStr := viper.GetString(constant.KBEnvCronJobs) - if jsonStr == "" { - logger.Info("env is not set", "env", constant.KBEnvCronJobs) - return &Manager{}, nil - } - - err := json.Unmarshal([]byte(jsonStr), &cronSettings) - if err != nil { - logger.Info("Failed to unmarshal env", "name", constant.KBEnvCronJobs, "value", jsonStr, "error", err.Error()) - return nil, err - } - - jobs := make(map[string]*Job) - for name, setting := range cronSettings { - job, err := NewJob(name, setting) - if err != nil { - logger.Info("Failed to create job", "error", err.Error(), "name", name, "setting", setting) - } - jobs[name] = job - } - return &Manager{ - Jobs: jobs, - }, nil -} - -func (m *Manager) Start() { - for _, job := range m.Jobs { - go job.Start() - } -} diff --git a/pkg/lorry/ctl/createuser.go b/pkg/lorry/ctl/createuser.go deleted file mode 100644 index 7f3037c8f5c..00000000000 --- a/pkg/lorry/ctl/createuser.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type CreateUserOptions struct { - lorryAddr string - userName string - password string -} - -var createUserOptions = &CreateUserOptions{} - -var CreateUserCmd = &cobra.Command{ - Use: "createuser", - Short: "create user.", - Example: ` -lorryctl createuser --username xxx --password xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(createUserOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - err = lorryClient.CreateUser(context.TODO(), createUserOptions.userName, createUserOptions.password, "", "") - if err != nil { - fmt.Printf("create user failed: %v\n", err) - return - } - fmt.Printf("create user success") - }, -} - -func init() { - CreateUserCmd.Flags().StringVarP(&createUserOptions.userName, "username", "", "", "The name of user to create") - CreateUserCmd.Flags().StringVarP(&createUserOptions.password, "password", "", "", "The password of user to create") - CreateUserCmd.Flags().StringVarP(&createUserOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - CreateUserCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(CreateUserCmd) -} diff --git a/pkg/lorry/ctl/ctr.go b/pkg/lorry/ctl/ctr.go deleted file mode 100644 index c4b1a212ab9..00000000000 --- a/pkg/lorry/ctl/ctr.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const cliVersionTemplateString = "CLI version: %s \nRuntime version: %s\n" - -var RootCmd = &cobra.Command{ - Use: "lorryctl", - Short: "LORRY CLI", - Long: ` - / / ____ ____________ __ / ____/ /______/ / - / / / __ \/ ___/ ___/ / / / / / / __/ ___/ / - / /___/ /_/ / / / / / /_/ / / /___/ /_/ / / / -/_____/\____/_/ /_/ \__, / \____/\__/_/ /_/ - /____/ -=============================== -Lorry service client`, - Run: func(cmd *cobra.Command, _ []string) { - if versionFlag { - printVersion() - } else { - _ = cmd.Help() - } - }, -} - -type lorryVersion struct { - CliVersion string `json:"Cli version"` - RuntimeVersion string `json:"Runtime version"` -} - -var ( - cliVersion string - versionFlag bool - lorryVer lorryVersion - lorryRuntimePath string -) - -// Execute adds all child commands to the root command. -func Execute(cliVersion, apiVersion string) { - lorryVer = lorryVersion{ - CliVersion: cliVersion, - RuntimeVersion: apiVersion, - } - - cobra.OnInitialize(initConfig) - - setVersion() - - if err := RootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(-1) - } -} - -func setVersion() { - template := fmt.Sprintf(cliVersionTemplateString, lorryVer.CliVersion, lorryVer.RuntimeVersion) - RootCmd.SetVersionTemplate(template) -} - -func printVersion() { - fmt.Printf(cliVersionTemplateString, lorryVer.CliVersion, lorryVer.RuntimeVersion) -} - -func initConfig() { - // err intentionally ignored since lorry may not yet be installed. - runtimeVer := GetRuntimeVersion() - - lorryVer = lorryVersion{ - // Set in Execute() method in this file before initConfig() is called by cmd.Execute(). - CliVersion: cliVersion, - RuntimeVersion: strings.ReplaceAll(runtimeVer, "\n", ""), - } - - viper.SetEnvPrefix("lorry") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() -} - -func init() { - RootCmd.PersistentFlags().StringVarP(&lorryRuntimePath, "kb-runtime-dir", "", "/kubeblocks/", "The directory of kubeblocks binaries") -} - -// GetRuntimeVersion returns the version for the local lorry runtime. -func GetRuntimeVersion() string { - lorryCMD := filepath.Join(lorryRuntimePath, "lorry") - - out, err := exec.Command(lorryCMD, "--version").Output() - if err != nil { - return "n/a\n" - } - return string(out) -} diff --git a/pkg/lorry/ctl/deleteuser.go b/pkg/lorry/ctl/deleteuser.go deleted file mode 100644 index 8abfb1fda0d..00000000000 --- a/pkg/lorry/ctl/deleteuser.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type DeleteUserOptions struct { - lorryAddr string - userName string -} - -var deleteUserOptions = &DeleteUserOptions{} - -var DeleteUserCmd = &cobra.Command{ - Use: "deleteuser", - Short: "delete user.", - Example: ` -lorryctl deleteuser --username xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(deleteUserOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - err = lorryClient.DeleteUser(context.TODO(), deleteUserOptions.userName) - if err != nil { - fmt.Printf("delete user failed: %v\n", err) - return - } - fmt.Printf("delete user success") - }, -} - -func init() { - DeleteUserCmd.Flags().StringVarP(&deleteUserOptions.userName, "username", "", "", "The name of user to delete") - DeleteUserCmd.Flags().StringVarP(&deleteUserOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - DeleteUserCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(DeleteUserCmd) -} diff --git a/pkg/lorry/ctl/describeuser.go b/pkg/lorry/ctl/describeuser.go deleted file mode 100644 index 1920c63eb3f..00000000000 --- a/pkg/lorry/ctl/describeuser.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type DescribeUserOptions struct { - lorryAddr string - userName string -} - -var describeUserOptions = &DescribeUserOptions{} - -var DescribeUserCmd = &cobra.Command{ - Use: "describeuser", - Short: "describe user.", - Example: ` -lorryctl describeuser --username xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(describeUserOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - user, err := lorryClient.DescribeUser(context.TODO(), describeUserOptions.userName) - if err != nil { - fmt.Printf("describe user failed: %v\n", err) - return - } - fmt.Println("describe user success:") - for k, v := range user { - fmt.Printf("%s: %v\n", k, v) - } - }, -} - -func init() { - DescribeUserCmd.Flags().StringVarP(&describeUserOptions.userName, "username", "", "", "The name of user to describe") - DescribeUserCmd.Flags().StringVarP(&describeUserOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - DescribeUserCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(DescribeUserCmd) -} diff --git a/pkg/lorry/ctl/grantrole.go b/pkg/lorry/ctl/grantrole.go deleted file mode 100644 index 505c2b60e00..00000000000 --- a/pkg/lorry/ctl/grantrole.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type GrantUserRoleOptions struct { - lorryAddr string - userName string - roleName string -} - -var grantUserRoleOptions = &GrantUserRoleOptions{} - -var GrantUserRoleCmd = &cobra.Command{ - Use: "grant-role", - Short: "grant user role.", - Example: ` -lorryctl grant-role --username xxx --rolename xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(grantUserRoleOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - err = lorryClient.GrantUserRole(context.TODO(), grantUserRoleOptions.userName, grantUserRoleOptions.roleName) - if err != nil { - fmt.Printf("grant user role failed: %v\n", err) - return - } - fmt.Printf("grant user role success") - }, -} - -func init() { - GrantUserRoleCmd.Flags().StringVarP(&grantUserRoleOptions.userName, "username", "", "", "The name of user to grant") - GrantUserRoleCmd.Flags().StringVarP(&grantUserRoleOptions.roleName, "rolename", "", "", "The name of role to grant") - GrantUserRoleCmd.Flags().StringVarP(&grantUserRoleOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - GrantUserRoleCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(GrantUserRoleCmd) -} diff --git a/pkg/lorry/ctl/listsystemaccounts.go b/pkg/lorry/ctl/listsystemaccounts.go deleted file mode 100644 index 4a98ba9d48a..00000000000 --- a/pkg/lorry/ctl/listsystemaccounts.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -var ( - lorryAddr1 string -) - -var ListSystemAccountsCmd = &cobra.Command{ - Use: "listsystemaccounts", - Short: "list system accounts.", - Example: ` -lorryctl listsystemaccounts - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(lorryAddr1) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - systemaccounts, err := lorryClient.ListSystemAccounts(context.TODO()) - if err != nil { - fmt.Printf("list systemaccounts failed: %v\n", err) - return - } - fmt.Printf("list systemaccounts:\n") - for _, m := range systemaccounts { - fmt.Println("-------------------------") - for k, v := range m { - fmt.Printf("%s: %v\n", k, v) - } - } - }, -} - -func init() { - ListSystemAccountsCmd.Flags().StringVarP(&lorryAddr1, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - ListSystemAccountsCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(ListSystemAccountsCmd) -} diff --git a/pkg/lorry/ctl/listusers.go b/pkg/lorry/ctl/listusers.go deleted file mode 100644 index ddd38915f67..00000000000 --- a/pkg/lorry/ctl/listusers.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -var ( - lorryAddr string -) - -var ListUsersCmd = &cobra.Command{ - Use: "listusers", - Short: "list normal users.", - Example: ` -lorryctl listusers - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - users, err := lorryClient.ListUsers(context.TODO()) - if err != nil { - fmt.Printf("list users failed: %v\n", err) - return - } - fmt.Printf("list users:\n") - for _, m := range users { - fmt.Println("-------------------------") - for k, v := range m { - fmt.Printf("%s: %v\n", k, v) - } - } - }, -} - -func init() { - ListUsersCmd.Flags().StringVarP(&lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - ListUsersCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(ListUsersCmd) -} diff --git a/pkg/lorry/ctl/options.go b/pkg/lorry/ctl/options.go deleted file mode 100644 index 125a8c8b2de..00000000000 --- a/pkg/lorry/ctl/options.go +++ /dev/null @@ -1,344 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "bufio" - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "reflect" - "sync" - "syscall" - "time" -) - -// Options represents the application configuration parameters. -type Options struct { - HTTPPort int `env:"DAPR_HTTP_PORT" arg:"dapr-http-port"` - GRPCPort int `env:"DAPR_GRPC_PORT" arg:"dapr-grpc-port"` - ConfigFile string `arg:"config"` - Protocol string `arg:"app-protocol"` - Arguments []string - LogLevel string `arg:"log-level"` - ComponentsPath string `arg:"components-path"` - InternalGRPCPort int `arg:"dapr-internal-grpc-port"` - EnableAppHealth bool `arg:"enable-app-health-check"` - AppHealthThreshold int `arg:"app-health-threshold" ifneq:"0"` -} - -func (options *Options) validate() error { - return nil -} - -func (options *Options) getArgs() []string { - args := []string{"--app-id", "dbservice"} - schema := reflect.ValueOf(*options) - for i := 0; i < schema.NumField(); i++ { - valueField := schema.Field(i).Interface() - typeField := schema.Type().Field(i) - key := typeField.Tag.Get("arg") - if len(key) == 0 { - continue - } - key = "--" + key - - ifneq, hasIfneq := typeField.Tag.Lookup("ifneq") - - switch valueField.(type) { - case bool: - if valueField == true { - args = append(args, key) - } - default: - value := fmt.Sprintf("%v", reflect.ValueOf(valueField)) - if len(value) != 0 && (!hasIfneq || value != ifneq) { - args = append(args, key, value) - } - } - } - - return args -} - -func (options *Options) getEnv() []string { - env := []string{} - schema := reflect.ValueOf(*options) - for i := 0; i < schema.NumField(); i++ { - valueField := schema.Field(i).Interface() - typeField := schema.Type().Field(i) - key := typeField.Tag.Get("env") - if len(key) == 0 { - continue - } - if value, ok := valueField.(int); ok && value <= 0 { - // ignore unset numeric variables. - continue - } - - value := fmt.Sprintf("%v", reflect.ValueOf(valueField)) - env = append(env, fmt.Sprintf("%s=%v", key, value)) - } - return env -} - -// Commands represents the managed subprocesses. -type Commands struct { - ctx context.Context - WaitGroup sync.WaitGroup - LorryCMD *exec.Cmd - LorryErr error - LorryHTTPPort int - LorryGRPCPort int - LorryStarted chan bool - AppCMD *exec.Cmd - AppErr error - AppStarted chan bool - Options *Options - SigCh chan os.Signal - // if it's false, lorryctl will not restart db service - restartDB bool -} - -func getLorryCommand(options *Options) (*exec.Cmd, error) { - lorryCMD := filepath.Join(lorryRuntimePath, "lorry") - args := options.getArgs() - cmd := exec.Command(lorryCMD, args...) - return cmd, nil -} - -func getAppCommand(options *Options) *exec.Cmd { - argCount := len(options.Arguments) - - if argCount == 0 { - return nil - } - command := options.Arguments[0] - - args := []string{} - if argCount > 1 { - args = options.Arguments[1:] - } - - cmd := exec.Command(command, args...) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, options.getEnv()...) - - return cmd -} - -func newCommands(ctx context.Context, options *Options) (*Commands, error) { - //nolint - err := options.validate() - if err != nil { - return nil, err - } - - lorryCMD, err := getLorryCommand(options) - if err != nil { - return nil, err - } - - //nolint - var appCMD *exec.Cmd = getAppCommand(options) - cmd := &Commands{ - ctx: ctx, - LorryCMD: lorryCMD, - LorryErr: nil, - LorryHTTPPort: options.HTTPPort, - LorryGRPCPort: options.GRPCPort, - LorryStarted: make(chan bool, 1), - AppCMD: appCMD, - AppErr: nil, - AppStarted: make(chan bool, 1), - Options: options, - SigCh: make(chan os.Signal, 1), - restartDB: true, - } - return cmd, nil -} - -func (commands *Commands) StartLorry() { - var startInfo string - fmt.Fprintf(os.Stdout, "Starting Lorry HTTP Port: %v. gRPC Port: %v\n", - commands.LorryHTTPPort, - commands.LorryGRPCPort) - fmt.Fprintln(os.Stdout, startInfo) - - commands.LorryCMD.Stdout = os.Stdout - commands.LorryCMD.Stderr = os.Stderr - - err := commands.LorryCMD.Start() - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) - } - - commands.LorryStarted <- true -} - -func (commands *Commands) RestartDBServiceIfExited() { - if commands.AppCMD == nil { - return - } - commands.WaitGroup.Add(1) - for { - select { - case <-commands.ctx.Done(): - commands.WaitGroup.Done() - return - default: - } - - if commands.AppCMD.ProcessState != nil { - Printf("DB service exits: %v\n", commands.AppCMD.ProcessState) - if !commands.restartDB { - Printf("restart DB service: %v\n", commands.restartDB) - time.Sleep(2 * time.Second) - continue - } - - status, ok := commands.AppCMD.ProcessState.Sys().(syscall.WaitStatus) - if commands.AppCMD.ProcessState.Exited() || (ok && status.Signaled()) { - commands.RestartDBService() - } - } - time.Sleep(1 * time.Second) - } -} - -func (commands *Commands) RestartDBService() { - if commands.AppCMD == nil { - return - } - Printf("DB service restart: %v\n", commands.AppCMD) - // commands.StopDBService() - commands.AppCMD = getAppCommand(commands.Options) - commands.AppErr = nil - commands.AppStarted = make(chan bool, 1) - commands.StartDBService() - -} - -func (commands *Commands) StartDBService() { - if commands.AppCMD == nil { - commands.AppStarted <- true - return - } - - stdErrPipe, pipeErr := commands.AppCMD.StderrPipe() - if pipeErr != nil { - fmt.Fprintf(os.Stderr, "Error creating stderr for DB Service: %s\n", pipeErr.Error()) - commands.AppStarted <- false - return - } - - stdOutPipe, pipeErr := commands.AppCMD.StdoutPipe() - if pipeErr != nil { - fmt.Fprintf(os.Stderr, "Error creating stdout for DB Service: %s\n", pipeErr.Error()) - commands.AppStarted <- false - return - } - - errScanner := bufio.NewScanner(stdErrPipe) - outScanner := bufio.NewScanner(stdOutPipe) - go func() { - for errScanner.Scan() { - fmt.Printf("== DB Service err == %s\n", errScanner.Text()) - } - }() - - go func() { - for outScanner.Scan() { - fmt.Printf("== DB Service == %s\n", outScanner.Text()) - } - }() - - err := commands.AppCMD.Start() - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - commands.AppStarted <- false - return - } - commands.AppStarted <- true - go commands.WaitDBService() -} - -func (commands *Commands) StopLorry() bool { - exitWithError := false - if commands.LorryErr != nil { - exitWithError = true - fmt.Fprintf(os.Stderr, "Error exiting Lorry: %s\n", commands.LorryErr) - } else if commands.LorryCMD.ProcessState == nil || !commands.LorryCMD.ProcessState.Exited() { - err := commands.LorryCMD.Process.Signal(syscall.SIGTERM) - if err != nil { - exitWithError = true - fmt.Fprintf(os.Stderr, "Error exiting Lorry: %s\n", err) - } else { - fmt.Fprintln(os.Stdout, "Send SIGTERM to Lorry") - } - } - // state, err = commands.LorryCMD.Process.Wait() - // fmt.Printf("state: %v, err: %v\n", state, err) - commands.WaitLorry() - return exitWithError -} - -func (commands *Commands) StopDBService() bool { - exitWithError := false - if commands.AppErr != nil { - exitWithError = true - fmt.Fprintf(os.Stderr, "Error exiting App: %s\n", commands.AppErr) - } else if commands.AppCMD != nil && (commands.AppCMD.ProcessState == nil || !commands.AppCMD.ProcessState.Exited()) { - err := commands.AppCMD.Process.Signal(syscall.SIGTERM) - if err != nil { - exitWithError = true - fmt.Fprintf(os.Stderr, "Error exiting App: %s\n", err) - } else { - fmt.Fprintln(os.Stdout, "Exited App successfully") - } - } - commands.WaitDBService() - return exitWithError -} - -func (commands *Commands) WaitDBService() { - commands.AppErr = waitCmd(commands.AppCMD) -} - -func (commands *Commands) WaitLorry() { - commands.LorryErr = waitCmd(commands.LorryCMD) -} - -func waitCmd(cmd *exec.Cmd) error { - if cmd == nil || (cmd.ProcessState != nil && cmd.ProcessState.Exited()) { - return nil - } - - err := cmd.Wait() - if err != nil { - fmt.Fprintf(os.Stderr, "The command [%s] exited with error code: %s\n", cmd.String(), err.Error()) - } else { - fmt.Fprintf(os.Stdout, "The command [%s] exited\n", cmd.String()) - } - return err -} diff --git a/pkg/lorry/ctl/revokerole.go b/pkg/lorry/ctl/revokerole.go deleted file mode 100644 index 60276eec182..00000000000 --- a/pkg/lorry/ctl/revokerole.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type RevokeUserRoleOptions struct { - lorryAddr string - userName string - roleName string -} - -var revokeUserRoleOptions = &RevokeUserRoleOptions{} - -var RevokeUserRoleCmd = &cobra.Command{ - Use: "revoke-role", - Short: "revoke user role.", - Example: ` -lorryctl revoke-role --username xxx --rolename xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - lorryClient, err := client.NewHTTPClientWithURL(revokeUserRoleOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - err = lorryClient.RevokeUserRole(context.TODO(), revokeUserRoleOptions.userName, revokeUserRoleOptions.roleName) - if err != nil { - fmt.Printf("revoke user role failed: %v\n", err) - return - } - fmt.Printf("revoke user role success") - }, -} - -func init() { - RevokeUserRoleCmd.Flags().StringVarP(&revokeUserRoleOptions.userName, "username", "", "", "The name of user to revoke") - RevokeUserRoleCmd.Flags().StringVarP(&revokeUserRoleOptions.roleName, "rolename", "", "", "The name of role to revoke") - RevokeUserRoleCmd.Flags().StringVarP(&revokeUserRoleOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - RevokeUserRoleCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(RevokeUserRoleCmd) -} diff --git a/pkg/lorry/ctl/run.go b/pkg/lorry/ctl/run.go deleted file mode 100644 index 40a7a54d3d3..00000000000 --- a/pkg/lorry/ctl/run.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - "os" - "os/signal" - "strings" - "syscall" - - "github.com/spf13/cobra" -) - -var ( - configFile string - port int - grpcPort int - internalGRPCPort int - logLevel string - componentsPath string - enableAppHealth bool -) - -var RunCmd = &cobra.Command{ - Use: "run", - Short: "Run Lorry and db service.", - Example: ` -lorryctl run -- mysqld - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("WARNING: no DB Service found.") - } - ctx, cancel := context.WithCancel(context.Background()) - - commands, err := newCommands(ctx, &Options{ - HTTPPort: port, - GRPCPort: grpcPort, - ConfigFile: configFile, - Arguments: args, - LogLevel: logLevel, - ComponentsPath: componentsPath, - EnableAppHealth: enableAppHealth, - InternalGRPCPort: internalGRPCPort, - }) - if err != nil { - fmt.Fprint(os.Stderr, err.Error()) - os.Exit(1) - } - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) - - go commands.StartLorry() - <-commands.LorryStarted - - go commands.StartDBService() - <-commands.AppStarted - go commands.RestartDBServiceIfExited() - - if commands.AppCMD != nil { - appCommand := strings.Join(args, " ") - fmt.Fprintf(os.Stdout, "Start DB Service with %s.\n", appCommand) - fmt.Fprintf(os.Stdout, "Lorry logs and DB logs will appear here.\n") - } else { - fmt.Fprintf(os.Stdout, "Lorry logs will appear here.\n") - } - - sig := <-sigCh - fmt.Printf("\n %v signal received: shutting down\n", sig) - cancel() - commands.WaitGroup.Wait() - - exitWithError := commands.StopLorry() || commands.StopDBService() - if exitWithError { - os.Exit(1) - } - }, -} - -func init() { - RunCmd.Flags().StringVarP(&configFile, "config", "c", "/kubeblocks/config/probe/config.yaml", "Dapr configuration file") - RunCmd.Flags().IntVarP(&port, "dapr-http-port", "H", -1, "The HTTP port for Dapr to listen on") - RunCmd.Flags().IntVarP(&grpcPort, "dapr-grpc-port", "G", -1, "The gRPC port for Dapr to listen on") - RunCmd.Flags().IntVarP(&internalGRPCPort, "dapr-internal-grpc-port", "I", 56471, "The gRPC port for the Dapr internal API to listen on") - RunCmd.Flags().StringVarP(&logLevel, "log-level", "", "info", "The log verbosity. Valid values are: debug, info, warn, error, fatal, or panic") - RunCmd.Flags().StringVarP(&componentsPath, "components-path", "d", "/kubeblocks/config/probe/components", "The path for components directory") - RunCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(RunCmd) -} diff --git a/pkg/lorry/ctl/switchover.go b/pkg/lorry/ctl/switchover.go deleted file mode 100644 index 988b4e8623a..00000000000 --- a/pkg/lorry/ctl/switchover.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "context" - "fmt" - "time" - - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -type SwitchOptions struct { - primary string - candidate string - lorryAddr string - force bool -} - -var switchOptions = &SwitchOptions{} -var SwitchCmd = &cobra.Command{ - Use: "switchover", - Short: "execute a switchover request.", - Example: ` -lorryctl switchover --primary xxx --candidate xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - if switchOptions.primary == "" && switchOptions.candidate == "" { - fmt.Println("Primary or Candidate must be specified") - return - } - - lorryClient, err := client.NewHTTPClientWithURL(switchOptions.lorryAddr) - if err != nil { - fmt.Printf("new lorry http client failed: %v\n", err) - return - } - - lorryClient.ReconcileTimeout = 30 * time.Second - err = lorryClient.Switchover(context.TODO(), switchOptions.primary, switchOptions.candidate, switchOptions.force) - if err != nil { - fmt.Printf("switchover failed: %v\n", err) - return - } - fmt.Printf("switchover success\n") - }, -} - -func init() { - SwitchCmd.Flags().StringVarP(&switchOptions.primary, "primary", "p", "", "The primary pod name") - SwitchCmd.Flags().StringVarP(&switchOptions.candidate, "candidate", "c", "", "The candidate pod name") - SwitchCmd.Flags().BoolVarP(&switchOptions.force, "force", "f", false, "force to swithover if failed") - SwitchCmd.Flags().StringVarP(&switchOptions.lorryAddr, "lorry-addr", "", "http://localhost:3501/v1.0/", "The addr of lorry to request") - SwitchCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(SwitchCmd) -} diff --git a/pkg/lorry/ctl/util.go b/pkg/lorry/ctl/util.go deleted file mode 100644 index 3d273a1f809..00000000000 --- a/pkg/lorry/ctl/util.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "fmt" - "os" - "time" -) - -func Printf(format string, args ...any) { - fmt.Fprintf(os.Stdout, "["+time.Now().Format("2006-01-02T15:04:05 -07:00:00")+"] "+format, args...) -} diff --git a/pkg/lorry/ctl/vault_plugin.go b/pkg/lorry/ctl/vault_plugin.go deleted file mode 100644 index d19c2d2fde8..00000000000 --- a/pkg/lorry/ctl/vault_plugin.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package ctl - -import ( - "os" - - hclog "github.com/hashicorp/go-hclog" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/spf13/cobra" - - "github.com/apecloud/kubeblocks/pkg/lorry/vault" -) - -// Run instantiates a lorry vault plugin object, and runs the RPC server for the plugin -func Run() error { - db, err := vault.New() - if err != nil { - return err - } - - dbplugin.Serve(db.(dbplugin.Database)) - - return nil -} - -type VaultPluginOptions struct { - primary string - candidate string - lorryAddr string -} - -var vaultPluginOptions = &VaultPluginOptions{} -var VaultPluginCmd = &cobra.Command{ - Use: "vault-plugin", - Short: "run a vault-plugin service.", - Example: ` -lorryctl vault-plugin --primary xxx --candidate xxx - `, - Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { - err := Run() - if err != nil { - logger := hclog.New(&hclog.LoggerOptions{}) - - logger.Error("plugin shutting down", "error", err) - os.Exit(1) - } - }, -} - -func init() { - VaultPluginCmd.Flags().StringVarP(&vaultPluginOptions.primary, "primary", "l", "", "The primary pod name") - VaultPluginCmd.Flags().StringVarP(&vaultPluginOptions.candidate, "candidate", "c", "", "The candidate pod name") - VaultPluginCmd.Flags().StringVarP(&vaultPluginOptions.lorryAddr, "lorry-addr", "", "localhost:3501", "The addr of lorry to request") - VaultPluginCmd.Flags().BoolP("help", "h", false, "Print this help message") - - RootCmd.AddCommand(VaultPluginCmd) -} diff --git a/pkg/lorry/dcs/dcs.go b/pkg/lorry/dcs/dcs.go deleted file mode 100644 index 9e137ef6a64..00000000000 --- a/pkg/lorry/dcs/dcs.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package dcs - -import ( - "github.com/apecloud/kubeblocks/pkg/constant" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -type DCS interface { - Initialize() error - - // cluster manage functions - GetClusterName() string - GetCluster() (*Cluster, error) - GetClusterFromCache() *Cluster - ResetCluster() - DeleteCluster() - - // cluster scole ha config - GetHaConfig() (*HaConfig, error) - UpdateHaConfig() error - - // member manager functions - GetMembers() ([]Member, error) - AddCurrentMember() error - - // manual switchover - GetSwitchover() (*Switchover, error) - CreateSwitchover(string, string) error - DeleteSwitchover() error - - // cluster scope leader lock - AttemptAcquireLease() error - CreateLease() error - IsLeaseExist() (bool, error) - HasLease() bool - ReleaseLease() error - UpdateLease() error - - GetLeader() (*Leader, error) -} - -var dcs DCS - -func init() { - viper.SetDefault(constant.KBEnvTTL, 15) - viper.SetDefault(constant.KBEnvMaxLag, 10) - viper.SetDefault(constant.KubernetesClusterDomainEnv, constant.DefaultDNSDomain) -} - -func SetStore(d DCS) { - dcs = d -} - -func GetStore() DCS { - return dcs -} - -func InitStore() error { - store, err := NewKubernetesStore() - if err != nil { - return err - } - dcs = store - return nil -} diff --git a/pkg/lorry/dcs/dcs_mock.go b/pkg/lorry/dcs/dcs_mock.go deleted file mode 100644 index 992ea24c0d0..00000000000 --- a/pkg/lorry/dcs/dcs_mock.go +++ /dev/null @@ -1,337 +0,0 @@ -// /* -// Copyright (C) 2022-2024 ApeCloud Co., Ltd -// -// This file is part of KubeBlocks project -// -// 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 . -// */ -// -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/apecloud/kubeblocks/pkg/lorry/dcs (interfaces: DCS) - -// Package dcs is a generated GoMock package. -package dcs - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockDCS is a mock of DCS interface. -type MockDCS struct { - ctrl *gomock.Controller - recorder *MockDCSMockRecorder -} - -// MockDCSMockRecorder is the mock recorder for MockDCS. -type MockDCSMockRecorder struct { - mock *MockDCS -} - -// NewMockDCS creates a new mock instance. -func NewMockDCS(ctrl *gomock.Controller) *MockDCS { - mock := &MockDCS{ctrl: ctrl} - mock.recorder = &MockDCSMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDCS) EXPECT() *MockDCSMockRecorder { - return m.recorder -} - -// AddCurrentMember mocks base method. -func (m *MockDCS) AddCurrentMember() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddCurrentMember") - ret0, _ := ret[0].(error) - return ret0 -} - -// AddCurrentMember indicates an expected call of AddCurrentMember. -func (mr *MockDCSMockRecorder) AddCurrentMember() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCurrentMember", reflect.TypeOf((*MockDCS)(nil).AddCurrentMember)) -} - -// AttemptAcquireLease mocks base method. -func (m *MockDCS) AttemptAcquireLease() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AttemptAcquireLease") - ret0, _ := ret[0].(error) - return ret0 -} - -// AttemptAcquireLease indicates an expected call of AttemptAcquireLease. -func (mr *MockDCSMockRecorder) AttemptAcquireLease() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AttemptAcquireLease", reflect.TypeOf((*MockDCS)(nil).AttemptAcquireLease)) -} - -// CreateLease mocks base method. -func (m *MockDCS) CreateLease() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateLease") - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateLease indicates an expected call of CreateLease. -func (mr *MockDCSMockRecorder) CreateLease() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLease", reflect.TypeOf((*MockDCS)(nil).CreateLease)) -} - -// CreateSwitchover mocks base method. -func (m *MockDCS) CreateSwitchover(arg0, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSwitchover", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateSwitchover indicates an expected call of CreateSwitchover. -func (mr *MockDCSMockRecorder) CreateSwitchover(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSwitchover", reflect.TypeOf((*MockDCS)(nil).CreateSwitchover), arg0, arg1) -} - -// DeleteCluster mocks base method. -func (m *MockDCS) DeleteCluster() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteCluster") -} - -// DeleteCluster indicates an expected call of DeleteCluster. -func (mr *MockDCSMockRecorder) DeleteCluster() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockDCS)(nil).DeleteCluster)) -} - -// DeleteSwitchover mocks base method. -func (m *MockDCS) DeleteSwitchover() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSwitchover") - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSwitchover indicates an expected call of DeleteSwitchover. -func (mr *MockDCSMockRecorder) DeleteSwitchover() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSwitchover", reflect.TypeOf((*MockDCS)(nil).DeleteSwitchover)) -} - -// GetCluster mocks base method. -func (m *MockDCS) GetCluster() (*Cluster, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCluster") - ret0, _ := ret[0].(*Cluster) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCluster indicates an expected call of GetCluster. -func (mr *MockDCSMockRecorder) GetCluster() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCluster", reflect.TypeOf((*MockDCS)(nil).GetCluster)) -} - -// GetClusterFromCache mocks base method. -func (m *MockDCS) GetClusterFromCache() *Cluster { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetClusterFromCache") - ret0, _ := ret[0].(*Cluster) - return ret0 -} - -// GetClusterFromCache indicates an expected call of GetClusterFromCache. -func (mr *MockDCSMockRecorder) GetClusterFromCache() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterFromCache", reflect.TypeOf((*MockDCS)(nil).GetClusterFromCache)) -} - -// GetClusterName mocks base method. -func (m *MockDCS) GetClusterName() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetClusterName") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetClusterName indicates an expected call of GetClusterName. -func (mr *MockDCSMockRecorder) GetClusterName() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterName", reflect.TypeOf((*MockDCS)(nil).GetClusterName)) -} - -// GetHaConfig mocks base method. -func (m *MockDCS) GetHaConfig() (*HaConfig, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetHaConfig") - ret0, _ := ret[0].(*HaConfig) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetHaConfig indicates an expected call of GetHaConfig. -func (mr *MockDCSMockRecorder) GetHaConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHaConfig", reflect.TypeOf((*MockDCS)(nil).GetHaConfig)) -} - -// GetLeader mocks base method. -func (m *MockDCS) GetLeader() (*Leader, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLeader") - ret0, _ := ret[0].(*Leader) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLeader indicates an expected call of GetLeader. -func (mr *MockDCSMockRecorder) GetLeader() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeader", reflect.TypeOf((*MockDCS)(nil).GetLeader)) -} - -// GetMembers mocks base method. -func (m *MockDCS) GetMembers() ([]Member, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMembers") - ret0, _ := ret[0].([]Member) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMembers indicates an expected call of GetMembers. -func (mr *MockDCSMockRecorder) GetMembers() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembers", reflect.TypeOf((*MockDCS)(nil).GetMembers)) -} - -// GetSwitchover mocks base method. -func (m *MockDCS) GetSwitchover() (*Switchover, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSwitchover") - ret0, _ := ret[0].(*Switchover) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSwitchover indicates an expected call of GetSwitchover. -func (mr *MockDCSMockRecorder) GetSwitchover() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSwitchover", reflect.TypeOf((*MockDCS)(nil).GetSwitchover)) -} - -// HasLease mocks base method. -func (m *MockDCS) HasLease() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasLease") - ret0, _ := ret[0].(bool) - return ret0 -} - -// HasLease indicates an expected call of HasLease. -func (mr *MockDCSMockRecorder) HasLease() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasLease", reflect.TypeOf((*MockDCS)(nil).HasLease)) -} - -// Initialize mocks base method. -func (m *MockDCS) Initialize() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Initialize") - ret0, _ := ret[0].(error) - return ret0 -} - -// Initialize indicates an expected call of Initialize. -func (mr *MockDCSMockRecorder) Initialize() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Initialize", reflect.TypeOf((*MockDCS)(nil).Initialize)) -} - -// IsLeaseExist mocks base method. -func (m *MockDCS) IsLeaseExist() (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsLeaseExist") - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsLeaseExist indicates an expected call of IsLeaseExist. -func (mr *MockDCSMockRecorder) IsLeaseExist() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLeaseExist", reflect.TypeOf((*MockDCS)(nil).IsLeaseExist)) -} - -// ReleaseLease mocks base method. -func (m *MockDCS) ReleaseLease() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReleaseLease") - ret0, _ := ret[0].(error) - return ret0 -} - -// ReleaseLease indicates an expected call of ReleaseLease. -func (mr *MockDCSMockRecorder) ReleaseLease() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseLease", reflect.TypeOf((*MockDCS)(nil).ReleaseLease)) -} - -// ResetCluster mocks base method. -func (m *MockDCS) ResetCluster() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "ResetCluster") -} - -// ResetCluster indicates an expected call of ResetCluster. -func (mr *MockDCSMockRecorder) ResetCluster() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetCluster", reflect.TypeOf((*MockDCS)(nil).ResetCluster)) -} - -// UpdateHaConfig mocks base method. -func (m *MockDCS) UpdateHaConfig() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateHaConfig") - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateHaConfig indicates an expected call of UpdateHaConfig. -func (mr *MockDCSMockRecorder) UpdateHaConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateHaConfig", reflect.TypeOf((*MockDCS)(nil).UpdateHaConfig)) -} - -// UpdateLease mocks base method. -func (m *MockDCS) UpdateLease() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateLease") - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateLease indicates an expected call of UpdateLease. -func (mr *MockDCSMockRecorder) UpdateLease() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLease", reflect.TypeOf((*MockDCS)(nil).UpdateLease)) -} diff --git a/pkg/lorry/dcs/generate.go b/pkg/lorry/dcs/generate.go deleted file mode 100644 index 27284ffd097..00000000000 --- a/pkg/lorry/dcs/generate.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package dcs - -//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package dcs -destination dcs_mock.go github.com/apecloud/kubeblocks/pkg/lorry/dcs DCS diff --git a/pkg/lorry/dcs/k8s.go b/pkg/lorry/dcs/k8s.go deleted file mode 100644 index 6cd46a06a63..00000000000 --- a/pkg/lorry/dcs/k8s.go +++ /dev/null @@ -1,721 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package dcs - -import ( - "context" - "encoding/json" - "fmt" - "os" - "strconv" - "time" - - "github.com/go-logr/logr" - - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - k8s "github.com/apecloud/kubeblocks/pkg/lorry/util/kubernetes" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -type KubernetesStore struct { - ctx context.Context - clusterName string - componentName string - clusterCompName string - currentMemberName string - namespace string - cluster *Cluster - client *rest.RESTClient - clientset *kubernetes.Clientset - LeaderObservedTime int64 - logger logr.Logger - IsLeaderClusterWide bool -} - -func NewKubernetesStore() (*KubernetesStore, error) { - ctx := context.Background() - logger := ctrl.Log.WithName("DCS-K8S") - clientset, err := k8s.GetClientSet() - if err != nil { - err = errors.Wrap(err, "clientset init failed") - return nil, err - } - client, err := k8s.GetRESTClientForKB() - if err != nil { - err = errors.Wrap(err, "restClient init failed") - return nil, err - } - - clusterName := os.Getenv(constant.KBEnvClusterName) - if clusterName == "" { - return nil, errors.New(fmt.Sprintf("%s must be set", constant.KBEnvClusterName)) - } - - componentName := os.Getenv(constant.KBEnvCompName) - if componentName == "" { - return nil, errors.New(fmt.Sprintf("%s must be set", constant.KBEnvCompName)) - } - - clusterCompName := os.Getenv(constant.KBEnvClusterCompName) - if clusterCompName == "" { - clusterCompName = clusterName + "-" + componentName - } - - currentMemberName := os.Getenv(constant.KBEnvPodName) - if currentMemberName == "" { - return nil, errors.New(fmt.Sprintf("%s must be set", constant.KBEnvPodName)) - } - - namespace := os.Getenv(constant.KBEnvNamespace) - if namespace == "" { - return nil, errors.New("KB_NAMESPACE must be set") - } - - isLeaderClusterWide := false - // characterType := viper.GetString(constant.KBEnvCharacterType) - // if viper.IsSet(constant.KBEnvBuiltinHandler) { - // characterType = viper.GetString(constant.KBEnvBuiltinHandler) - // } - // if characterType == string(models.Oceanbase) { - // isLeaderClusterWide = true - // } - - store := &KubernetesStore{ - ctx: ctx, - clusterName: clusterName, - componentName: componentName, - clusterCompName: clusterCompName, - currentMemberName: currentMemberName, - namespace: namespace, - client: client, - clientset: clientset, - logger: logger, - IsLeaderClusterWide: isLeaderClusterWide, - } - return store, err -} - -func (store *KubernetesStore) Initialize() error { - store.logger.Info("k8s store initializing") - _, err := store.GetCluster() - if err != nil { - return err - } - - err = store.CreateHaConfig() - if err != nil { - store.logger.Error(err, "Create Ha ConfigMap failed") - } - - err = store.CreateLease() - if err != nil { - store.logger.Error(err, "Create Leader ConfigMap failed") - } - return err -} - -func (store *KubernetesStore) GetClusterName() string { - return store.clusterName -} - -func (store *KubernetesStore) SetCompName(componentName string) { - store.componentName = componentName - store.clusterCompName = store.clusterName + "-" + componentName -} - -func (store *KubernetesStore) GetClusterFromCache() *Cluster { - if store.cluster != nil { - return store.cluster - } - cluster, _ := store.GetCluster() - return cluster -} - -func (store *KubernetesStore) GetCluster() (*Cluster, error) { - clusterResource := &appsv1alpha1.Cluster{} - err := store.client.Get(). - Namespace(store.namespace). - Resource("clusters"). - Name(store.clusterName). - VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec). - Do(store.ctx). - Into(clusterResource) - if err != nil { - store.logger.Error(err, "k8s get cluster error") - return nil, err - } - - var replicas int32 - for _, component := range clusterResource.Spec.ComponentSpecs { - if store.IsLeaderClusterWide { - replicas += component.Replicas - } else if component.Name == store.componentName { - replicas = component.Replicas - break - } - } - - var members []Member - if store.cluster != nil { - hasPodIP := true - for _, m := range store.cluster.Members { - if m.PodIP == "" { - hasPodIP = false - break - } - } - if hasPodIP && int(replicas) == len(store.cluster.Members) { - members = store.cluster.Members - } - } - if len(members) == 0 { - members, err = store.GetMembers() - if err != nil { - return nil, err - } - } - - leader, err := store.GetLeader() - if err != nil { - store.logger.Info("get leader failed", "error", err.Error()) - } - - switchover, err := store.GetSwitchover() - if err != nil { - store.logger.Info("get switchover failed", "error", err.Error()) - } - - haConfig, err := store.GetHaConfig() - if err != nil { - store.logger.Info("get HaConfig failed", "error", err.Error()) - } - - cluster := &Cluster{ - ClusterCompName: store.clusterCompName, - Namespace: store.namespace, - Replicas: replicas, - Members: members, - Leader: leader, - Switchover: switchover, - HaConfig: haConfig, - Resource: clusterResource, - } - - store.cluster = cluster - return cluster, nil -} - -func (store *KubernetesStore) GetMembers() ([]Member, error) { - labelsMap := map[string]string{ - constant.AppInstanceLabelKey: store.clusterName, - constant.AppManagedByLabelKey: "kubeblocks", - } - if !store.IsLeaderClusterWide { - labelsMap[constant.KBAppComponentLabelKey] = store.componentName - } - - selector := labels.SelectorFromSet(labelsMap) - store.logger.Info(fmt.Sprintf("pod selector: %s", selector.String())) - podList, err := store.clientset.CoreV1().Pods(store.namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) - if err != nil { - return nil, err - } - - store.logger.Info(fmt.Sprintf("podlist: %d", len(podList.Items))) - members := make([]Member, 0, len(podList.Items)) - for _, pod := range podList.Items { - componentName := pod.Labels[constant.KBAppComponentLabelKey] - if componentName == "" { - // it is not a member pod - continue - } - member := Member{} - member.Name = pod.Name - // member.Name = fmt.Sprintf("%s.%s-headless.%s.svc", pod.Name, store.clusterCompName, store.namespace) - member.Role = pod.Labels[constant.RoleLabelKey] - member.ComponentName = componentName - member.PodIP = pod.Status.PodIP - member.DBPort = getDBPort(&pod) - member.LorryPort = getLorryPort(&pod) - member.HAPort = getHAPort(&pod) - member.UID = string(pod.UID) - if pod.Spec.HostNetwork { - member.UseIP = true - } - member.resource = pod.DeepCopy() - members = append(members, member) - } - - return members, nil -} - -func (store *KubernetesStore) ResetCluster() {} -func (store *KubernetesStore) DeleteCluster() {} - -func (store *KubernetesStore) GetLeaderConfigMap() (*corev1.ConfigMap, error) { - leaderName := store.getLeaderName() - leaderConfigMap, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Get(store.ctx, leaderName, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - store.logger.Info("Leader configmap is not found", "configmap", leaderName) - return nil, nil - } - store.logger.Error(err, "Get Leader configmap failed") - } - return leaderConfigMap, err -} - -func (store *KubernetesStore) IsLeaseExist() (bool, error) { - leaderConfigMap, err := store.GetLeaderConfigMap() - appCluster, ok := store.cluster.Resource.(*appsv1alpha1.Cluster) - if leaderConfigMap != nil && ok && leaderConfigMap.CreationTimestamp.Before(&appCluster.CreationTimestamp) { - store.logger.Info("A previous leader configmap resource exists, delete it", "name", leaderConfigMap.Name) - _ = store.DeleteLeader() - return false, nil - } - return leaderConfigMap != nil, err -} - -func (store *KubernetesStore) CreateLease() error { - isExist, err := store.IsLeaseExist() - if isExist || err != nil { - return err - } - - leaderConfigMapName := store.getLeaderName() - leaderName := store.currentMemberName - now := time.Now().Unix() - nowStr := strconv.FormatInt(now, 10) - ttl := viper.GetString(constant.KBEnvTTL) - leaderConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: leaderConfigMapName, - Annotations: map[string]string{ - "leader": leaderName, - "acquire-time": nowStr, - "renew-time": nowStr, - "ttl": ttl, - "extra": "", - }, - }, - } - - store.logger.Info(fmt.Sprintf("K8S store initializing, create leader ConfigMap: %s", leaderConfigMapName)) - err = store.createConfigMap(leaderConfigMap) - if err != nil { - store.logger.Error(err, "Create Leader ConfigMap failed") - return err - } - return nil -} - -func (store *KubernetesStore) GetLeader() (*Leader, error) { - configmap, err := store.GetLeaderConfigMap() - if err != nil { - return nil, err - } - - if configmap == nil { - return nil, nil - } - - annotations := configmap.Annotations - acquireTime, err := strconv.ParseInt(annotations["acquire-time"], 10, 64) - if err != nil { - acquireTime = 0 - } - renewTime, err := strconv.ParseInt(annotations["renew-time"], 10, 64) - if err != nil { - renewTime = 0 - } - ttl, err := strconv.Atoi(annotations["ttl"]) - if err != nil { - ttl = viper.GetInt(constant.KBEnvTTL) - } - leader := annotations["leader"] - stateStr, ok := annotations["dbstate"] - var dbState *DBState - if ok { - dbState = new(DBState) - err = json.Unmarshal([]byte(stateStr), &dbState) - if err != nil { - store.logger.Info("get leader dbstate failed", "annotations", annotations, "error", err.Error()) - } - } - - if ttl > 0 && time.Now().Unix()-renewTime > int64(ttl) { - store.logger.Info(fmt.Sprintf("lock expired: %v, now: %d", annotations, time.Now().Unix())) - leader = "" - } - - return &Leader{ - Index: configmap.ResourceVersion, - Name: leader, - AcquireTime: acquireTime, - RenewTime: renewTime, - TTL: ttl, - Resource: configmap, - DBState: dbState, - }, nil -} - -func (store *KubernetesStore) DeleteLeader() error { - leaderName := store.getLeaderName() - err := store.clientset.CoreV1().ConfigMaps(store.namespace).Delete(store.ctx, leaderName, metav1.DeleteOptions{}) - if err != nil { - store.logger.Error(err, "Delete leader configmap failed") - } - return err -} - -func (store *KubernetesStore) AttemptAcquireLease() error { - timestamp := time.Now().Unix() - now := strconv.FormatInt(timestamp, 10) - ttl := store.cluster.HaConfig.ttl - leaderName := store.currentMemberName - annotation := map[string]string{ - "leader": leaderName, - "ttl": strconv.Itoa(ttl), - "renew-time": now, - "acquire-time": now, - } - - configMap := store.cluster.Leader.Resource.(*corev1.ConfigMap) - configMap.SetAnnotations(annotation) - if store.cluster.Leader.DBState != nil { - str, _ := json.Marshal(store.cluster.Leader.DBState) - configMap.Annotations["dbstate"] = string(str) - } - cm, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - if err != nil { - store.logger.Error(err, "Acquire lease failed") - return err - } - - store.cluster.Leader.Resource = cm - store.cluster.Leader.AcquireTime = timestamp - store.cluster.Leader.RenewTime = timestamp - return nil -} - -func (store *KubernetesStore) HasLease() bool { - return store.cluster != nil && store.cluster.Leader != nil && store.cluster.Leader.Name == store.currentMemberName -} - -func (store *KubernetesStore) UpdateLease() error { - configMap := store.cluster.Leader.Resource.(*corev1.ConfigMap) - - annotations := configMap.GetAnnotations() - if annotations["leader"] != store.currentMemberName { - return errors.Errorf("lost lease") - } - ttl := store.cluster.HaConfig.ttl - annotations["ttl"] = strconv.Itoa(ttl) - annotations["renew-time"] = strconv.FormatInt(time.Now().Unix(), 10) - - if store.cluster.Leader.DBState != nil { - str, _ := json.Marshal(store.cluster.Leader.DBState) - configMap.Annotations["dbstate"] = string(str) - } - configMap.SetAnnotations(annotations) - - _, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - return err -} - -func (store *KubernetesStore) ReleaseLease() error { - store.logger.Info("release lease") - configMap := store.cluster.Leader.Resource.(*corev1.ConfigMap) - configMap.Annotations["leader"] = "" - store.cluster.Leader.Name = "" - - if store.cluster.Leader.DBState != nil { - str, _ := json.Marshal(store.cluster.Leader.DBState) - configMap.Annotations["dbstate"] = string(str) - } - _, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - if err != nil { - store.logger.Error(err, "release lease failed") - } - // TODO: if response status code is 409, it means operation conflict. - return err -} - -func (store *KubernetesStore) CreateHaConfig() error { - haName := store.getHAConfigName() - haConfig, _ := store.GetHaConfig() - if haConfig.resource != nil { - return nil - } - - store.logger.Info(fmt.Sprintf("Create Ha ConfigMap: %s", haName)) - ttl := viper.GetString(constant.KBEnvTTL) - maxLag := viper.GetString(constant.KBEnvMaxLag) - enableHA := viper.GetString(constant.KBEnvEnableHA) - if enableHA == "" { - // enable HA by default - enableHA = "true" - } - haConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: haName, - Annotations: map[string]string{ - "ttl": ttl, - "enable": enableHA, - "MaxLagOnSwitchover": maxLag, - }, - }, - } - - err := store.createConfigMap(haConfigMap) - if err != nil { - store.logger.Error(err, "Create Ha ConfigMap failed") - } - return err -} - -func (store *KubernetesStore) GetHaConfig() (*HaConfig, error) { - configmapName := store.getHAConfigName() - deleteMembers := make(map[string]MemberToDelete) - configmap, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Get(context.TODO(), configmapName, metav1.GetOptions{}) - if err != nil { - if !apierrors.IsNotFound(err) { - store.logger.Error(err, fmt.Sprintf("Get ha configmap [%s] error", configmapName)) - } else { - err = nil - } - return &HaConfig{ - index: "", - ttl: viper.GetInt(constant.KBEnvTTL), - maxLagOnSwitchover: 1048576, - DeleteMembers: deleteMembers, - }, err - } - - annotations := configmap.Annotations - ttl, err := strconv.Atoi(annotations["ttl"]) - if err != nil { - ttl = viper.GetInt(constant.KBEnvTTL) - } - maxLagOnSwitchover, err := strconv.Atoi(annotations["MaxLagOnSwitchover"]) - if err != nil { - maxLagOnSwitchover = 1048576 - } - - enable := false - enableStr := annotations["enable"] - if enableStr != "" { - enable, err = strconv.ParseBool(enableStr) - } - - str := annotations["delete-members"] - if str != "" { - err := json.Unmarshal([]byte(str), &deleteMembers) - if err != nil { - store.logger.Error(err, fmt.Sprintf("Get delete members [%s] error", str)) - } - } - - return &HaConfig{ - index: configmap.ResourceVersion, - ttl: ttl, - enable: enable, - maxLagOnSwitchover: int64(maxLagOnSwitchover), - DeleteMembers: deleteMembers, - resource: configmap, - }, err -} - -func (store *KubernetesStore) UpdateHaConfig() error { - haConfig := store.cluster.HaConfig - if haConfig.resource == nil { - return errors.New("No HA configmap") - } - - configMap := haConfig.resource.(*corev1.ConfigMap) - annotations := configMap.Annotations - annotations["ttl"] = strconv.Itoa(haConfig.ttl) - deleteMembers, err := json.Marshal(haConfig.DeleteMembers) - if err != nil { - store.logger.Error(err, fmt.Sprintf("marsha delete members [%v]", haConfig)) - } - annotations["delete-members"] = string(deleteMembers) - annotations["MaxLagOnSwitchover"] = strconv.Itoa(int(haConfig.maxLagOnSwitchover)) - - _, err = store.clientset.CoreV1().ConfigMaps(store.namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) - return err -} - -func (store *KubernetesStore) GetSwitchOverConfigMap() (*corev1.ConfigMap, error) { - switchoverName := store.getSwitchoverName() - switchoverConfigMap, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Get(store.ctx, switchoverName, metav1.GetOptions{}) - if err != nil { - if apierrors.IsNotFound(err) { - return nil, nil - } - store.logger.Error(err, "Get switchover configmap failed") - } - store.logger.Info("Found switchover Setting", "configmap", switchoverConfigMap.Annotations) - return switchoverConfigMap, err -} - -func (store *KubernetesStore) GetSwitchover() (*Switchover, error) { - switchOverConfigMap, _ := store.GetSwitchOverConfigMap() - if switchOverConfigMap == nil { - return nil, nil - } - annotations := switchOverConfigMap.Annotations - scheduledAt, _ := strconv.Atoi(annotations["scheduled-at"]) - switchOver := newSwitchover(switchOverConfigMap.ResourceVersion, annotations["leader"], annotations["candidate"], int64(scheduledAt)) - return switchOver, nil -} - -func (store *KubernetesStore) CreateSwitchover(leader, candidate string) error { - switchoverName := store.getSwitchoverName() - switchover, _ := store.GetSwitchover() - if switchover != nil { - return fmt.Errorf("there is another switchover %s unfinished", switchoverName) - } - - store.logger.Info(fmt.Sprintf("Create switchover configmap %s", switchoverName)) - swConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: switchoverName, - Annotations: map[string]string{ - "leader": leader, - "candidate": candidate, - }, - }, - } - - err := store.createConfigMap(swConfigMap) - if err != nil { - store.logger.Error(err, "Create switchover configmap failed") - return err - } - return nil -} - -func (store *KubernetesStore) DeleteSwitchover() error { - switchoverName := store.getSwitchoverName() - err := store.clientset.CoreV1().ConfigMaps(store.namespace).Delete(store.ctx, switchoverName, metav1.DeleteOptions{}) - if err != nil { - store.logger.Error(err, "Delete switchOver configmap failed") - } - return err -} - -func (store *KubernetesStore) getLeaderName() string { - if store.IsLeaderClusterWide { - return store.clusterName + "-leader" - } - return store.clusterCompName + "-leader" -} - -func (store *KubernetesStore) getHAConfigName() string { - if store.IsLeaderClusterWide { - return store.clusterName + "-haconfig" - } - return store.clusterCompName + "-haconfig" -} - -func (store *KubernetesStore) getSwitchoverName() string { - if store.IsLeaderClusterWide { - return store.clusterName + "-switchover" - } - return store.clusterCompName + "-switchover" -} - -func (store *KubernetesStore) createConfigMap(configMap *corev1.ConfigMap) error { - labelsMap := map[string]string{ - constant.AppInstanceLabelKey: store.clusterName, - constant.AppManagedByLabelKey: "kubeblocks", - } - if !store.IsLeaderClusterWide { - labelsMap[constant.KBAppComponentLabelKey] = store.componentName - } - - configMap.Labels = labelsMap - configMap.Namespace = store.namespace - configMap.OwnerReferences = []metav1.OwnerReference{getOwnerRef(store.cluster)} - _, err := store.clientset.CoreV1().ConfigMaps(store.namespace).Create(store.ctx, configMap, metav1.CreateOptions{}) - if err != nil { - return err - } - return nil -} - -func (store *KubernetesStore) AddCurrentMember() error { - return nil -} - -// TODO: Use the database instance's character type to determine its port number more precisely -func getDBPort(pod *corev1.Pod) string { - mainContainer := pod.Spec.Containers[0] - port := mainContainer.Ports[0] - dbPort := port.ContainerPort - return strconv.Itoa(int(dbPort)) -} - -func getLorryPort(pod *corev1.Pod) string { - for _, container := range pod.Spec.Containers { - for _, port := range container.Ports { - if port.Name == constant.LorryHTTPPortName { - return strconv.Itoa(int(port.ContainerPort)) - } - } - } - return "" -} - -func getHAPort(pod *corev1.Pod) string { - for _, container := range pod.Spec.Containers { - for _, port := range container.Ports { - if port.Name == "ha" { - return strconv.Itoa(int(port.ContainerPort)) - } - } - } - return "" -} - -func getOwnerRef(cluster *Cluster) metav1.OwnerReference { - clusterObj := cluster.Resource.(*appsv1alpha1.Cluster) - gvk, _ := apiutil.GVKForObject(clusterObj, scheme.Scheme) - ownerRef := metav1.OwnerReference{ - APIVersion: gvk.GroupVersion().String(), - Kind: gvk.Kind, - UID: clusterObj.UID, - Name: clusterObj.Name, - } - return ownerRef -} diff --git a/pkg/lorry/dcs/types.go b/pkg/lorry/dcs/types.go deleted file mode 100644 index f2b03a7f963..00000000000 --- a/pkg/lorry/dcs/types.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package dcs - -import ( - "fmt" - "strings" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/util" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -type Cluster struct { - ClusterCompName string - Namespace string - Replicas int32 - HaConfig *HaConfig - Leader *Leader - Members []Member - Switchover *Switchover - Extra map[string]string - Resource any -} - -func (c *Cluster) HasMember(memberName string) bool { - for _, member := range c.Members { - if memberName == member.Name { - return true - } - } - return false -} - -func (c *Cluster) GetLeaderMember() *Member { - if c.Leader == nil || c.Leader.Name == "" { - return nil - } - - return c.GetMemberWithName(c.Leader.Name) -} - -func (c *Cluster) GetMemberWithName(name string) *Member { - for _, m := range c.Members { - if m.Name == name { - return &m - } - } - - return nil -} - -func (c *Cluster) GetMemberWithHost(host string) *Member { - for _, m := range c.Members { - if strings.HasPrefix(host, m.Name) || strings.HasPrefix(host, m.PodIP) { - return &m - } - } - - return nil -} - -func (c *Cluster) GetMemberName() []string { - var memberList []string - for _, member := range c.Members { - memberList = append(memberList, member.Name) - } - - return memberList -} - -func (c *Cluster) IsLocked() bool { - return c.Leader != nil && c.Leader.Name != "" -} - -func (c *Cluster) GetMemberAddrWithPort(member Member) string { - addr := c.GetMemberAddr(member) - return fmt.Sprintf("%s:%s", addr, member.DBPort) -} - -func (c *Cluster) GetMemberAddr(member Member) string { - if member.UseIP { - return member.PodIP - } - clusterDomain := viper.GetString(constant.KubernetesClusterDomainEnv) - clusterCompName := "" - index := strings.LastIndex(member.Name, "-") - if index > 0 { - clusterCompName = member.Name[:index] - } - return fmt.Sprintf("%s.%s-headless.%s.svc.%s", member.Name, clusterCompName, c.Namespace, clusterDomain) -} - -func (c *Cluster) GetMemberShortAddr(member Member) string { - clusterCompName := "" - index := strings.LastIndex(member.Name, "-") - if index > 0 { - clusterCompName = member.Name[:index] - } - return fmt.Sprintf("%s.%s-headless", member.Name, clusterCompName) -} - -func (c *Cluster) GetMemberAddrs() []string { - hosts := make([]string, len(c.Members)) - for i, member := range c.Members { - hosts[i] = c.GetMemberAddrWithPort(member) - } - return hosts -} - -type MemberToDelete struct { - UID string - IsFinished bool -} - -type HaConfig struct { - index string - ttl int - enable bool - maxLagOnSwitchover int64 - DeleteMembers map[string]MemberToDelete - resource any -} - -func (c *HaConfig) GetTTL() int { - return c.ttl -} - -func (c *HaConfig) IsEnable() bool { - return c.enable -} - -func (c *HaConfig) SetEnable(enable bool) { - c.enable = enable -} - -func (c *HaConfig) GetMaxLagOnSwitchover() int64 { - return c.maxLagOnSwitchover -} - -func (c *HaConfig) IsDeleting(member *Member) bool { - memberToDelete := c.GetMemberToDelete(member) - return memberToDelete != nil -} - -func (c *HaConfig) IsDeleted(member *Member) bool { - memberToDelete := c.GetMemberToDelete(member) - if memberToDelete == nil { - return false - } - return memberToDelete.IsFinished -} - -func (c *HaConfig) FinishDeleted(member *Member) { - memberToDelete := c.GetMemberToDelete(member) - memberToDelete.IsFinished = true - c.DeleteMembers[member.Name] = *memberToDelete -} - -func (c *HaConfig) TryToRemoveDeleteRecord(member *Member) bool { - memberToDelete := c.GetDuplicatedMemberToDelete(member) - if memberToDelete != nil { - delete(c.DeleteMembers, member.Name) - return true - } - - return false -} - -// GetDuplicatedMemberToDelete get previous duplicated delete record in ha configmap -func (c *HaConfig) GetDuplicatedMemberToDelete(member *Member) *MemberToDelete { - memberToDelete, ok := c.DeleteMembers[member.Name] - if !ok { - return nil - } - - if memberToDelete.UID == member.UID { - return nil - } - return &memberToDelete -} - -func (c *HaConfig) GetMemberToDelete(member *Member) *MemberToDelete { - memberToDelete, ok := c.DeleteMembers[member.Name] - if !ok { - return nil - } - - if memberToDelete.UID != member.UID { - return nil - } - return &memberToDelete -} - -func (c *HaConfig) AddMemberToDelete(member *Member) { - memberToDelete := MemberToDelete{ - UID: member.UID, - IsFinished: false, - } - c.DeleteMembers[member.Name] = memberToDelete -} - -type Leader struct { - DBState *DBState - Index string - Name string - AcquireTime int64 - RenewTime int64 - TTL int - Resource any -} - -type DBState struct { - OpTimestamp int64 - Extra map[string]string -} -type Member struct { - Index string - Name string - Role string - PodIP string - DBPort string - LorryPort string - HAPort string - UID string - UseIP bool - resource any - ComponentName string -} - -func (m *Member) GetName() string { - return m.Name -} - -func (m *Member) IsLorryReady() bool { - if m.PodIP == "" { - return false - } - ready, err := util.IsTCPReady(m.PodIP, m.LorryPort) - if err != nil { - return false - } - return ready -} - -// func newMember(index string, name string, role string, url string) *Member { -// return &Member{ -// Index: index, -// Name: name, -// Role: role, -// } -// } - -type Switchover struct { - Index string - Leader string - Candidate string - ScheduledAt int64 -} - -func newSwitchover(index string, leader string, candidate string, scheduledAt int64) *Switchover { - return &Switchover{ - Index: index, - Leader: leader, - Candidate: candidate, - ScheduledAt: scheduledAt, - } -} - -func (s *Switchover) GetLeader() string { - return s.Leader -} - -func (s *Switchover) GetCandidate() string { - return s.Candidate -} diff --git a/pkg/lorry/engines/base.go b/pkg/lorry/engines/base.go deleted file mode 100644 index a7571f8a314..00000000000 --- a/pkg/lorry/engines/base.go +++ /dev/null @@ -1,259 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "context" - "fmt" - "strings" - - "github.com/go-logr/logr" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -type DBManagerBase struct { - CurrentMemberName string - CurrentMemberIP string - ClusterCompName string - Namespace string - DataDir string - Logger logr.Logger - DBStartupReady bool - IsLocked bool - DBState *dcs.DBState -} - -func NewDBManagerBase(logger logr.Logger) (*DBManagerBase, error) { - currentMemberName := viper.GetString(constant.KBEnvPodName) - if currentMemberName == "" { - return nil, fmt.Errorf("%s is not set", constant.KBEnvPodName) - } - - mgr := DBManagerBase{ - CurrentMemberName: currentMemberName, - CurrentMemberIP: viper.GetString(constant.KBEnvPodIP), - ClusterCompName: viper.GetString(constant.KBEnvClusterCompName), - Namespace: viper.GetString(constant.KBEnvNamespace), - Logger: logger, - } - return &mgr, nil -} - -func (mgr *DBManagerBase) IsDBStartupReady() bool { - return mgr.DBStartupReady -} - -func (mgr *DBManagerBase) GetLogger() logr.Logger { - return mgr.Logger -} - -func (mgr *DBManagerBase) SetLogger(logger logr.Logger) { - mgr.Logger = logger -} - -func (mgr *DBManagerBase) GetCurrentMemberName() string { - return mgr.CurrentMemberName -} - -func (mgr *DBManagerBase) IsFirstMember() bool { - return strings.HasSuffix(mgr.CurrentMemberName, "-0") -} - -func (mgr *DBManagerBase) IsPromoted(context.Context) bool { - return true -} - -func (mgr *DBManagerBase) Promote(context.Context, *dcs.Cluster) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Demote(context.Context) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Follow(context.Context, *dcs.Cluster) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Recover(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) IsLeader(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - return false, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) IsLeaderMember(context.Context, *dcs.Cluster, *dcs.Member) (bool, error) { - return false, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) GetMemberAddrs(context.Context, *dcs.Cluster) []string { - return nil -} - -func (mgr *DBManagerBase) InitializeCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) IsClusterInitialized(context.Context, *dcs.Cluster) (bool, error) { - return true, nil -} - -func (mgr *DBManagerBase) IsClusterHealthy(context.Context, *dcs.Cluster) bool { - return true -} - -func (mgr *DBManagerBase) MemberHealthyCheck(context.Context, *dcs.Cluster, *dcs.Member) error { - return nil -} - -func (mgr *DBManagerBase) LeaderHealthyCheck(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) CurrentMemberHealthyCheck(ctx context.Context, cluster *dcs.Cluster) error { - member := cluster.GetMemberWithName(mgr.CurrentMemberName) - return mgr.MemberHealthyCheck(ctx, cluster, member) -} - -func (mgr *DBManagerBase) HasOtherHealthyLeader(context.Context, *dcs.Cluster) *dcs.Member { - return nil -} - -func (mgr *DBManagerBase) HasOtherHealthyMembers(context.Context, *dcs.Cluster, string) []*dcs.Member { - return nil -} - -func (mgr *DBManagerBase) IsMemberHealthy(context.Context, *dcs.Cluster, *dcs.Member) bool { - return false -} - -func (mgr *DBManagerBase) IsCurrentMemberHealthy(context.Context, *dcs.Cluster) bool { - return true -} - -func (mgr *DBManagerBase) IsCurrentMemberInCluster(context.Context, *dcs.Cluster) bool { - return true -} - -func (mgr *DBManagerBase) JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) LeaveMemberFromCluster(context.Context, *dcs.Cluster, string) error { - return nil -} - -func (mgr *DBManagerBase) IsMemberLagging(context.Context, *dcs.Cluster, *dcs.Member) (bool, int64) { - return false, 0 -} - -func (mgr *DBManagerBase) GetLag(context.Context, *dcs.Cluster) (int64, error) { - return 0, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) GetDBState(context.Context, *dcs.Cluster) *dcs.DBState { - // mgr.DBState = DBState - return nil -} - -func (mgr *DBManagerBase) MoveData(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) GetReplicaRole(context.Context, *dcs.Cluster) (string, error) { - return "", models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Exec(context.Context, string) (int64, error) { - return 0, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Query(context.Context, string) ([]byte, error) { - return []byte{}, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) GetPort() (int, error) { - return 0, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) IsRootCreated(context.Context) (bool, error) { - return true, nil -} - -func (mgr *DBManagerBase) ListUsers(context.Context) ([]models.UserInfo, error) { - return nil, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) ListSystemAccounts(context.Context) ([]models.UserInfo, error) { - return nil, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) CreateUser(context.Context, string, string, string) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) DeleteUser(context.Context, string) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) DescribeUser(context.Context, string) (*models.UserInfo, error) { - return nil, models.ErrNotImplemented -} - -func (mgr *DBManagerBase) GrantUserRole(context.Context, string, string) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) RevokeUserRole(context.Context, string, string) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) IsRunning() bool { - return false -} - -func (mgr *DBManagerBase) Lock(context.Context, string) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Unlock(context.Context) error { - return models.ErrNotImplemented -} - -func (mgr *DBManagerBase) Start(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *DBManagerBase) Stop() error { - return nil -} - -func (mgr *DBManagerBase) CreateRoot(context.Context) error { - return nil -} - -func (mgr *DBManagerBase) ShutDownWithWait() { - mgr.Logger.Info("Override me if need") -} diff --git a/pkg/lorry/engines/cluster_commands.go b/pkg/lorry/engines/cluster_commands.go deleted file mode 100644 index 49a45e1e5a3..00000000000 --- a/pkg/lorry/engines/cluster_commands.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "fmt" - "sort" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - HOST = "host" - PORT = "port" - USER = "user" - PASSWORD = "password" - COMMAND = "command" -) - -type NewCommandFunc func() ClusterCommands - -var ( - EnvVarMap = map[string]string{ - HOST: "KB_HOST", - PORT: "KB_PORT", - USER: "KB_USER", - PASSWORD: "KB_PASSWD", - } - - NewCommandFuncs = map[string]NewCommandFunc{} -) - -// AuthInfo is the authentication information for the database -type AuthInfo struct { - UserName string - UserPasswd string -} - -type ClusterCommands interface { - ConnectCommand(info *AuthInfo) []string - Container() string - ConnectExample(info *ConnectionInfo, client string) string - ExecuteCommand([]string) ([]string, []corev1.EnvVar, error) -} - -type EngineInfo struct { - Client string - Container string - PasswordEnv string - UserEnv string - Database string -} - -func newClusterCommands(typeName string) (ClusterCommands, error) { - newFunc, ok := NewCommandFuncs[typeName] - if !ok { - return nil, fmt.Errorf("unsupported engine type: %s", typeName) - } - - return newFunc(), nil -} - -type ConnectionInfo struct { - Host string - User string - Password string - Database string - Port string - ClusterName string - ComponentName string - HeadlessEndpoint string -} - -type BuildConnectExample func(info *ConnectionInfo) string - -func BuildExample(info *ConnectionInfo, client string, examples map[models.ClientType]BuildConnectExample) string { - // if client is not specified, output all examples - if len(client) == 0 { - var keys = make([]string, len(examples)) - var i = 0 - for k := range examples { - keys[i] = k.String() - i++ - } - sort.Strings(keys) - - var b strings.Builder - for _, k := range keys { - buildFn := examples[models.ClientType(k)] - b.WriteString(fmt.Sprintf("========= %s connection example =========\n", k)) - b.WriteString(buildFn(info)) - b.WriteString("\n") - } - return b.String() - } - - // return specified example - if buildFn, ok := examples[models.ClientType(client)]; ok { - return buildFn(info) - } - - return "" -} diff --git a/pkg/lorry/engines/cluster_commands_test.go b/pkg/lorry/engines/cluster_commands_test.go deleted file mode 100644 index 68ec91344f6..00000000000 --- a/pkg/lorry/engines/cluster_commands_test.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ = Describe("Cluster commands", func() { - It("new commands", func() { - for _, engineType := range []models.EngineType{models.MySQL, models.PostgreSQL, models.Redis, models.PostgreSQL, models.Nebula, models.FoxLake} { - typeName := string(engineType) - engine, _ := newClusterCommands(typeName) - Expect(engine).Should(BeNil()) - } - }) - - It("new unknown engine", func() { - typeName := "unknown-type" - engine, err := newClusterCommands(typeName) - Expect(engine).Should(BeNil()) - Expect(err).Should(HaveOccurred()) - }) -}) diff --git a/pkg/lorry/engines/custom/get_replica_role.go b/pkg/lorry/engines/custom/get_replica_role.go deleted file mode 100644 index 9298f09e62e..00000000000 --- a/pkg/lorry/engines/custom/get_replica_role.go +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package custom - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "regexp" - "strings" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/common" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -var perNodeRegx = regexp.MustCompile("^[^,]*$") - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - if mgr.actionSvcPorts != nil && len(*mgr.actionSvcPorts) > 0 { - return mgr.GetReplicaRoleThroughASMAction(ctx, cluster) - } - return mgr.GetReplicaRoleThroughCommands(ctx, cluster) -} - -// GetReplicaRoleThroughCommands provides the following dedicated environment variables for the action: -// -// - KB_POD_FQDN: The pod FQDN of the replica to check the role. -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service and retrieve the role information with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service and retrieve the role information. -func (mgr *Manager) GetReplicaRoleThroughCommands(ctx context.Context, cluster *dcs.Cluster) (string, error) { - roleProbeCmd, ok := mgr.actionCommands[constant.RoleProbeAction] - if !ok || len(roleProbeCmd) == 0 { - return "", errors.New("role probe commands is empty!") - } - - supportedShells := []string{"sh", "bash", "zsh", "csh", "ksh", "tcsh", "fish"} - // Check if the cmd is one kind of "sh" - if !contains(supportedShells, roleProbeCmd[0]) { - roleProbeCmd = append([]string{"sh", "-c"}, strings.Join(roleProbeCmd, " ")) - } - - // envs, err := util.GetGlobalSharedEnvs() - // if err != nil { - // return "", err - // } - return util.ExecCommand(ctx, roleProbeCmd, os.Environ()) -} - -func (mgr *Manager) GetReplicaRoleThroughASMAction(ctx context.Context, cluster *dcs.Cluster) (string, error) { - var ( - lastOutput []byte - err error - ) - - for _, port := range *mgr.actionSvcPorts { - u := fmt.Sprintf("http://127.0.0.1:%d/role?KB_RSM_LAST_STDOUT=%s", port, url.QueryEscape(string(lastOutput))) - lastOutput, err = mgr.callAction(ctx, u) - if err != nil { - return "", err - } - mgr.Logger.Info("action succeed", "url", u, "output", string(lastOutput)) - } - finalOutput := strings.TrimSpace(string(lastOutput)) - - if perNodeRegx.MatchString(finalOutput) { - return finalOutput, nil - } - - // csv format: term,podName,role - parseCSV := func(input string) (string, error) { - res := common.GlobalRoleSnapshot{} - lines := strings.Split(input, "\n") - for _, line := range lines { - fields := strings.Split(strings.TrimSpace(line), ",") - if len(fields) != 3 { - return "", err - } - res.Version = strings.TrimSpace(fields[0]) - pair := common.PodRoleNamePair{ - PodName: strings.TrimSpace(fields[1]), - RoleName: strings.ToLower(strings.TrimSpace(fields[2])), - } - res.PodRoleNamePairs = append(res.PodRoleNamePairs, pair) - } - resByte, err := json.Marshal(res) - return string(resByte), err - } - return parseCSV(finalOutput) -} - -// callAction sends an HTTP POST request to the specified URL and returns the response body. -// It takes a context.Context and the URL as input parameters. -// The function returns the response body as a byte slice and an error if any. -func (mgr *Manager) callAction(ctx context.Context, url string) ([]byte, error) { - // construct http request - request, err := http.NewRequestWithContext(ctx, "POST", url, nil) - if err != nil { - return nil, err - } - - // send http request - resp, err := mgr.client.Do(request) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // parse http response - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("received status code %d", resp.StatusCode) - } - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return b, err -} - -func contains(supportedShells []string, shell string) bool { - cmds := strings.Split(shell, "/") - shell = cmds[len(cmds)-1] - for _, s := range supportedShells { - if s == shell { - return true - } - } - return false -} diff --git a/pkg/lorry/engines/custom/manager.go b/pkg/lorry/engines/custom/manager.go deleted file mode 100644 index 70f2b191182..00000000000 --- a/pkg/lorry/engines/custom/manager.go +++ /dev/null @@ -1,344 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package custom - -import ( - "context" - "encoding/json" - "net" - "net/http" - "os" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Manager struct { - engines.DBManagerBase - - // For ASM Actions - actionSvcPorts *[]int - client *http.Client - - // For ComponentDefinition Actions - actionCommands map[string][]string -} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("custom") - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - managerBase.DBStartupReady = true - mgr := &Manager{ - actionSvcPorts: &[]int{}, - DBManagerBase: *managerBase, - } - - err = mgr.InitInstanceSetActions() - if err != nil { - mgr.Logger.Info("init InstanceSet commands failed", "error", err.Error()) - return nil, err - } - err = mgr.InitComponentDefinitionActions() - if err != nil { - mgr.Logger.Info("init component definition commands failed", "error", err.Error()) - return nil, err - } - return mgr, nil -} - -func (mgr *Manager) InitInstanceSetActions() error { - actionSvcList := viper.GetString("KB_RSM_ACTION_SVC_LIST") - if actionSvcList == "" || actionSvcList == "null" { - return nil - } - err := json.Unmarshal([]byte(actionSvcList), mgr.actionSvcPorts) - if err != nil { - return err - } - - // See guidance on proper HTTP client settings here: - // https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779 - dialer := &net.Dialer{ - Timeout: 5 * time.Second, - } - netTransport := &http.Transport{ - Dial: dialer.Dial, - TLSHandshakeTimeout: 5 * time.Second, - } - mgr.client = &http.Client{ - Timeout: time.Second * 30, - Transport: netTransport, - } - - return nil -} - -func (mgr *Manager) InitComponentDefinitionActions() error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - err := json.Unmarshal([]byte(actionJSON), &mgr.actionCommands) - if err != nil { - return err - } - } - return nil -} - -// JoinCurrentMemberToCluster provides the following dedicated environment variables for the action: -// -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -// - KB_PRIMARY_POD_FQDN: The FQDN of the original primary Pod before switchover. -// - KB_MEMBER_ADDRESSES: The addresses of all members. -// - KB_NEW_MEMBER_POD_NAME: The name of the new member's Pod. -// - KB_NEW_MEMBER_POD_IP: The name of the new member's Pod. -func (mgr *Manager) JoinCurrentMemberToCluster(ctx context.Context, cluster *dcs.Cluster) error { - memberJoinCmd, ok := mgr.actionCommands[constant.MemberJoinAction] - if !ok || len(memberJoinCmd) == 0 { - // return errors.New("member join command is empty!") - return nil - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - - if cluster.Leader != nil && cluster.Leader.Name != "" { - leaderMember := cluster.GetMemberWithName(cluster.Leader.Name) - fqdn := cluster.GetMemberAddr(*leaderMember) - envs = append(envs, "KB_PRIMARY_POD_FQDN"+"="+fqdn) - } - - addrs := cluster.GetMemberAddrs() - envs = append(envs, "KB_MEMBER_ADDRESSES"+"="+strings.Join(addrs, ",")) - envs = append(envs, "KB_NEW_MEMBER_POD_NAME"+"="+mgr.CurrentMemberName) - member := cluster.GetMemberWithName(mgr.CurrentMemberName) - if member != nil { - envs = append(envs, "KB_NEW_MEMBER_POD_IP"+"="+member.PodIP) - } - output, err := util.ExecCommand(ctx, memberJoinCmd, envs) - - if output != "" { - mgr.Logger.Info("member join", "output", output) - } - return err -} - -// LeaveMemberFromCluster provides the following dedicated environment variables for the action: -// -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -// - KB_PRIMARY_POD_FQDN: The FQDN of the original primary Pod before switchover. -// - KB_MEMBER_ADDRESSES: The addresses of all members. -// - KB_LEAVE_MEMBER_POD_NAME: The name of the leave member's Pod. -// - KB_LEAVE_MEMBER_POD_IP: The IP of the leave member's Pod. -func (mgr *Manager) LeaveMemberFromCluster(ctx context.Context, cluster *dcs.Cluster, memberName string) error { - memberLeaveCmd, ok := mgr.actionCommands[constant.MemberLeaveAction] - if !ok || len(memberLeaveCmd) == 0 { - // return errors.New("member leave command is empty!") - return nil - } - envs := os.Environ() - if cluster.Leader != nil && cluster.Leader.Name != "" { - leaderMember := cluster.GetMemberWithName(cluster.Leader.Name) - fqdn := cluster.GetMemberAddr(*leaderMember) - envs = append(envs, "KB_PRIMARY_POD_FQDN"+"="+fqdn) - } - - addrs := cluster.GetMemberAddrs() - envs = append(envs, "KB_MEMBER_ADDRESSES"+"="+strings.Join(addrs, ",")) - envs = append(envs, "KB_LEAVE_MEMBER_POD_NAME"+"="+memberName) - member := cluster.GetMemberWithName(memberName) - if member != nil { - envs = append(envs, "KB_LEAVE_MEMBER_POD_IP"+"="+member.PodIP) - } - output, err := util.ExecCommand(ctx, memberLeaveCmd, envs) - - if output != "" { - mgr.Logger.Info("member leave", "output", output) - } - return err -} - -// CurrentMemberHealthCheck provides the following dedicated environment variables for the action: -// -// - KB_POD_FQDN: The FQDN of the replica pod to check the role. -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -func (mgr *Manager) CurrentMemberHealthCheck(ctx context.Context, cluster *dcs.Cluster) error { - healthyCheckCmd, ok := mgr.actionCommands[constant.HealthyCheckAction] - if !ok || len(healthyCheckCmd) == 0 { - return errors.New("member healthyCheck command is empty!") - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - output, err := util.ExecCommand(ctx, healthyCheckCmd, envs) - - if output != "" { - mgr.Logger.Info("member healthy check", "output", output) - } - return err -} - -// Lock provides the following dedicated environment variables for the action: -// -// - KB_POD_FQDN: The FQDN of the replica pod to check the role. -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -func (mgr *Manager) Lock(ctx context.Context, reason string) error { - readonlyCmd, ok := mgr.actionCommands[constant.ReadonlyAction] - if !ok || len(readonlyCmd) == 0 { - // return errors.New("member lock command is empty!") - return nil - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - output, err := util.ExecCommand(ctx, readonlyCmd, envs) - - if output != "" { - mgr.Logger.Info("member lock", "output", output) - } - return err -} - -// Unlock provides the following dedicated environment variables for the action: -// -// - KB_POD_FQDN: The FQDN of the replica pod to check the role. -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -func (mgr *Manager) Unlock(ctx context.Context) error { - readWriteCmd, ok := mgr.actionCommands[constant.ReadWriteAction] - if !ok || len(readWriteCmd) == 0 { - // return errors.New("member unlock command is empty!") - return nil - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - output, err := util.ExecCommand(ctx, readWriteCmd, envs) - - if output != "" { - mgr.Logger.Info("member unlock", "output", output) - } - return err -} - -// CreateUser provides the following dedicated environment variables for the action: -// -// - KB_ACCOUNT_NAME: The name of the account to create. -// - KB_ACCOUNT_PASSWORD: The password of the account to create. -// - KB_ACCOUNT_STATEMENT: The statement used to create the account. -func (mgr *Manager) CreateUser(ctx context.Context, userName, password, statement string) error { - accountProvisionCmd, ok := mgr.actionCommands[constant.AccountProvisionAction] - if !ok || len(accountProvisionCmd) == 0 { - return nil - } - envs := os.Environ() - envs = append(envs, "KB_ACCOUNT_NAME"+"="+userName) - envs = append(envs, "KB_ACCOUNT_PASSWORD"+"="+password) - envs = append(envs, "KB_ACCOUNT_STATEMENT"+"="+statement) - output, err := util.ExecCommand(ctx, accountProvisionCmd, envs) - - if output != "" { - mgr.Logger.Info("account provision", "output", output) - } - return err -} - -// PostProvision provides the following dedicated environment variables for the action: -// -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -// - KB_CLUSTER_COMPONENT_LIST: Lists all components in the cluster, joined by ',' (e.g., "comp1,comp2"). -// - KB_CLUSTER_COMPONENT_POD_NAME_LIST: Lists all pod names in this component, joined by ',' (e.g., "pod1,pod2"). -// - KB_CLUSTER_COMPONENT_POD_IP_LIST: Lists the IP addresses of each pod in this component, corresponding one-to-one with each pod in the KB_CLUSTER_COMPONENT_POD_NAME_LIST. Joined by ',' (e.g., "podIp1,podIp2"). -// - KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST: Lists the host names where each pod resides in this component, corresponding one-to-one with each pod in the KB_CLUSTER_COMPONENT_POD_NAME_LIST. Joined by ',' (e.g., "hostName1,hostName2"). -// - KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST: Lists the host IP addresses where each pod resides in this component, corresponding one-to-one with each pod in the KB_CLUSTER_COMPONENT_POD_NAME_LIST. Joined by ',' (e.g., "hostIp1,hostIp2"). -func (mgr *Manager) PostProvision(ctx context.Context, componentNames, podNames, podIPs, podHostNames, podHostIPs string) error { - postProvisionCmd, ok := mgr.actionCommands[constant.PostProvisionAction] - if !ok || len(postProvisionCmd) == 0 { - // return errors.New("component postprovision command is empty!") - return nil - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - - envs = append(envs, "KB_CLUSTER_COMPONENT_LIST"+"="+componentNames) - envs = append(envs, "KB_CLUSTER_COMPONENT_POD_NAME_LIST"+"="+podNames) - envs = append(envs, "KB_CLUSTER_COMPONENT_POD_IP_LIST"+"="+podIPs) - envs = append(envs, "KB_CLUSTER_COMPONENT_POD_HOST_NAME_LIST"+"="+podHostNames) - envs = append(envs, "KB_CLUSTER_COMPONENT_POD_HOST_IP_LIST"+"="+podHostIPs) - output, err := util.ExecCommand(ctx, postProvisionCmd, envs) - - if output != "" { - mgr.Logger.Info("component postprovision", "output", output) - } - return err -} - -// PreTerminate provides the following dedicated environment variables for the action: -// -// - KB_POD_FQDN: The FQDN of the replica pod to check the role. -// - KB_SERVICE_PORT: The port on which the DB service listens. -// - KB_SERVICE_USER: The username used to access the DB service with sufficient privileges. -// - KB_SERVICE_PASSWORD: The password of the user used to access the DB service . -func (mgr *Manager) PreTerminate(ctx context.Context) error { - preTerminateCmd, ok := mgr.actionCommands[constant.PreTerminateAction] - if !ok || len(preTerminateCmd) == 0 { - // return errors.New("component preterminate command is empty!") - return nil - } - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - output, err := util.ExecCommand(ctx, preTerminateCmd, envs) - - if output != "" { - mgr.Logger.Info("component preterminate", "output", output) - } - return err -} diff --git a/pkg/lorry/engines/custom/manager_test.go b/pkg/lorry/engines/custom/manager_test.go deleted file mode 100644 index 5c11dbc1f81..00000000000 --- a/pkg/lorry/engines/custom/manager_test.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package custom - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strconv" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/common" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var _ = Describe("ETCD DBManager", func() { - // Set up relevant viper config variables - Context("new db manager", func() { - It("with right configurations", func() { - viper.Set("KB_RSM_ACTION_SVC_LIST", "[3502]") - properties := engines.Properties{} - dbManger, err := NewManager(properties) - Expect(err).Should(Succeed()) - Expect(dbManger).ShouldNot(BeNil()) - }) - - It("with wrong configurations", func() { - viper.Set("KB_RSM_ACTION_SVC_LIST", "wrong-setting") - properties := engines.Properties{} - dbManger, err := NewManager(properties) - Expect(err).Should(HaveOccurred()) - Expect(dbManger).Should(BeNil()) - }) - }) - - Context("global role snapshot", func() { - It("success", func() { - _ = setUpHost() - manager, err := NewManager(nil) - Expect(err).Should(BeNil()) - globalRole, err := manager.GetReplicaRole(context.TODO(), nil) - Expect(err).Should(BeNil()) - snapshot := &common.GlobalRoleSnapshot{} - Expect(json.Unmarshal([]byte(globalRole), snapshot)).Should(Succeed()) - Expect(snapshot.PodRoleNamePairs).Should(HaveLen(3)) - Expect(snapshot.Version).Should(Equal("1")) - }) - }) -}) - -func setUpHost() *httptest.Server { - var lines []string - for i := 0; i < 3; i++ { - podName := "pod-" + strconv.Itoa(i) - var role string - if i == 0 { - role = "leader" - } else { - role = "follower" - } - lines = append(lines, fmt.Sprintf("%d,%s,%s", 1, podName, role)) - } - respContent := strings.Join(lines, "\n") - - s := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - _, _ = w.Write([]byte(respContent)) - }), - ) - addr := s.Listener.Addr().String() - index := strings.LastIndex(addr, ":") - portStr := addr[index+1:] - viper.Set("KB_RSM_ACTION_SVC_LIST", "["+portStr+"]") - return s -} diff --git a/pkg/lorry/engines/custom/suite_test.go b/pkg/lorry/engines/custom/suite_test.go deleted file mode 100644 index e96931a532f..00000000000 --- a/pkg/lorry/engines/custom/suite_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package custom - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -var ( - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test-0") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestCustomDBManager(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Custom DBManager. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock dcs store - InitMockDCSStore() - -}) - -var _ = AfterSuite(func() { -}) - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} diff --git a/pkg/lorry/engines/dbmanager_mock.go b/pkg/lorry/engines/dbmanager_mock.go deleted file mode 100644 index 15bd8bd8473..00000000000 --- a/pkg/lorry/engines/dbmanager_mock.go +++ /dev/null @@ -1,743 +0,0 @@ -// /* -// Copyright (C) 2022-2024 ApeCloud Co., Ltd -// -// This file is part of KubeBlocks project -// -// 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 . -// */ -// -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/apecloud/kubeblocks/pkg/lorry/engines (interfaces: DBManager) - -// Package engines is a generated GoMock package. -package engines - -import ( - context "context" - reflect "reflect" - - logr "github.com/go-logr/logr" - gomock "github.com/golang/mock/gomock" - - dcs "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - models "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -// MockDBManager is a mock of DBManager interface. -type MockDBManager struct { - ctrl *gomock.Controller - recorder *MockDBManagerMockRecorder -} - -// MockDBManagerMockRecorder is the mock recorder for MockDBManager. -type MockDBManagerMockRecorder struct { - mock *MockDBManager -} - -// NewMockDBManager creates a new mock instance. -func NewMockDBManager(ctrl *gomock.Controller) *MockDBManager { - mock := &MockDBManager{ctrl: ctrl} - mock.recorder = &MockDBManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockDBManager) EXPECT() *MockDBManagerMockRecorder { - return m.recorder -} - -// CreateRoot mocks base method. -func (m *MockDBManager) CreateRoot(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateRoot", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateRoot indicates an expected call of CreateRoot. -func (mr *MockDBManagerMockRecorder) CreateRoot(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRoot", reflect.TypeOf((*MockDBManager)(nil).CreateRoot), arg0) -} - -// CreateUser mocks base method. -func (m *MockDBManager) CreateUser(arg0 context.Context, arg1, arg2, arg3 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUser", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateUser indicates an expected call of CreateUser. -func (mr *MockDBManagerMockRecorder) CreateUser(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockDBManager)(nil).CreateUser), arg0, arg1, arg2, arg3) -} - -// CurrentMemberHealthyCheck mocks base method. -func (m *MockDBManager) CurrentMemberHealthyCheck(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CurrentMemberHealthyCheck", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// CurrentMemberHealthyCheck indicates an expected call of CurrentMemberHealthyCheck. -func (mr *MockDBManagerMockRecorder) CurrentMemberHealthyCheck(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentMemberHealthyCheck", reflect.TypeOf((*MockDBManager)(nil).CurrentMemberHealthyCheck), arg0, arg1) -} - -// DeleteUser mocks base method. -func (m *MockDBManager) DeleteUser(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteUser", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteUser indicates an expected call of DeleteUser. -func (mr *MockDBManagerMockRecorder) DeleteUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockDBManager)(nil).DeleteUser), arg0, arg1) -} - -// Demote mocks base method. -func (m *MockDBManager) Demote(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Demote", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Demote indicates an expected call of Demote. -func (mr *MockDBManagerMockRecorder) Demote(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Demote", reflect.TypeOf((*MockDBManager)(nil).Demote), arg0) -} - -// DescribeUser mocks base method. -func (m *MockDBManager) DescribeUser(arg0 context.Context, arg1 string) (*models.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeUser", arg0, arg1) - ret0, _ := ret[0].(*models.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DescribeUser indicates an expected call of DescribeUser. -func (mr *MockDBManagerMockRecorder) DescribeUser(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeUser", reflect.TypeOf((*MockDBManager)(nil).DescribeUser), arg0, arg1) -} - -// Exec mocks base method. -func (m *MockDBManager) Exec(arg0 context.Context, arg1 string) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Exec", arg0, arg1) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Exec indicates an expected call of Exec. -func (mr *MockDBManagerMockRecorder) Exec(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDBManager)(nil).Exec), arg0, arg1) -} - -// Follow mocks base method. -func (m *MockDBManager) Follow(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Follow", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Follow indicates an expected call of Follow. -func (mr *MockDBManagerMockRecorder) Follow(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Follow", reflect.TypeOf((*MockDBManager)(nil).Follow), arg0, arg1) -} - -// GetCurrentMemberName mocks base method. -func (m *MockDBManager) GetCurrentMemberName() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentMemberName") - ret0, _ := ret[0].(string) - return ret0 -} - -// GetCurrentMemberName indicates an expected call of GetCurrentMemberName. -func (mr *MockDBManagerMockRecorder) GetCurrentMemberName() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentMemberName", reflect.TypeOf((*MockDBManager)(nil).GetCurrentMemberName)) -} - -// GetDBState mocks base method. -func (m *MockDBManager) GetDBState(arg0 context.Context, arg1 *dcs.Cluster) *dcs.DBState { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDBState", arg0, arg1) - ret0, _ := ret[0].(*dcs.DBState) - return ret0 -} - -// GetDBState indicates an expected call of GetDBState. -func (mr *MockDBManagerMockRecorder) GetDBState(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBState", reflect.TypeOf((*MockDBManager)(nil).GetDBState), arg0, arg1) -} - -// GetLag mocks base method. -func (m *MockDBManager) GetLag(arg0 context.Context, arg1 *dcs.Cluster) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLag", arg0, arg1) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLag indicates an expected call of GetLag. -func (mr *MockDBManagerMockRecorder) GetLag(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLag", reflect.TypeOf((*MockDBManager)(nil).GetLag), arg0, arg1) -} - -// GetLogger mocks base method. -func (m *MockDBManager) GetLogger() logr.Logger { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogger") - ret0, _ := ret[0].(logr.Logger) - return ret0 -} - -// GetLogger indicates an expected call of GetLogger. -func (mr *MockDBManagerMockRecorder) GetLogger() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockDBManager)(nil).GetLogger)) -} - -// GetMemberAddrs mocks base method. -func (m *MockDBManager) GetMemberAddrs(arg0 context.Context, arg1 *dcs.Cluster) []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMemberAddrs", arg0, arg1) - ret0, _ := ret[0].([]string) - return ret0 -} - -// GetMemberAddrs indicates an expected call of GetMemberAddrs. -func (mr *MockDBManagerMockRecorder) GetMemberAddrs(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberAddrs", reflect.TypeOf((*MockDBManager)(nil).GetMemberAddrs), arg0, arg1) -} - -// GetPort mocks base method. -func (m *MockDBManager) GetPort() (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPort") - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPort indicates an expected call of GetPort. -func (mr *MockDBManagerMockRecorder) GetPort() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPort", reflect.TypeOf((*MockDBManager)(nil).GetPort)) -} - -// GetReplicaRole mocks base method. -func (m *MockDBManager) GetReplicaRole(arg0 context.Context, arg1 *dcs.Cluster) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetReplicaRole", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetReplicaRole indicates an expected call of GetReplicaRole. -func (mr *MockDBManagerMockRecorder) GetReplicaRole(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicaRole", reflect.TypeOf((*MockDBManager)(nil).GetReplicaRole), arg0, arg1) -} - -// GrantUserRole mocks base method. -func (m *MockDBManager) GrantUserRole(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GrantUserRole", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// GrantUserRole indicates an expected call of GrantUserRole. -func (mr *MockDBManagerMockRecorder) GrantUserRole(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantUserRole", reflect.TypeOf((*MockDBManager)(nil).GrantUserRole), arg0, arg1, arg2) -} - -// HasOtherHealthyLeader mocks base method. -func (m *MockDBManager) HasOtherHealthyLeader(arg0 context.Context, arg1 *dcs.Cluster) *dcs.Member { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasOtherHealthyLeader", arg0, arg1) - ret0, _ := ret[0].(*dcs.Member) - return ret0 -} - -// HasOtherHealthyLeader indicates an expected call of HasOtherHealthyLeader. -func (mr *MockDBManagerMockRecorder) HasOtherHealthyLeader(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasOtherHealthyLeader", reflect.TypeOf((*MockDBManager)(nil).HasOtherHealthyLeader), arg0, arg1) -} - -// HasOtherHealthyMembers mocks base method. -func (m *MockDBManager) HasOtherHealthyMembers(arg0 context.Context, arg1 *dcs.Cluster, arg2 string) []*dcs.Member { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HasOtherHealthyMembers", arg0, arg1, arg2) - ret0, _ := ret[0].([]*dcs.Member) - return ret0 -} - -// HasOtherHealthyMembers indicates an expected call of HasOtherHealthyMembers. -func (mr *MockDBManagerMockRecorder) HasOtherHealthyMembers(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasOtherHealthyMembers", reflect.TypeOf((*MockDBManager)(nil).HasOtherHealthyMembers), arg0, arg1, arg2) -} - -// InitializeCluster mocks base method. -func (m *MockDBManager) InitializeCluster(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InitializeCluster", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// InitializeCluster indicates an expected call of InitializeCluster. -func (mr *MockDBManagerMockRecorder) InitializeCluster(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitializeCluster", reflect.TypeOf((*MockDBManager)(nil).InitializeCluster), arg0, arg1) -} - -// IsClusterHealthy mocks base method. -func (m *MockDBManager) IsClusterHealthy(arg0 context.Context, arg1 *dcs.Cluster) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsClusterHealthy", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsClusterHealthy indicates an expected call of IsClusterHealthy. -func (mr *MockDBManagerMockRecorder) IsClusterHealthy(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsClusterHealthy", reflect.TypeOf((*MockDBManager)(nil).IsClusterHealthy), arg0, arg1) -} - -// IsClusterInitialized mocks base method. -func (m *MockDBManager) IsClusterInitialized(arg0 context.Context, arg1 *dcs.Cluster) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsClusterInitialized", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsClusterInitialized indicates an expected call of IsClusterInitialized. -func (mr *MockDBManagerMockRecorder) IsClusterInitialized(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsClusterInitialized", reflect.TypeOf((*MockDBManager)(nil).IsClusterInitialized), arg0, arg1) -} - -// IsCurrentMemberHealthy mocks base method. -func (m *MockDBManager) IsCurrentMemberHealthy(arg0 context.Context, arg1 *dcs.Cluster) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsCurrentMemberHealthy", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsCurrentMemberHealthy indicates an expected call of IsCurrentMemberHealthy. -func (mr *MockDBManagerMockRecorder) IsCurrentMemberHealthy(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCurrentMemberHealthy", reflect.TypeOf((*MockDBManager)(nil).IsCurrentMemberHealthy), arg0, arg1) -} - -// IsCurrentMemberInCluster mocks base method. -func (m *MockDBManager) IsCurrentMemberInCluster(arg0 context.Context, arg1 *dcs.Cluster) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsCurrentMemberInCluster", arg0, arg1) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsCurrentMemberInCluster indicates an expected call of IsCurrentMemberInCluster. -func (mr *MockDBManagerMockRecorder) IsCurrentMemberInCluster(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCurrentMemberInCluster", reflect.TypeOf((*MockDBManager)(nil).IsCurrentMemberInCluster), arg0, arg1) -} - -// IsDBStartupReady mocks base method. -func (m *MockDBManager) IsDBStartupReady() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsDBStartupReady") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsDBStartupReady indicates an expected call of IsDBStartupReady. -func (mr *MockDBManagerMockRecorder) IsDBStartupReady() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDBStartupReady", reflect.TypeOf((*MockDBManager)(nil).IsDBStartupReady)) -} - -// IsFirstMember mocks base method. -func (m *MockDBManager) IsFirstMember() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsFirstMember") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsFirstMember indicates an expected call of IsFirstMember. -func (mr *MockDBManagerMockRecorder) IsFirstMember() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFirstMember", reflect.TypeOf((*MockDBManager)(nil).IsFirstMember)) -} - -// IsLeader mocks base method. -func (m *MockDBManager) IsLeader(arg0 context.Context, arg1 *dcs.Cluster) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsLeader", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsLeader indicates an expected call of IsLeader. -func (mr *MockDBManagerMockRecorder) IsLeader(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLeader", reflect.TypeOf((*MockDBManager)(nil).IsLeader), arg0, arg1) -} - -// IsLeaderMember mocks base method. -func (m *MockDBManager) IsLeaderMember(arg0 context.Context, arg1 *dcs.Cluster, arg2 *dcs.Member) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsLeaderMember", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsLeaderMember indicates an expected call of IsLeaderMember. -func (mr *MockDBManagerMockRecorder) IsLeaderMember(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLeaderMember", reflect.TypeOf((*MockDBManager)(nil).IsLeaderMember), arg0, arg1, arg2) -} - -// IsMemberHealthy mocks base method. -func (m *MockDBManager) IsMemberHealthy(arg0 context.Context, arg1 *dcs.Cluster, arg2 *dcs.Member) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsMemberHealthy", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsMemberHealthy indicates an expected call of IsMemberHealthy. -func (mr *MockDBManagerMockRecorder) IsMemberHealthy(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMemberHealthy", reflect.TypeOf((*MockDBManager)(nil).IsMemberHealthy), arg0, arg1, arg2) -} - -// IsMemberLagging mocks base method. -func (m *MockDBManager) IsMemberLagging(arg0 context.Context, arg1 *dcs.Cluster, arg2 *dcs.Member) (bool, int64) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsMemberLagging", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(int64) - return ret0, ret1 -} - -// IsMemberLagging indicates an expected call of IsMemberLagging. -func (mr *MockDBManagerMockRecorder) IsMemberLagging(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsMemberLagging", reflect.TypeOf((*MockDBManager)(nil).IsMemberLagging), arg0, arg1, arg2) -} - -// IsPromoted mocks base method. -func (m *MockDBManager) IsPromoted(arg0 context.Context) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsPromoted", arg0) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsPromoted indicates an expected call of IsPromoted. -func (mr *MockDBManagerMockRecorder) IsPromoted(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsPromoted", reflect.TypeOf((*MockDBManager)(nil).IsPromoted), arg0) -} - -// IsRootCreated mocks base method. -func (m *MockDBManager) IsRootCreated(arg0 context.Context) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsRootCreated", arg0) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsRootCreated indicates an expected call of IsRootCreated. -func (mr *MockDBManagerMockRecorder) IsRootCreated(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRootCreated", reflect.TypeOf((*MockDBManager)(nil).IsRootCreated), arg0) -} - -// IsRunning mocks base method. -func (m *MockDBManager) IsRunning() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsRunning") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsRunning indicates an expected call of IsRunning. -func (mr *MockDBManagerMockRecorder) IsRunning() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRunning", reflect.TypeOf((*MockDBManager)(nil).IsRunning)) -} - -// JoinCurrentMemberToCluster mocks base method. -func (m *MockDBManager) JoinCurrentMemberToCluster(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "JoinCurrentMemberToCluster", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// JoinCurrentMemberToCluster indicates an expected call of JoinCurrentMemberToCluster. -func (mr *MockDBManagerMockRecorder) JoinCurrentMemberToCluster(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JoinCurrentMemberToCluster", reflect.TypeOf((*MockDBManager)(nil).JoinCurrentMemberToCluster), arg0, arg1) -} - -// LeaderHealthyCheck mocks base method. -func (m *MockDBManager) LeaderHealthyCheck(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LeaderHealthyCheck", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// LeaderHealthyCheck indicates an expected call of LeaderHealthyCheck. -func (mr *MockDBManagerMockRecorder) LeaderHealthyCheck(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LeaderHealthyCheck", reflect.TypeOf((*MockDBManager)(nil).LeaderHealthyCheck), arg0, arg1) -} - -// LeaveMemberFromCluster mocks base method. -func (m *MockDBManager) LeaveMemberFromCluster(arg0 context.Context, arg1 *dcs.Cluster, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LeaveMemberFromCluster", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// LeaveMemberFromCluster indicates an expected call of LeaveMemberFromCluster. -func (mr *MockDBManagerMockRecorder) LeaveMemberFromCluster(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LeaveMemberFromCluster", reflect.TypeOf((*MockDBManager)(nil).LeaveMemberFromCluster), arg0, arg1, arg2) -} - -// ListSystemAccounts mocks base method. -func (m *MockDBManager) ListSystemAccounts(arg0 context.Context) ([]models.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSystemAccounts", arg0) - ret0, _ := ret[0].([]models.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListSystemAccounts indicates an expected call of ListSystemAccounts. -func (mr *MockDBManagerMockRecorder) ListSystemAccounts(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSystemAccounts", reflect.TypeOf((*MockDBManager)(nil).ListSystemAccounts), arg0) -} - -// ListUsers mocks base method. -func (m *MockDBManager) ListUsers(arg0 context.Context) ([]models.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListUsers", arg0) - ret0, _ := ret[0].([]models.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListUsers indicates an expected call of ListUsers. -func (mr *MockDBManagerMockRecorder) ListUsers(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockDBManager)(nil).ListUsers), arg0) -} - -// Lock mocks base method. -func (m *MockDBManager) Lock(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Lock", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Lock indicates an expected call of Lock. -func (mr *MockDBManagerMockRecorder) Lock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockDBManager)(nil).Lock), arg0, arg1) -} - -// MemberHealthyCheck mocks base method. -func (m *MockDBManager) MemberHealthyCheck(arg0 context.Context, arg1 *dcs.Cluster, arg2 *dcs.Member) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MemberHealthyCheck", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// MemberHealthyCheck indicates an expected call of MemberHealthyCheck. -func (mr *MockDBManagerMockRecorder) MemberHealthyCheck(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MemberHealthyCheck", reflect.TypeOf((*MockDBManager)(nil).MemberHealthyCheck), arg0, arg1, arg2) -} - -// MoveData mocks base method. -func (m *MockDBManager) MoveData(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MoveData", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// MoveData indicates an expected call of MoveData. -func (mr *MockDBManagerMockRecorder) MoveData(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveData", reflect.TypeOf((*MockDBManager)(nil).MoveData), arg0, arg1) -} - -// Promote mocks base method. -func (m *MockDBManager) Promote(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Promote", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Promote indicates an expected call of Promote. -func (mr *MockDBManagerMockRecorder) Promote(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Promote", reflect.TypeOf((*MockDBManager)(nil).Promote), arg0, arg1) -} - -// Query mocks base method. -func (m *MockDBManager) Query(arg0 context.Context, arg1 string) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Query", arg0, arg1) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Query indicates an expected call of Query. -func (mr *MockDBManagerMockRecorder) Query(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDBManager)(nil).Query), arg0, arg1) -} - -// Recover mocks base method. -func (m *MockDBManager) Recover(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recover", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Recover indicates an expected call of Recover. -func (mr *MockDBManagerMockRecorder) Recover(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recover", reflect.TypeOf((*MockDBManager)(nil).Recover), arg0, arg1) -} - -// RevokeUserRole mocks base method. -func (m *MockDBManager) RevokeUserRole(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevokeUserRole", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// RevokeUserRole indicates an expected call of RevokeUserRole. -func (mr *MockDBManagerMockRecorder) RevokeUserRole(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserRole", reflect.TypeOf((*MockDBManager)(nil).RevokeUserRole), arg0, arg1, arg2) -} - -// ShutDownWithWait mocks base method. -func (m *MockDBManager) ShutDownWithWait() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "ShutDownWithWait") -} - -// ShutDownWithWait indicates an expected call of ShutDownWithWait. -func (mr *MockDBManagerMockRecorder) ShutDownWithWait() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutDownWithWait", reflect.TypeOf((*MockDBManager)(nil).ShutDownWithWait)) -} - -// Start mocks base method. -func (m *MockDBManager) Start(arg0 context.Context, arg1 *dcs.Cluster) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Start", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Start indicates an expected call of Start. -func (mr *MockDBManagerMockRecorder) Start(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDBManager)(nil).Start), arg0, arg1) -} - -// Stop mocks base method. -func (m *MockDBManager) Stop() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Stop") - ret0, _ := ret[0].(error) - return ret0 -} - -// Stop indicates an expected call of Stop. -func (mr *MockDBManagerMockRecorder) Stop() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockDBManager)(nil).Stop)) -} - -// Unlock mocks base method. -func (m *MockDBManager) Unlock(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Unlock", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Unlock indicates an expected call of Unlock. -func (mr *MockDBManagerMockRecorder) Unlock(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockDBManager)(nil).Unlock), arg0) -} diff --git a/pkg/lorry/engines/etcd/get_replica_role.go b/pkg/lorry/engines/etcd/get_replica_role.go deleted file mode 100644 index 42f37f0ac0f..00000000000 --- a/pkg/lorry/engines/etcd/get_replica_role.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package etcd - -import ( - "context" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - etcdResp, err := mgr.etcd.Status(ctx, mgr.endpoint) - if err != nil { - return "", err - } - - role := models.FOLLOWER - switch { - case etcdResp.Leader == etcdResp.Header.MemberId: - role = models.LEADER - case etcdResp.IsLearner: - role = models.LEARNER - } - - return role, nil -} diff --git a/pkg/lorry/engines/etcd/manager.go b/pkg/lorry/engines/etcd/manager.go deleted file mode 100644 index 45b8934f3f1..00000000000 --- a/pkg/lorry/engines/etcd/manager.go +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package etcd - -import ( - "context" - "strconv" - "strings" - "time" - - v3 "go.etcd.io/etcd/client/v3" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -const ( - endpoint = "endpoint" - - defaultPort = 2379 - defaultDialTimeout = 600 * time.Millisecond -) - -type Manager struct { - engines.DBManagerBase - etcd *v3.Client - endpoint string -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("ETCD") - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - mgr := &Manager{ - DBManagerBase: *managerBase, - } - - var endpoints []string - endpoint, ok := properties[endpoint] - if ok { - mgr.endpoint = endpoint - endpoints = []string{endpoint} - } - - cli, err := v3.New(v3.Config{ - Endpoints: endpoints, - DialTimeout: defaultDialTimeout, - }) - if err != nil { - return nil, err - } - - mgr.etcd = cli - return mgr, nil -} - -func (mgr *Manager) IsDBStartupReady() bool { - if mgr.DBStartupReady { - return true - } - - ctx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout) - status, err := mgr.etcd.Status(ctx, mgr.endpoint) - cancel() - if err != nil { - mgr.Logger.Info("get etcd status failed", "error", err, "status", status) - return false - } - - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func (mgr *Manager) GetRunningPort() int { - index := strings.Index(mgr.endpoint, ":") - if index < 0 { - return defaultPort - } - port, err := strconv.Atoi(mgr.endpoint[index+1:]) - if err != nil { - return defaultPort - } - - return port -} diff --git a/pkg/lorry/engines/etcd/manager_test.go b/pkg/lorry/engines/etcd/manager_test.go deleted file mode 100644 index 36027a1d870..00000000000 --- a/pkg/lorry/engines/etcd/manager_test.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package etcd - -import ( - "context" - "fmt" - "net" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -const ( - urlWithPort = "127.0.0.1:2379" -) - -// Test case for Init() function -var _ = Describe("ETCD DBManager", func() { - // Set up relevant viper config variables - viper.Set(constant.KBEnvServiceUser, "testuser") - viper.Set(constant.KBEnvServicePassword, "testpassword") - Context("new db manager", func() { - It("with right configurations", func() { - properties := engines.Properties{ - "endpoint": urlWithPort, - } - dbManger, err := NewManager(properties) - Expect(err).Should(Succeed()) - Expect(dbManger).ShouldNot(BeNil()) - }) - - It("with wrong configurations", func() { - properties := engines.Properties{} - dbManger, err := NewManager(properties) - Expect(err).Should(HaveOccurred()) - Expect(dbManger).Should(BeNil()) - }) - }) - - Context("is db startup ready", func() { - It("it is ready", func() { - etcdServer, err := StartEtcdServer() - Expect(err).Should(BeNil()) - defer etcdServer.Stop() - testEndpoint := fmt.Sprintf("http://%s", etcdServer.ETCD.Clients[0].Addr().(*net.TCPAddr).String()) - manager := &Manager{ - etcd: etcdServer.client, - endpoint: testEndpoint, - } - Expect(manager.IsDBStartupReady()).Should(BeTrue()) - }) - - It("it is not ready", func() { - etcdServer, err := StartEtcdServer() - Expect(err).Should(BeNil()) - etcdServer.Stop() - testEndpoint := fmt.Sprintf("http://%s", etcdServer.ETCD.Clients[0].Addr().(*net.TCPAddr).String()) - properties := engines.Properties{ - "endpoint": testEndpoint, - } - manager, err := NewManager(properties) - Expect(err).Should(BeNil()) - Expect(manager).ShouldNot(BeNil()) - Expect(manager.IsDBStartupReady()).Should(BeFalse()) - }) - }) - - Context("get replica role", func() { - It("get leader", func() { - etcdServer, err := StartEtcdServer() - Expect(err).Should(BeNil()) - defer etcdServer.Stop() - testEndpoint := fmt.Sprintf("http://%s", etcdServer.ETCD.Clients[0].Addr().(*net.TCPAddr).String()) - manager := &Manager{ - etcd: etcdServer.client, - endpoint: testEndpoint, - } - role, err := manager.GetReplicaRole(context.Background(), nil) - Expect(err).Should(BeNil()) - Expect(role).Should(Equal("Leader")) - }) - }) -}) diff --git a/pkg/lorry/engines/etcd/suite_test.go b/pkg/lorry/engines/etcd/suite_test.go deleted file mode 100644 index ceb891e7a8f..00000000000 --- a/pkg/lorry/engines/etcd/suite_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package etcd - -import ( - "errors" - "net/url" - "os" - "testing" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/go-logr/logr" - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - clientv3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/server/v3/embed" - "go.etcd.io/etcd/server/v3/etcdserver/api/v3client" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -const ( - etcdStartTimeout = 30 -) - -var ( - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS - etcdServer *EmbeddedETCD -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test-0") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestETCDDBManager(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ETCD DBManager. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock dcs store - InitMockDCSStore() - - // Start ETCD Server - // server, err := StartEtcdServer() - // Expect(err).Should(BeNil()) - // etcdServer = server -}) - -var _ = AfterSuite(func() { - StopEtcdServer(etcdServer) -}) - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} - -func StartEtcdServer() (*EmbeddedETCD, error) { - peerAddress := "http://localhost:0" - - etcdServer := &EmbeddedETCD{} - logger := ctrl.Log.WithName("ETCD server") - etcdServer.logger = logger - return etcdServer, etcdServer.Start(peerAddress) -} - -func StopEtcdServer(etcdServer *EmbeddedETCD) { - if etcdServer != nil { - etcdServer.Stop() - } -} - -type EmbeddedETCD struct { - logger logr.Logger - tmpDir string - ETCD *embed.Etcd - client *clientv3.Client -} - -// Start starts embedded ETCD. -func (e *EmbeddedETCD) Start(peerAddress string) error { - dir, err := os.MkdirTemp("", "ETCD") - if err != nil { - return err - } - - cfg := embed.NewConfig() - cfg.Dir = dir - lpurl, _ := url.Parse("http://localhost:0") - lcurl, _ := url.Parse(peerAddress) - cfg.ListenPeerUrls = []url.URL{*lpurl} - cfg.ListenClientUrls = []url.URL{*lcurl} - e.ETCD, err = embed.StartEtcd(cfg) - if err != nil { - return err - } - - select { - case <-e.ETCD.Server.ReadyNotify(): - e.logger.Info("ETCD Server is ready!") - case <-time.After(etcdStartTimeout * time.Second): - e.ETCD.Server.Stop() // trigger a shutdown - return errors.New("start embedded etcd server timeout") - } - e.client = v3client.New(e.ETCD.Server) - - return nil -} - -// Stop stops the embedded ETCD & cleanups the tmp dir. -func (e *EmbeddedETCD) Stop() { - e.ETCD.Close() - e.ETCD.Server.Stop() - os.RemoveAll(e.tmpDir) -} diff --git a/pkg/lorry/engines/foxlake/commands.go b/pkg/lorry/engines/foxlake/commands.go deleted file mode 100644 index 31e360a7b8f..00000000000 --- a/pkg/lorry/engines/foxlake/commands.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package foxlake - -import ( - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "usql", - Container: "foxlake", - UserEnv: "$FOXLAKE_ROOT_USER", - PasswordEnv: "$FOXLAKE_ROOT_PASSWORD", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# foxlake client connection example -mysql -h%s -P%s -u%s -p%s -`, info.Host, info.Port, info.User, info.Password) - }, - }, - } -} - -func (r *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := r.info.UserEnv - userPass := r.info.PasswordEnv - dsn := fmt.Sprintf("mysql://%s:'%s'@:${serverPort}", userName, userPass) - if connectInfo != nil { - userName = connectInfo.UserName - userPass = connectInfo.UserPasswd - dsn = fmt.Sprintf("mysql://%s:'%s'@:${serverPort}", userName, userPass) - } - - foxlakeCmd := []string{fmt.Sprintf("%s %s", r.info.Client, dsn)} - return []string{"sh", "-c", strings.Join(foxlakeCmd, " ")} -} - -func (r *Commands) Container() string { - return r.info.Container -} - -func (r *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, r.examples) -} - -func (r *Commands) ExecuteCommand([]string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("%s not implemented", r.info.Client) -} diff --git a/pkg/lorry/engines/foxlake/commands_test.go b/pkg/lorry/engines/foxlake/commands_test.go deleted file mode 100644 index f1015938264..00000000000 --- a/pkg/lorry/engines/foxlake/commands_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package foxlake - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Foxlake Engine", func() { - It("connection command", func() { - foxlake := NewCommands() - - Expect(foxlake.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(foxlake.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - foxlake := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range foxlake.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(foxlake.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(foxlake.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/foxlake/suite_test.go b/pkg/lorry/engines/foxlake/suite_test.go deleted file mode 100644 index b844f4b1851..00000000000 --- a/pkg/lorry/engines/foxlake/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package foxlake - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "foxlake Suite") -} diff --git a/pkg/lorry/engines/generate.go b/pkg/lorry/engines/generate.go deleted file mode 100644 index 5e984435afd..00000000000 --- a/pkg/lorry/engines/generate.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package engines -destination dbmanager_mock.go github.com/apecloud/kubeblocks/pkg/lorry/engines DBManager diff --git a/pkg/lorry/engines/interface.go b/pkg/lorry/engines/interface.go deleted file mode 100644 index c194e9cd64a..00000000000 --- a/pkg/lorry/engines/interface.go +++ /dev/null @@ -1,128 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "context" - - "github.com/go-logr/logr" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -type DBManager interface { - IsRunning() bool - - IsDBStartupReady() bool - - // Functions related to cluster initialization. - InitializeCluster(context.Context, *dcs.Cluster) error - IsClusterInitialized(context.Context, *dcs.Cluster) (bool, error) - // IsCurrentMemberInCluster checks if current member is configured in cluster for consensus. - // it will always return true for replicationset. - IsCurrentMemberInCluster(context.Context, *dcs.Cluster) bool - - // IsClusterHealthy is only for consensus cluster healthy check. - // For Replication cluster IsClusterHealthy will always return true, - // and its cluster's healthy is equal to leader member's healthy. - IsClusterHealthy(context.Context, *dcs.Cluster) bool - - // Member healthy check - MemberHealthyCheck(context.Context, *dcs.Cluster, *dcs.Member) error - LeaderHealthyCheck(context.Context, *dcs.Cluster) error - CurrentMemberHealthyCheck(context.Context, *dcs.Cluster) error - // IsMemberHealthy focuses on the database's read and write capabilities. - IsMemberHealthy(context.Context, *dcs.Cluster, *dcs.Member) bool - IsCurrentMemberHealthy(context.Context, *dcs.Cluster) bool - // IsMemberLagging focuses on the latency between the leader and standby - IsMemberLagging(context.Context, *dcs.Cluster, *dcs.Member) (bool, int64) - GetLag(context.Context, *dcs.Cluster) (int64, error) - - // GetDBState will get most required database kernel states of current member in one HA loop to Avoiding duplicate queries and conserve I/O. - // We believe that the states of database kernel remains unchanged within a single HA loop. - GetDBState(context.Context, *dcs.Cluster) *dcs.DBState - - // HasOtherHealthyLeader is applicable only to consensus cluster, - // where the db's internal role services as the source of truth. - // for replicationset cluster, HasOtherHealthyLeader will always be nil. - HasOtherHealthyLeader(context.Context, *dcs.Cluster) *dcs.Member - HasOtherHealthyMembers(context.Context, *dcs.Cluster, string) []*dcs.Member - - // Functions related to replica member relationship. - IsLeader(context.Context, *dcs.Cluster) (bool, error) - IsLeaderMember(context.Context, *dcs.Cluster, *dcs.Member) (bool, error) - IsFirstMember() bool - GetReplicaRole(context.Context, *dcs.Cluster) (string, error) - - JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error - LeaveMemberFromCluster(context.Context, *dcs.Cluster, string) error - - // IsPromoted is applicable only to consensus cluster, which is used to - // check if DB has complete switchover. - // for replicationset cluster, it will always be true. - IsPromoted(context.Context) bool - // Functions related to HA - // The functions should be idempotent, indicating that if they have been executed in one ha cycle, - // any subsequent calls during that cycle will have no effect. - Promote(context.Context, *dcs.Cluster) error - Demote(context.Context) error - Follow(context.Context, *dcs.Cluster) error - Recover(context.Context, *dcs.Cluster) error - - // Start and Stop just send signal to lorryctl - Start(context.Context, *dcs.Cluster) error - Stop() error - - // GetHealthiestMember(*dcs.Cluster, string) *dcs.Member - // IsHealthiestMember(*dcs.Cluster) bool - - GetCurrentMemberName() string - GetMemberAddrs(context.Context, *dcs.Cluster) []string - - // Functions related to account manage - IsRootCreated(context.Context) (bool, error) - CreateRoot(context.Context) error - - // Readonly lock for disk full - Lock(context.Context, string) error - Unlock(context.Context) error - - // sql query - Exec(context.Context, string) (int64, error) - Query(context.Context, string) ([]byte, error) - - // user management - ListUsers(context.Context) ([]models.UserInfo, error) - ListSystemAccounts(context.Context) ([]models.UserInfo, error) - CreateUser(context.Context, string, string, string) error - DeleteUser(context.Context, string) error - DescribeUser(context.Context, string) (*models.UserInfo, error) - GrantUserRole(context.Context, string, string) error - RevokeUserRole(context.Context, string, string) error - - GetPort() (int, error) - - MoveData(context.Context, *dcs.Cluster) error - - GetLogger() logr.Logger - - ShutDownWithWait() -} diff --git a/pkg/lorry/engines/mock.go b/pkg/lorry/engines/mock.go deleted file mode 100644 index b943b5a4082..00000000000 --- a/pkg/lorry/engines/mock.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "context" - "fmt" - - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -type MockManager struct { - DBManagerBase -} - -var _ DBManager = &MockManager{} - -func NewMockManager(properties Properties) (DBManager, error) { - logger := ctrl.Log.WithName("MockManager") - - managerBase, err := NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - Mgr := &MockManager{ - DBManagerBase: *managerBase, - } - - return Mgr, nil -} -func (*MockManager) IsRunning() bool { - return true -} - -func (*MockManager) IsDBStartupReady() bool { - return true -} - -func (*MockManager) InitializeCluster(context.Context, *dcs.Cluster) error { - return fmt.Errorf("NotSupported") -} -func (*MockManager) IsClusterInitialized(context.Context, *dcs.Cluster) (bool, error) { - return false, fmt.Errorf("NotSupported") -} - -func (*MockManager) IsCurrentMemberInCluster(context.Context, *dcs.Cluster) bool { - return true -} - -func (*MockManager) IsCurrentMemberHealthy(context.Context, *dcs.Cluster) bool { - return true -} - -func (*MockManager) IsClusterHealthy(context.Context, *dcs.Cluster) bool { - return true -} - -func (*MockManager) IsMemberHealthy(context.Context, *dcs.Cluster, *dcs.Member) bool { - return true -} - -func (*MockManager) HasOtherHealthyLeader(context.Context, *dcs.Cluster) *dcs.Member { - return nil -} - -func (*MockManager) HasOtherHealthyMembers(context.Context, *dcs.Cluster, string) []*dcs.Member { - return nil -} - -func (*MockManager) IsLeader(context.Context, *dcs.Cluster) (bool, error) { - return false, fmt.Errorf("NotSupported") -} - -func (*MockManager) IsLeaderMember(context.Context, *dcs.Cluster, *dcs.Member) (bool, error) { - return false, fmt.Errorf("NotSupported") -} - -func (*MockManager) IsFirstMember() bool { - return true -} - -func (*MockManager) JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) LeaveMemberFromCluster(context.Context, *dcs.Cluster, string) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) Promote(context.Context, *dcs.Cluster) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) IsPromoted(context.Context) bool { - return true -} - -func (*MockManager) Demote(context.Context) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) Follow(context.Context, *dcs.Cluster) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) Recover(context.Context, *dcs.Cluster) error { - return nil - -} - -func (*MockManager) GetHealthiestMember(*dcs.Cluster, string) *dcs.Member { - return nil -} - -func (*MockManager) GetMemberAddrs(context.Context, *dcs.Cluster) []string { - return nil -} - -func (*MockManager) IsRootCreated(context.Context) (bool, error) { - return false, fmt.Errorf("NotSupported") -} - -func (*MockManager) CreateRoot(context.Context) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) Lock(context.Context, string) error { - return fmt.Errorf("NotSupported") -} - -func (*MockManager) Unlock(context.Context) error { - return fmt.Errorf("NotSupported") -} diff --git a/pkg/lorry/engines/models/client_types.go b/pkg/lorry/engines/models/client_types.go deleted file mode 100644 index 6bf1d1d2688..00000000000 --- a/pkg/lorry/engines/models/client_types.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -type ClientType string - -const ( - CLI ClientType = "cli" - DJANGO ClientType = "django" - DOTNET ClientType = ".net" - GO ClientType = "go" - JAVA ClientType = "java" - NODEJS ClientType = "node.js" - PHP ClientType = "php" - PRISMA ClientType = "prisma" - PYTHON ClientType = "python" - RAILS ClientType = "rails" - RUST ClientType = "rust" - SYMFONY ClientType = "symfony" -) - -func ClientTypes() []string { - return []string{CLI.String(), DJANGO.String(), DOTNET.String(), GO.String(), - JAVA.String(), NODEJS.String(), PHP.String(), PRISMA.String(), - PYTHON.String(), RAILS.String(), RUST.String(), SYMFONY.String(), - } -} - -func (t ClientType) String() string { - return string(t) -} diff --git a/pkg/lorry/engines/models/engine_types.go b/pkg/lorry/engines/models/engine_types.go deleted file mode 100644 index ea163258281..00000000000 --- a/pkg/lorry/engines/models/engine_types.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -type EngineType string - -const ( - MySQL EngineType = "mysql" - WeSQL EngineType = "wesql" - PostgreSQL EngineType = "postgresql" - OfficialPostgreSQL EngineType = "official-postgresql" - ApecloudPostgreSQL EngineType = "apecloud-postgresql" - Redis EngineType = "redis" - ETCD EngineType = "etcd" - MongoDB EngineType = "mongodb" - Nebula EngineType = "nebula" - PolarDBX EngineType = "polardbx" - PulsarBroker EngineType = "pulsar-broker" - PulsarProxy EngineType = "pulsar-proxy" - FoxLake EngineType = "foxlake" - Oceanbase EngineType = "oceanbase" - Oracle EngineType = "oracle" - OpenGauss EngineType = "opengauss" - Custom EngineType = "custom" -) diff --git a/pkg/lorry/engines/models/errors.go b/pkg/lorry/engines/models/errors.go deleted file mode 100644 index fc4fbe10ba2..00000000000 --- a/pkg/lorry/engines/models/errors.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -import "fmt" - -const ( - errMsgNoSQL = "no sql provided" - errMsgNoUserName = "no username provided" - errMsgNoPassword = "no password provided" - errMsgNoRoleName = "no rolename provided" - errMsgInvalidRoleName = "invalid rolename, should be one of [superuser, readwrite, readonly]" - errMsgNoSuchUser = "no such user" - errMsgNotImplemented = "not implemented" -) - -var ( - ErrNoSQL = fmt.Errorf("%s", errMsgNoSQL) - ErrNoUserName = fmt.Errorf("%s", errMsgNoUserName) - ErrNoPassword = fmt.Errorf("%s", errMsgNoPassword) - ErrNoRoleName = fmt.Errorf("%s", errMsgNoRoleName) - ErrInvalidRoleName = fmt.Errorf("%s", errMsgInvalidRoleName) - ErrNoSuchUser = fmt.Errorf("%s", errMsgNoSuchUser) - ErrNotImplemented = fmt.Errorf("%s", errMsgNotImplemented) -) diff --git a/pkg/lorry/engines/models/replca_role_types.go b/pkg/lorry/engines/models/replca_role_types.go deleted file mode 100644 index 04f67ab6cf9..00000000000 --- a/pkg/lorry/engines/models/replca_role_types.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -import "strings" - -const ( - PRIMARY = "primary" - SECONDARY = "secondary" - MASTER = "master" - SLAVE = "slave" - LEADER = "Leader" - FOLLOWER = "Follower" - LEARNER = "Learner" - CANDIDATE = "Candidate" -) - -// IsLikelyPrimaryRole returns true if the role is primary, -// it is used for the case where db manager do not implemement the IsLeader method. -// use it curefully, as it is for normal case, and may be wrong for some special cases. -func IsLikelyPrimaryRole(role string) bool { - return strings.EqualFold(role, PRIMARY) || strings.EqualFold(role, MASTER) || strings.EqualFold(role, LEADER) -} diff --git a/pkg/lorry/engines/models/role_types.go b/pkg/lorry/engines/models/role_types.go deleted file mode 100644 index 0aef4650001..00000000000 --- a/pkg/lorry/engines/models/role_types.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -import "strings" - -const ( - SuperUserRole RoleType = "superuser" - ReadWriteRole RoleType = "readwrite" - ReadOnlyRole RoleType = "readonly" - NoPrivileges RoleType = "" - CustomizedRole RoleType = "customized" - InvalidRole RoleType = "invalid" -) - -type RoleType string - -func (r RoleType) EqualTo(role string) bool { - return strings.EqualFold(string(r), role) -} - -func (r RoleType) GetWeight() int32 { - switch r { - case SuperUserRole: - return 1 << 3 - case ReadWriteRole: - return 1 << 2 - case ReadOnlyRole: - return 1 << 1 - case CustomizedRole: - return 1 - default: - return 0 - } -} - -func SortRoleByWeight(r1, r2 RoleType) bool { - return int(r1.GetWeight()) > int(r2.GetWeight()) -} - -func String2RoleType(roleName string) RoleType { - if SuperUserRole.EqualTo(roleName) { - return SuperUserRole - } - if ReadWriteRole.EqualTo(roleName) { - return ReadWriteRole - } - if ReadOnlyRole.EqualTo(roleName) { - return ReadOnlyRole - } - if NoPrivileges.EqualTo(roleName) { - return NoPrivileges - } - return CustomizedRole -} diff --git a/pkg/lorry/engines/models/userinfo.go b/pkg/lorry/engines/models/userinfo.go deleted file mode 100644 index 22cb7789b3e..00000000000 --- a/pkg/lorry/engines/models/userinfo.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package models - -import ( - "time" -) - -// UserInfo is the user information for account management -type UserInfo struct { - UserName string `json:"userName"` - Password string `json:"password,omitempty"` - Statement string `json:"statement,omitempty"` - Expired string `json:"expired,omitempty"` - ExpireAt time.Duration `json:"expireAt,omitempty"` - RoleName string `json:"roleName,omitempty"` -} - -func (user *UserInfo) UserNameValidator() error { - if user.UserName == "" { - return ErrNoUserName - } - return nil -} - -func (user *UserInfo) PasswdValidator() error { - if user.Password == "" { - return ErrNoPassword - } - return nil -} - -func (user *UserInfo) RoleValidator() error { - if user.RoleName == "" { - return ErrNoRoleName - } - - roles := []RoleType{ReadOnlyRole, ReadWriteRole, SuperUserRole} - for _, role := range roles { - if role.EqualTo(user.RoleName) { - return nil - } - } - return ErrInvalidRoleName -} - -func (user *UserInfo) UserNameAndPasswdValidator() error { - if err := user.UserNameValidator(); err != nil { - return err - } - - if err := user.PasswdValidator(); err != nil { - return err - } - return nil -} - -func (user *UserInfo) UserNameAndRoleValidator() error { - if err := user.UserNameValidator(); err != nil { - return err - } - - if err := user.RoleValidator(); err != nil { - return err - } - return nil -} diff --git a/pkg/lorry/engines/mongodb/client.go b/pkg/lorry/engines/mongodb/client.go deleted file mode 100644 index 79020076f5d..00000000000 --- a/pkg/lorry/engines/mongodb/client.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" - "go.mongodb.org/mongo-driver/mongo/writeconcern" -) - -func NewMongodbClient(ctx context.Context, config *Config) (*mongo.Client, error) { - if len(config.Hosts) == 0 { - return nil, errors.New("Get replset client without hosts") - } - - opts := options.Client(). - SetHosts(config.Hosts). - SetReplicaSet(config.ReplSetName). - SetAuth(options.Credential{ - Password: config.Password, - Username: config.Username, - }). - SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))). - SetReadPreference(readpref.Primary()). - SetDirect(config.Direct) - - client, err := mongo.Connect(ctx, opts) - if err != nil { - return nil, errors.Wrap(err, "connect to mongodb") - } - return client, nil -} - -func NewReplSetClient(ctx context.Context, hosts []string) (*mongo.Client, error) { - config := GetConfig().DeepCopy() - config.Hosts = hosts - config.Direct = false - return NewMongodbClient(ctx, config) - -} - -func NewMongosClient(ctx context.Context, hosts []string) (*mongo.Client, error) { - config := GetConfig().DeepCopy() - config.Hosts = hosts - config.Direct = false - config.ReplSetName = "" - - return NewMongodbClient(ctx, config) -} - -func NewStandaloneClient(ctx context.Context, host string) (*mongo.Client, error) { - config := GetConfig().DeepCopy() - config.Hosts = []string{host} - config.Direct = true - config.ReplSetName = "" - - return NewMongodbClient(ctx, config) -} - -func NewLocalUnauthClient(ctx context.Context) (*mongo.Client, error) { - config := GetConfig().DeepCopy() - config.Direct = true - config.ReplSetName = "" - - opts := options.Client(). - SetHosts(config.Hosts). - SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))). - SetReadPreference(readpref.Primary()). - SetDirect(config.Direct) - - client, err := mongo.Connect(ctx, opts) - if err != nil { - return nil, errors.Wrap(err, "connect to mongodb") - } - - return client, nil -} diff --git a/pkg/lorry/engines/mongodb/commands.go b/pkg/lorry/engines/mongodb/commands.go deleted file mode 100644 index ad62c0ec62c..00000000000 --- a/pkg/lorry/engines/mongodb/commands.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "fmt" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "mongosh", - Container: "mongodb", - UserEnv: "$MONGODB_ROOT_USER", - PasswordEnv: "$MONGODB_ROOT_PASSWORD", - Database: "admin", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# mongodb client connection example -mongosh mongodb://%s:%s@%s/%s -`, info.User, info.Password, info.Host, info.Database) - }, - }, - } -} - -func (r Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := r.info.UserEnv - userPass := r.info.PasswordEnv - dsn := fmt.Sprintf("mongodb://%s:%s@$KB_POD_FQDN:$SERVICE_PORT/admin?replicaSet=$KB_CLUSTER_COMP_NAME", userName, userPass) - if connectInfo != nil { - userName = connectInfo.UserName - userPass = connectInfo.UserPasswd - dsn = fmt.Sprintf("mongodb://%s:%s@$KB_POD_FQDN:$SERVICE_PORT/admin?replicaSet=$KB_CLUSTER_COMP_NAME", userName, userPass) - } - - mongodbCmd := fmt.Sprintf("export CLIENT=`which mongosh>/dev/null&&echo %s||echo mongo`; $CLIENT %s", r.info.Client, dsn) - return []string{"sh", "-c", mongodbCmd} -} - -func (r Commands) Container() string { - return r.info.Container -} - -func (r Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - if len(info.Database) == 0 { - info.Database = r.info.Database - } - return engines.BuildExample(info, client, r.examples) -} - -func (r Commands) ExecuteCommand([]string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("%s not implemented", r.info.Client) -} diff --git a/pkg/lorry/engines/mongodb/commands_test.go b/pkg/lorry/engines/mongodb/commands_test.go deleted file mode 100644 index 58b51e03c6f..00000000000 --- a/pkg/lorry/engines/mongodb/commands_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("MongoDB Engine", func() { - It("connection command", func() { - mongodb := NewCommands() - - Expect(mongodb.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(mongodb.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - mongodb := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range mongodb.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(mongodb.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(mongodb.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/mongodb/config.go b/pkg/lorry/engines/mongodb/config.go deleted file mode 100644 index 9465c3e11a1..00000000000 --- a/pkg/lorry/engines/mongodb/config.go +++ /dev/null @@ -1,141 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "errors" - "net" - "strconv" - "time" - - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - utilconfig "github.com/apecloud/kubeblocks/pkg/lorry/util/config" -) - -const ( - host = "host" - username = "username" - password = "password" - server = "server" - databaseName = "databaseName" - operationTimeout = "operationTimeout" - params = "params" - adminDatabase = "admin" - - defaultTimeout = 5 * time.Second - defaultDBPort = 27017 -) - -type Config struct { - Hosts []string - Username string - Password string - ReplSetName string - DatabaseName string - Params string - Direct bool - OperationTimeout time.Duration -} - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - config = &Config{ - Direct: true, - Username: "root", - OperationTimeout: defaultTimeout, - } - - if val, ok := properties[host]; ok && val != "" { - config.Hosts = []string{val} - } - - if viper.IsSet(constant.KBEnvServicePort) { - config.Hosts = []string{"localhost:" + viper.GetString(constant.KBEnvServicePort)} - } - - if len(config.Hosts) == 0 { - return nil, errors.New("must set 'host' in metadata or KB_SERVICE_PORT environment variable") - } - - if val, ok := properties[username]; ok && val != "" { - config.Username = val - } - - if val, ok := properties[password]; ok && val != "" { - config.Password = val - } - - if viper.IsSet(constant.KBEnvServiceUser) { - config.Username = viper.GetString(constant.KBEnvServiceUser) - } - - if viper.IsSet(constant.KBEnvServicePassword) { - config.Password = viper.GetString(constant.KBEnvServicePassword) - } - - if viper.IsSet(constant.KBEnvClusterCompName) { - config.ReplSetName = viper.GetString(constant.KBEnvClusterCompName) - } - - config.DatabaseName = adminDatabase - if val, ok := properties[databaseName]; ok && val != "" { - config.DatabaseName = val - } - - if val, ok := properties[params]; ok && val != "" { - config.Params = val - } - - var err error - if val, ok := properties[operationTimeout]; ok && val != "" { - config.OperationTimeout, err = time.ParseDuration(val) - if err != nil { - return nil, errors.New("incorrect operationTimeout field from metadata") - } - } - - return config, nil -} - -func (config *Config) GetDBPort() int { - _, portStr, err := net.SplitHostPort(config.Hosts[0]) - if err != nil { - return defaultDBPort - } - - port, err := strconv.Atoi(portStr) - if err != nil { - return defaultDBPort - } - - return port -} - -func (config *Config) DeepCopy() *Config { - newConf, _ := utilconfig.Clone(config) - return newConf.(*Config) -} - -func GetConfig() *Config { - return config -} diff --git a/pkg/lorry/engines/mongodb/config_test.go b/pkg/lorry/engines/mongodb/config_test.go deleted file mode 100644 index e8ab8fea8ea..00000000000 --- a/pkg/lorry/engines/mongodb/config_test.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetMongoDBMetadata(t *testing.T) { - t.Run("With defaults", func(t *testing.T) { - properties := map[string]string{ - host: "127.0.0.1", - } - - metadata, err := NewConfig(properties) - assert.Nil(t, err) - assert.Equal(t, properties[host], metadata.Hosts[0]) - assert.Equal(t, adminDatabase, metadata.DatabaseName) - }) - - t.Run("With custom values", func(t *testing.T) { - properties := map[string]string{ - host: "127.0.0.2", - databaseName: "TestDB", - username: "username", - password: "password", - } - - metadata, err := NewConfig(properties) - assert.Nil(t, err) - assert.Equal(t, properties[host], metadata.Hosts[0]) - assert.Equal(t, properties[databaseName], metadata.DatabaseName) - assert.Equal(t, properties[username], metadata.Username) - assert.Equal(t, properties[password], metadata.Password) - }) - - t.Run("Missing hosts", func(t *testing.T) { - properties := map[string]string{ - username: "username", - password: "password", - } - - _, err := NewConfig(properties) - assert.NotNil(t, err) - }) - - t.Run("Invalid without host/server", func(t *testing.T) { - properties := map[string]string{ - databaseName: "TestDB", - } - - _, err := NewConfig(properties) - assert.NotNil(t, err) - - expected := "must set 'host' in metadata or KB_SERVICE_PORT environment variable" - assert.Equal(t, expected, err.Error()) - }) -} diff --git a/pkg/lorry/engines/mongodb/get_replica_role.go b/pkg/lorry/engines/mongodb/get_replica_role.go deleted file mode 100644 index 2012a3b566d..00000000000 --- a/pkg/lorry/engines/mongodb/get_replica_role.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - return mgr.GetMemberState(ctx) -} diff --git a/pkg/lorry/engines/mongodb/manager.go b/pkg/lorry/engines/mongodb/manager.go deleted file mode 100644 index a8be2187015..00000000000 --- a/pkg/lorry/engines/mongodb/manager.go +++ /dev/null @@ -1,836 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - "encoding/json" - "fmt" - "math/rand" - "strings" - "time" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" - "go.mongodb.org/mongo-driver/mongo/writeconcern" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -const ( - PrimaryPriority = 2 - SecondaryPriority = 1 - - ServiceType = "mongodb" -) - -type Manager struct { - engines.DBManagerBase - Client *mongo.Client - Database *mongo.Database -} - -var Mgr *Manager -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - ctx := context.Background() - logger := ctrl.Log.WithName("MongoDB") - config, err := NewConfig(properties) - if err != nil { - return nil, err - } - - opts := options.Client(). - SetHosts(config.Hosts). - SetReplicaSet(config.ReplSetName). - SetAuth(options.Credential{ - Password: config.Password, - Username: config.Username, - }). - SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))). - SetReadPreference(readpref.Primary()). - SetDirect(config.Direct) - - client, err := mongo.Connect(ctx, opts) - if err != nil { - return nil, errors.Wrap(err, "connect to mongodb") - } - - defer func() { - if err != nil { - derr := client.Disconnect(ctx) - if derr != nil { - logger.Info("failed to disconnect", "error", derr.Error()) - } - } - }() - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - Mgr = &Manager{ - DBManagerBase: *managerBase, - Client: client, - Database: client.Database(config.DatabaseName), - } - - return Mgr, nil -} - -func (mgr *Manager) InitializeCluster(ctx context.Context, cluster *dcs.Cluster) error { - return mgr.InitiateReplSet(ctx, cluster) -} - -// InitiateReplSet is a method to create MongoDB cluster -func (mgr *Manager) InitiateReplSet(ctx context.Context, cluster *dcs.Cluster) error { - configMembers := make([]ConfigMember, len(cluster.Members)) - - for i, member := range cluster.Members { - configMembers[i].ID = i - configMembers[i].Host = cluster.GetMemberAddrWithPort(member) - if strings.HasPrefix(member.Name, mgr.CurrentMemberName) || strings.HasPrefix(member.Name, mgr.CurrentMemberIP) { - configMembers[i].Priority = PrimaryPriority - } else { - configMembers[i].Priority = SecondaryPriority - } - } - - config := RSConfig{ - ID: mgr.ClusterCompName, - Members: configMembers, - } - client, err := NewLocalUnauthClient(ctx) - if err != nil { - mgr.Logger.Info("Get local unauth client failed", "error", err.Error()) - return err - } - defer client.Disconnect(context.TODO()) //nolint:errcheck - - configJSON, _ := json.Marshal(config) - mgr.Logger.Info(fmt.Sprintf("Initial Replset Config: %s", string(configJSON))) - response := client.Database("admin").RunCommand(ctx, bson.M{"replSetInitiate": config}) - if response.Err() != nil { - return response.Err() - } - return nil -} - -// IsClusterInitialized is a method to check if cluster is initialized or not -func (mgr *Manager) IsClusterInitialized(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - mgr.Logger.Info("Get leader client failed", "error", err) - return false, err - } - defer client.Disconnect(ctx) //nolint:errcheck - - ctx1, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) - defer cancel() - rsStatus, err := GetReplSetStatus(ctx1, client) - if rsStatus != nil { - return rsStatus.Set != "", nil - } - mgr.Logger.Info("Get replSet status failed", "error", err) - - if !mgr.IsFirstMember() { - return false, nil - } - - client, err = NewLocalUnauthClient(ctx) - if err != nil { - mgr.Logger.Info("Get local unauth client failed", "error", err) - return false, err - } - defer client.Disconnect(ctx) //nolint:errcheck - - rsStatus, err = GetReplSetStatus(ctx, client) - if rsStatus != nil { - return rsStatus.Set != "", nil - } - - err = errors.Cause(err) - if cmdErr, ok := err.(mongo.CommandError); ok && cmdErr.Name == "NotYetInitialized" { - return false, nil - } - mgr.Logger.Info("Get replSet status with local unauth client failed", "error", err) - - rsStatus, err = mgr.GetReplSetStatus(ctx) - if rsStatus != nil { - return rsStatus.Set != "", nil - } - if err != nil { - mgr.Logger.Info("Get replSet status with local auth client failed", "error", err) - return false, err - } - - mgr.Logger.Info("Get replSet status failed", "error", err) - return false, err -} - -func (mgr *Manager) IsRootCreated(ctx context.Context) (bool, error) { - if !mgr.IsFirstMember() { - return true, nil - } - - client, err := NewLocalUnauthClient(ctx) - if err != nil { - mgr.Logger.Info("Get local unauth client failed", "error", err) - return false, err - } - defer client.Disconnect(ctx) //nolint:errcheck - - _, err = GetReplSetStatus(ctx, client) - if err == nil { - return false, nil - } - err = errors.Cause(err) - if cmdErr, ok := err.(mongo.CommandError); ok && cmdErr.Name == "Unauthorized" { - return true, nil - } - - mgr.Logger.Info("Get replSet status with local unauth client failed", "error", err) - - _, err = mgr.GetReplSetStatus(ctx) - if err == nil { - return true, nil - } - - mgr.Logger.Info("Get replSet status with local auth client failed", "error", err) - return false, err - -} - -func (mgr *Manager) CreateRoot(ctx context.Context) error { - if !mgr.IsFirstMember() { - return nil - } - - client, err := NewLocalUnauthClient(ctx) - if err != nil { - mgr.Logger.Info("Get local unauth client failed", "error", err) - return err - } - defer client.Disconnect(ctx) //nolint:errcheck - - role := map[string]interface{}{ - "role": "root", - "db": "admin", - } - - mgr.Logger.Info(fmt.Sprintf("Create user: %s, passwd: %s, roles: %v", config.Username, config.Password, role)) - err = CreateUser(ctx, client, config.Username, config.Password, role) - if err != nil { - mgr.Logger.Info("Create Root failed", "error", err) - return err - } - - return nil -} - -func (mgr *Manager) IsRunning() bool { - // ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - // defer cancel() - - // err := mgr.Client.Ping(ctx, readpref.Nearest()) - // if err != nil { - // mgr.Logger.Infof("DB is not ready: %v", err) - // return false - // } - return true -} - -func (mgr *Manager) IsDBStartupReady() bool { - if mgr.DBStartupReady { - return true - } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - err := mgr.Client.Ping(ctx, readpref.Primary()) - if err != nil { - mgr.Logger.Info("DB is not ready", "error", err) - return false - } - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func (mgr *Manager) GetMemberState(ctx context.Context) (string, error) { - status, err := mgr.GetReplSetStatus(ctx) - if err != nil { - mgr.Logger.Info("rs.status() error", "error", err.Error()) - return "", err - } - - self := status.GetSelf() - if self == nil { - return "", nil - } - return strings.ToLower(self.StateStr), nil -} - -func (mgr *Manager) GetReplSetStatus(ctx context.Context) (*ReplSetStatus, error) { - return GetReplSetStatus(ctx, mgr.Client) -} - -func (mgr *Manager) IsLeaderMember(ctx context.Context, cluster *dcs.Cluster, dcsMember *dcs.Member) (bool, error) { - memberName := mgr.CurrentMemberName - memberIP := mgr.CurrentMemberIP - - if dcsMember != nil { - memberName = dcsMember.Name - memberIP = dcsMember.PodIP - } - - status, err := mgr.GetReplSetStatus(ctx) - if err != nil { - mgr.Logger.Info("rs.status() error", "error", err.Error()) - return false, err - } - for _, member := range status.Members { - if strings.HasPrefix(member.Name, memberName) || strings.HasPrefix(member.Name, memberIP) { - if member.StateStr == "PRIMARY" { - return true, nil - } - break - } - } - return false, nil -} - -func (mgr *Manager) IsLeader(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - return mgr.IsLeaderMember(ctx, cluster, nil) -} - -func (mgr *Manager) GetReplSetConfig(ctx context.Context) (*RSConfig, error) { - return GetReplSetConfig(ctx, mgr.Client) -} - -func (mgr *Manager) GetMemberAddrs(ctx context.Context, cluster *dcs.Cluster) []string { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - mgr.Logger.Info("Get replSet client failed", "error", err.Error()) - return nil - } - defer client.Disconnect(ctx) //nolint:errcheck - - rsConfig, err := GetReplSetConfig(ctx, client) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return nil - } - - return mgr.GetMemberAddrsFromRSConfig(rsConfig) -} - -func (mgr *Manager) GetMemberAddrsFromRSConfig(rsConfig *RSConfig) []string { - if rsConfig == nil { - return []string{} - } - - hosts := make([]string, len(rsConfig.Members)) - for i, member := range rsConfig.Members { - hosts[i] = member.Host - } - return hosts -} - -func (mgr *Manager) GetReplSetClient(ctx context.Context, cluster *dcs.Cluster) (*mongo.Client, error) { - hosts := cluster.GetMemberAddrs() - return NewReplSetClient(ctx, hosts) -} - -func (mgr *Manager) GetLeaderClient(ctx context.Context, cluster *dcs.Cluster) (*mongo.Client, error) { - if cluster.Leader == nil || cluster.Leader.Name == "" { - return nil, fmt.Errorf("cluster has no leader") - } - - leaderMember := cluster.GetMemberWithName(cluster.Leader.Name) - host := cluster.GetMemberAddrWithPort(*leaderMember) - return NewReplSetClient(context.TODO(), []string{host}) -} - -func (mgr *Manager) GetReplSetClientWithHosts(ctx context.Context, hosts []string) (*mongo.Client, error) { - if len(hosts) == 0 { - err := errors.New("Get replset client without hosts") - mgr.Logger.Info("Get replset client without hosts", "error", err.Error()) - return nil, err - } - - opts := options.Client(). - SetHosts(hosts). - SetReplicaSet(config.ReplSetName). - SetAuth(options.Credential{ - Password: config.Password, - Username: config.Username, - }). - SetWriteConcern(writeconcern.New(writeconcern.WMajority(), writeconcern.J(true))). - SetReadPreference(readpref.Primary()). - SetDirect(false) - - client, err := mongo.Connect(ctx, opts) - if err != nil { - return nil, errors.Wrap(err, "connect to mongodb") - } - return client, err -} - -func (mgr *Manager) IsCurrentMemberInCluster(ctx context.Context, cluster *dcs.Cluster) bool { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - mgr.Logger.Info("Get replSet client failed", "error", err.Error()) - return true - } - defer client.Disconnect(ctx) //nolint:errcheck - - rsConfig, err := GetReplSetConfig(ctx, client) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - // - return true - } - - for _, member := range rsConfig.Members { - if strings.HasPrefix(member.Host, mgr.CurrentMemberName) || strings.HasPrefix(member.Host, mgr.CurrentMemberIP) { - return true - } - } - - return false -} - -func (mgr *Manager) IsCurrentMemberHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - return mgr.IsMemberHealthy(ctx, cluster, nil) -} - -func (mgr *Manager) IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool { - var memberName string - if member != nil { - memberName = member.Name - } else { - memberName = mgr.CurrentMemberName - } - - rsStatus, err := mgr.GetReplSetStatus(ctx) - if err != nil { - mgr.Logger.Info("get replset status failed", "error", err.Error()) - return false - } - - if rsStatus == nil { - return false - } - - for _, member := range rsStatus.Members { - if (strings.HasPrefix(member.Name, memberName) || strings.HasPrefix(member.Name, mgr.CurrentMemberIP)) && - member.Health == 1 { - return true - } - } - return false -} - -func (mgr *Manager) Recover(ctx context.Context, cluster *dcs.Cluster) error { - if mgr.IsCurrentMemberInCluster(ctx, cluster) { - return nil - } - return mgr.UpdateCurrentMemberHost(ctx, cluster) -} - -func (mgr *Manager) UpdateCurrentMemberHost(ctx context.Context, cluster *dcs.Cluster) error { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - return err - } - defer client.Disconnect(ctx) //nolint:errcheck - - currentMember := cluster.GetMemberWithName(mgr.GetCurrentMemberName()) - currentHost := cluster.GetMemberAddrWithPort(*currentMember) - rsConfig, err := GetReplSetConfig(ctx, client) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return err - } - - var invalidMembers []*ConfigMember - for i, configMember := range rsConfig.Members { - host := configMember.Host - isInvalid := true - for _, member := range cluster.Members { - if strings.HasPrefix(host, member.Name) || strings.HasPrefix(host, member.PodIP) { - isInvalid = false - continue - } - } - if isInvalid { - invalidMembers = append(invalidMembers, &rsConfig.Members[i]) - } - } - if len(invalidMembers) > 1 { - return errors.Errorf("the replica set has more than one invalid members: %v", invalidMembers) - } - if len(invalidMembers) == 0 { - return nil - } - configMember := invalidMembers[0] - configMember.Host = currentHost - - rsConfig.Version++ - return SetReplSetConfig(ctx, client, rsConfig) -} - -func (mgr *Manager) JoinCurrentMemberToCluster(ctx context.Context, cluster *dcs.Cluster) error { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - return err - } - defer client.Disconnect(ctx) //nolint:errcheck - - currentMember := cluster.GetMemberWithName(mgr.GetCurrentMemberName()) - currentHost := cluster.GetMemberAddrWithPort(*currentMember) - rsConfig, err := GetReplSetConfig(ctx, client) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return err - } - - var lastID int - var configMember ConfigMember - for _, configMember = range rsConfig.Members { - if configMember.ID > lastID { - lastID = configMember.ID - } - } - configMember.ID = lastID + 1 - configMember.Host = currentHost - configMember.Priority = SecondaryPriority - rsConfig.Members = append(rsConfig.Members, configMember) - - rsConfig.Version++ - return SetReplSetConfig(ctx, client, rsConfig) -} - -func (mgr *Manager) LeaveMemberFromCluster(ctx context.Context, cluster *dcs.Cluster, memberName string) error { - client, err := mgr.GetLeaderClient(ctx, cluster) - if err != nil { - return err - } - defer client.Disconnect(ctx) //nolint:errcheck - - rsConfig, err := GetReplSetConfig(ctx, client) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return err - } - - mgr.Logger.Info(fmt.Sprintf("Delete member: %s", memberName)) - configMembers := make([]ConfigMember, 0, len(rsConfig.Members)-1) - isDeleted := true - for _, configMember := range rsConfig.Members { - if strings.HasPrefix(configMember.Host, memberName) || strings.HasPrefix(configMember.Host, mgr.CurrentMemberIP) { - isDeleted = false - continue - } - configMembers = append(configMembers, configMember) - } - if isDeleted { - mgr.Logger.Info("member is already deleted", "member", memberName) - return nil - } - - rsConfig.Members = configMembers - rsConfig.Version++ - return SetReplSetConfig(ctx, client, rsConfig) -} - -func (mgr *Manager) IsClusterHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - client, err := mgr.GetReplSetClient(ctx, cluster) - if err != nil { - mgr.Logger.Info("Get leader client failed", "error", err.Error()) - return false - } - defer client.Disconnect(ctx) //nolint:errcheck - - status, err := GetReplSetStatus(ctx, client) - if err != nil { - return false - } - isHeathly := status.OK != 0 - if !isHeathly { - statusJSON, _ := json.Marshal(status) - mgr.Logger.Info("cluster is unhealthy", "status", string(statusJSON)) - } - return isHeathly -} - -func (mgr *Manager) IsPromoted(ctx context.Context) bool { - isLeader, err := mgr.IsLeader(ctx, nil) - if err != nil { - mgr.Logger.Info("Is leader check failed", "error", err.Error()) - return false - } - - if !isLeader { - return false - } - - rsConfig, err := mgr.GetReplSetConfig(ctx) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return false - } - for i := range rsConfig.Members { - host := rsConfig.Members[i].Host - if strings.HasPrefix(host, mgr.CurrentMemberName) || strings.HasPrefix(host, mgr.CurrentMemberIP) { - if rsConfig.Members[i].Priority == PrimaryPriority { - return true - } - } - } - return false -} - -func (mgr *Manager) Promote(ctx context.Context, cluster *dcs.Cluster) error { - rsConfig, err := mgr.GetReplSetConfig(ctx) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return err - } - - for i := range rsConfig.Members { - host := rsConfig.Members[i].Host - if strings.HasPrefix(host, mgr.CurrentMemberName) || strings.HasPrefix(host, mgr.CurrentMemberIP) { - if rsConfig.Members[i].Priority == PrimaryPriority { - mgr.Logger.Info("Current member already has the highest priority!") - return nil - } - - rsConfig.Members[i].Priority = PrimaryPriority - } else if rsConfig.Members[i].Priority == PrimaryPriority { - rsConfig.Members[i].Priority = SecondaryPriority - } - } - - rsConfig.Version++ - - hosts := mgr.GetMemberAddrsFromRSConfig(rsConfig) - client, err := NewReplSetClient(ctx, hosts) - if err != nil { - return err - } - defer client.Disconnect(ctx) //nolint:errcheck - mgr.Logger.Info("reconfig replset", "config", rsConfig) - return SetReplSetConfig(ctx, client, rsConfig) -} - -func (mgr *Manager) Demote(context.Context) error { - // mongodb do premote and demote in one action, here do nothing. - return nil -} - -func (mgr *Manager) Follow(ctx context.Context, cluster *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) GetHealthiestMember(cluster *dcs.Cluster, candidate string) *dcs.Member { - rsStatus, _ := mgr.GetReplSetStatus(context.TODO()) - if rsStatus == nil { - return nil - } - healthyMembers := make([]string, 0, len(rsStatus.Members)) - var leader string - for _, member := range rsStatus.Members { - if member.Health == 1 { - m := cluster.GetMemberWithHost(member.Name) - if m == nil { - continue - } - memberName := m.Name - if memberName == candidate { - return m - } - healthyMembers = append(healthyMembers, memberName) - if member.State == 1 { - leader = memberName - } - } - } - - if candidate != "" { - mgr.Logger.Info("no health member for candidate", "candidate", candidate) - return nil - } - - if leader != "" { - return cluster.GetMemberWithName(leader) - } - - // TODO: use lag and other info to pick the healthiest member - r := rand.New(rand.NewSource(time.Now().UnixNano())) - healthiestMember := healthyMembers[r.Intn(len(healthyMembers))] - return cluster.GetMemberWithName(healthiestMember) - -} - -func (mgr *Manager) HasOtherHealthyLeader(ctx context.Context, cluster *dcs.Cluster) *dcs.Member { - rsStatus, _ := mgr.GetReplSetStatus(ctx) - if rsStatus == nil { - return nil - } - healthMembers := map[string]struct{}{} - var otherLeader string - for _, member := range rsStatus.Members { - memberName := member.Name - if member.State == 1 || member.State == 2 { - healthMembers[memberName] = struct{}{} - } - - if member.State != 1 { - continue - } - if !strings.HasPrefix(memberName, mgr.CurrentMemberName) && !strings.HasPrefix(memberName, mgr.CurrentMemberIP) { - otherLeader = memberName - } - } - if otherLeader != "" { - return cluster.GetMemberWithHost(otherLeader) - } - - rsConfig, err := mgr.GetReplSetConfig(ctx) - if rsConfig == nil { - mgr.Logger.Info("Get replSet config failed", "error", err.Error()) - return nil - } - - for _, mb := range rsConfig.Members { - memberName := mb.Host - if mb.Priority == PrimaryPriority && !strings.HasPrefix(memberName, mgr.CurrentMemberName) && !strings.HasPrefix(memberName, mgr.CurrentMemberIP) { - if _, ok := healthMembers[memberName]; ok { - otherLeader = memberName - } - } - } - - if otherLeader != "" { - return cluster.GetMemberWithHost(otherLeader) - } - - return nil -} - -// HasOtherHealthyMembers Are there any healthy members other than the leader? -func (mgr *Manager) HasOtherHealthyMembers(ctx context.Context, cluster *dcs.Cluster, leader string) []*dcs.Member { - members := make([]*dcs.Member, 0) - rsStatus, _ := mgr.GetReplSetStatus(ctx) - if rsStatus == nil { - return members - } - - for _, member := range rsStatus.Members { - if member == nil { - continue - } - if member.Health != 1 { - continue - } - m := cluster.GetMemberWithHost(member.Name) - if m == nil { - continue - } - memberName := m.Name - if memberName == leader { - continue - } - members = append(members, m) - } - - return members -} - -func (mgr *Manager) Lock(ctx context.Context, reason string) error { - mgr.Logger.Info(fmt.Sprintf("Lock db: %s", reason)) - m := bson.D{ - {Key: "fsync", Value: 1}, - {Key: "lock", Value: true}, - {Key: "comment", Value: reason}, - } - lockResp := LockResp{} - - response := mgr.Client.Database("admin").RunCommand(ctx, m) - if response.Err() != nil { - mgr.Logger.Info(fmt.Sprintf("Lock db (%s) failed", reason), "error", response.Err().Error()) - return response.Err() - } - if err := response.Decode(&lockResp); err != nil { - err := errors.Wrap(err, "failed to decode lock response") - return err - } - - if lockResp.OK != 1 { - err := errors.Errorf("mongo says: %s", lockResp.Errmsg) - return err - } - mgr.IsLocked = true - mgr.Logger.Info(fmt.Sprintf("Lock db success times: %d", lockResp.LockCount)) - return nil -} - -func (mgr *Manager) Unlock(ctx context.Context) error { - mgr.Logger.Info("Unlock db") - m := bson.M{"fsyncUnlock": 1} - unlockResp := LockResp{} - response := mgr.Client.Database("admin").RunCommand(ctx, m) - if response.Err() != nil { - mgr.Logger.Info("Unlock db failed", "error", response.Err().Error()) - return response.Err() - } - if err := response.Decode(&unlockResp); err != nil { - err := errors.Wrap(err, "failed to decode unlock response") - return err - } - - if unlockResp.OK != 1 { - err := errors.Errorf("mongo says: %s", unlockResp.Errmsg) - return err - } - for unlockResp.LockCount > 0 { - response = mgr.Client.Database("admin").RunCommand(ctx, m) - if response.Err() != nil { - mgr.Logger.Info("Unlock db failed", "error", response.Err().Error()) - return response.Err() - } - if err := response.Decode(&unlockResp); err != nil { - err := errors.Wrap(err, "failed to decode unlock response") - return err - } - } - mgr.IsLocked = false - mgr.Logger.Info("Unlock db success") - return nil -} diff --git a/pkg/lorry/engines/mongodb/replset.go b/pkg/lorry/engines/mongodb/replset.go deleted file mode 100644 index b4f247bfccf..00000000000 --- a/pkg/lorry/engines/mongodb/replset.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -func GetReplSetStatus(ctx context.Context, client *mongo.Client) (*ReplSetStatus, error) { - status := &ReplSetStatus{} - - resp := client.Database("admin").RunCommand(ctx, bson.D{{Key: "replSetGetStatus", Value: 1}}) - if resp.Err() != nil { - err := errors.Wrap(resp.Err(), "replSetGetStatus") - return nil, err - } - - if err := resp.Decode(status); err != nil { - err := errors.Wrap(err, "failed to decode rs status") - return nil, err - } - - if status.OK != 1 { - err := errors.Errorf("mongo says: %s", status.Errmsg) - return nil, err - } - - return status, nil -} - -func SetReplSetConfig(ctx context.Context, rsClient *mongo.Client, cfg *RSConfig) error { - resp := OKResponse{} - - res := rsClient.Database("admin").RunCommand(ctx, bson.D{{Key: "replSetReconfig", Value: cfg}}) - if res.Err() != nil { - err := errors.Wrap(res.Err(), "replSetReconfig") - return err - } - - if err := res.Decode(&resp); err != nil { - err = errors.Wrap(err, "failed to decode to replSetReconfigResponse") - return err - } - - if resp.OK != 1 { - err := errors.Errorf("mongo says: %s", resp.Errmsg) - return err - } - - return nil -} - -func GetReplSetConfig(ctx context.Context, client *mongo.Client) (*RSConfig, error) { - resp := ReplSetGetConfig{} - res := client.Database("admin").RunCommand(ctx, bson.D{{Key: "replSetGetConfig", Value: 1}}) - if res.Err() != nil { - err := errors.Wrap(res.Err(), "replSetGetConfig") - return nil, err - } - if err := res.Decode(&resp); err != nil { - err := errors.Wrap(err, "failed to decode to replSetGetConfig") - return nil, err - } - - if resp.Config == nil { - err := errors.Errorf("mongo says: %s", resp.Errmsg) - return nil, err - } - - return resp.Config, nil -} diff --git a/pkg/lorry/engines/mongodb/roles.go b/pkg/lorry/engines/mongodb/roles.go deleted file mode 100644 index 974a651b86d..00000000000 --- a/pkg/lorry/engines/mongodb/roles.go +++ /dev/null @@ -1,124 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -func CreateRole(ctx context.Context, client *mongo.Client, role string, privileges []RolePrivilege, roles []interface{}) error { - resp := OKResponse{} - - privilegesArr := bson.A{} - for _, p := range privileges { - privilegesArr = append(privilegesArr, p) - } - - rolesArr := bson.A{} - for _, r := range roles { - rolesArr = append(rolesArr, r) - } - - m := bson.D{ - {Key: "createRole", Value: role}, - {Key: "privileges", Value: privilegesArr}, - {Key: "roles", Value: rolesArr}, - } - - res := client.Database("admin").RunCommand(ctx, m) - if res.Err() != nil { - return errors.Wrap(res.Err(), "failed to create role") - } - - err := res.Decode(&resp) - if err != nil { - return errors.Wrap(err, "failed to decode response") - } - - if resp.OK != 1 { - return errors.Errorf("mongo says: %s", resp.Errmsg) - } - - return nil -} - -func UpdateRole(ctx context.Context, client *mongo.Client, role string, privileges []RolePrivilege, roles []interface{}) error { - resp := OKResponse{} - - privilegesArr := bson.A{} - for _, p := range privileges { - privilegesArr = append(privilegesArr, p) - } - - rolesArr := bson.A{} - for _, r := range roles { - rolesArr = append(rolesArr, r) - } - - m := bson.D{ - {Key: "updateRole", Value: role}, - {Key: "privileges", Value: privilegesArr}, - {Key: "roles", Value: rolesArr}, - } - - res := client.Database("admin").RunCommand(ctx, m) - if res.Err() != nil { - return errors.Wrap(res.Err(), "failed to create role") - } - - err := res.Decode(&resp) - if err != nil { - return errors.Wrap(err, "failed to decode response") - } - - if resp.OK != 1 { - return errors.Errorf("mongo says: %s", resp.Errmsg) - } - - return nil -} - -func GetRole(ctx context.Context, client *mongo.Client, role string) (*Role, error) { - resp := RoleInfo{} - - res := client.Database("admin").RunCommand(ctx, bson.D{ - {Key: "rolesInfo", Value: role}, - {Key: "showPrivileges", Value: true}, - }) - if res.Err() != nil { - return nil, errors.Wrap(res.Err(), "run command") - } - - err := res.Decode(&resp) - if err != nil { - return nil, errors.Wrap(err, "failed to decode response") - } - if resp.OK != 1 { - return nil, errors.Errorf("mongo says: %s", resp.Errmsg) - } - if len(resp.Roles) == 0 { - return nil, nil - } - return &resp.Roles[0], nil -} diff --git a/pkg/lorry/engines/mongodb/types.go b/pkg/lorry/engines/mongodb/types.go deleted file mode 100644 index 0c5c2e84e1d..00000000000 --- a/pkg/lorry/engines/mongodb/types.go +++ /dev/null @@ -1,292 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "time" - - "go.mongodb.org/mongo-driver/bson/primitive" -) - -const ( - MinVotingMembers = 1 - MaxVotingMembers = 7 - MaxMembers = 50 - DefaultPriority = 2 - DefaultVotes = 1 - DefaultReadConcern = "majority" - DefaultWriteConcern = "majority" -) - -// ReplsetTags Set tags: https://docs.mongodb.com/manual/tutorial/configure-replica-set-tag-sets/#add-tag-sets-to-a-replica-set -type ReplsetTags map[string]string - -// ConfigMember document from 'replSetGetConfig': https://docs.mongodb.com/manual/reference/command/replSetGetConfig/#dbcmd.replSetGetConfig -type ConfigMember struct { - ID int `bson:"_id" json:"_id"` - Host string `bson:"host" json:"host"` - ArbiterOnly *bool `bson:"arbiterOnly,omitempty" json:"arbiterOnly,omitempty"` - BuildIndexes *bool `bson:"buildIndexes,omitempty" json:"buildIndexes,omitempty"` - Hidden *bool `bson:"hidden,omitempty" json:"hidden,omitempty"` - Priority int `bson:"priority,omitempty" json:"priority,omitempty"` - Tags ReplsetTags `bson:"tags,omitempty" json:"tags,omitempty"` - SlaveDelay *int64 `bson:"slaveDelay,omitempty" json:"slaveDelay,omitempty"` - SecondaryDelaySecs *int64 `bson:"secondaryDelaySecs,omitempty" json:"secondaryDelaySecs,omitempty"` - Votes *int `bson:"votes,omitempty" json:"votes,omitempty"` -} - -type ConfigMembers []ConfigMember - -type RSConfig struct { - ID string `bson:"_id,omitempty" json:"_id,omitempty"` - Version int `bson:"version,omitempty" json:"version,omitempty"` - Members ConfigMembers `bson:"members" json:"members"` - Configsvr bool `bson:"configsvr,omitempty" json:"configsvr,omitempty"` - ProtocolVersion int `bson:"protocolVersion,omitempty" json:"protocolVersion,omitempty"` - Settings Settings `bson:"-" json:"settings,omitempty"` - WriteConcernMajorityJournalDefault bool `bson:"writeConcernMajorityJournalDefault,omitempty" json:"writeConcernMajorityJournalDefault,omitempty"` -} - -// Settings document from 'replSetGetConfig': https://docs.mongodb.com/manual/reference/command/replSetGetConfig/#dbcmd.replSetGetConfig -type Settings struct { - ChainingAllowed bool `bson:"chainingAllowed,omitempty" json:"chainingAllowed,omitempty"` - HeartbeatIntervalMillis int64 `bson:"heartbeatIntervalMillis,omitempty" json:"heartbeatIntervalMillis,omitempty"` - HeartbeatTimeoutSecs int `bson:"heartbeatTimeoutSecs,omitempty" json:"heartbeatTimeoutSecs,omitempty"` - ElectionTimeoutMillis int64 `bson:"electionTimeoutMillis,omitempty" json:"electionTimeoutMillis,omitempty"` - CatchUpTimeoutMillis int64 `bson:"catchUpTimeoutMillis,omitempty" json:"catchUpTimeoutMillis,omitempty"` - GetLastErrorModes map[string]ReplsetTags `bson:"getLastErrorModes,omitempty" json:"getLastErrorModes,omitempty"` - GetLastErrorDefaults WriteConcern `bson:"getLastErrorDefaults,omitempty" json:"getLastErrorDefaults,omitempty"` - ReplicaSetID primitive.ObjectID `bson:"replicaSetId,omitempty" json:"replicaSetId,omitempty"` -} - -// ReplSetGetConfig Response document from 'replSetGetConfig': https://docs.mongodb.com/manual/reference/command/replSetGetConfig/#dbcmd.replSetGetConfig -type ReplSetGetConfig struct { - Config *RSConfig `bson:"config" json:"config"` - OKResponse `bson:",inline"` -} - -// BuildInfo contains information about mongod build params -type BuildInfo struct { - Version string `json:"version" bson:"version"` - OKResponse `bson:",inline"` -} - -// OKResponse is a standard MongoDB response -type OKResponse struct { - Errmsg string `bson:"errmsg,omitempty" json:"errmsg,omitempty"` - OK int `bson:"ok" json:"ok"` -} - -// WriteConcern document: https://docs.mongodb.com/manual/reference/write-concern/ -type WriteConcern struct { - WriteConcern interface{} `bson:"w" json:"w"` - WriteTimeout int `bson:"wtimeout" json:"wtimeout"` - Journal bool `bson:"j,omitempty" json:"j,omitempty"` -} - -type BalancerStatus struct { - Mode string `json:"mode"` - OKResponse `bson:",inline"` -} - -type LockResp struct { - Info string `bson:"info" json:"info"` - LockCount int64 `bson:"lockCount" json:"lockCount"` - OKResponse `bson:",inline"` -} - -type DBList struct { - DBs []struct { - Name string `bson:"name" json:"name"` - } `bson:"databases" json:"databases"` - OKResponse `bson:",inline"` -} - -type ShardList struct { - Shards []struct { - ID string `json:"_id" bson:"_id"` - Host string `json:"host" bson:"host"` - State int `json:"state" bson:"state"` - } `json:"shards" bson:"shards"` - OKResponse `bson:",inline"` -} - -type FCV struct { - FCV struct { - Version string `json:"version" bson:"version"` - } `json:"featureCompatibilityVersion" bson:"featureCompatibilityVersion"` - OKResponse `bson:",inline"` -} - -const ShardRemoveCompleted string = "completed" - -type ShardRemoveResp struct { - Msg string `json:"msg" bson:"msg"` - State string `json:"state" bson:"state"` - Remaining struct { - Chunks int `json:"chunks" bson:"chunks"` - JumboChunks int `json:"jumboChunks" bson:"jumboChunks"` - } `json:"remaining" bson:"remaining"` - OKResponse `bson:",inline"` -} - -type IsMasterResp struct { - IsMaster bool `bson:"ismaster" json:"ismaster"` - IsArbiter bool `bson:"arbiterOnly" json:"arbiterOnly"` - Primary string `bson:"primary" json:"primary"` - Me string `bson:"me" json:"me"` - Msg string `bson:"msg" json:"msg"` - OKResponse `bson:",inline"` -} - -type ReplSetStatus struct { - Set string `bson:"set" json:"set"` - Date time.Time `bson:"date" json:"date"` - MyState MemberState `bson:"myState" json:"myState"` - Members []*Member `bson:"members" json:"members"` - Term int64 `bson:"term,omitempty" json:"term,omitempty"` - HeartbeatIntervalMillis int64 `bson:"heartbeatIntervalMillis,omitempty" json:"heartbeatIntervalMillis,omitempty"` - Optimes *StatusOptimes `bson:"optimes,omitempty" json:"optimes,omitempty"` - OKResponse `bson:",inline"` -} - -type Member struct { - ID int `bson:"_id" json:"_id"` - Name string `bson:"name" json:"name"` - Health MemberHealth `bson:"health" json:"health"` - State MemberState `bson:"state" json:"state"` - StateStr string `bson:"stateStr" json:"stateStr"` - Uptime int64 `bson:"uptime" json:"uptime"` - Optime *Optime `bson:"optime" json:"optime"` - OptimeDate time.Time `bson:"optimeDate" json:"optimeDate"` - ConfigVersion int `bson:"configVersion" json:"configVersion"` - ElectionTime primitive.Timestamp `bson:"electionTime,omitempty" json:"electionTime,omitempty"` - ElectionDate time.Time `bson:"electionDate,omitempty" json:"electionDate,omitempty"` - InfoMessage string `bson:"infoMessage,omitempty" json:"infoMessage,omitempty"` - OptimeDurable *Optime `bson:"optimeDurable,omitempty" json:"optimeDurable,omitempty"` - OptimeDurableDate time.Time `bson:"optimeDurableDate,omitempty" json:"optimeDurableDate,omitempty"` - LastHeartbeat time.Time `bson:"lastHeartbeat,omitempty" json:"lastHeartbeat,omitempty"` - LastHeartbeatRecv time.Time `bson:"lastHeartbeatRecv,omitempty" json:"lastHeartbeatRecv,omitempty"` - PingMs int64 `bson:"pingMs,omitempty" json:"pingMs,omitempty"` - Self bool `bson:"self,omitempty" json:"self,omitempty"` - SyncingTo string `bson:"syncingTo,omitempty" json:"syncingTo,omitempty"` -} - -func (s *ReplSetStatus) GetSelf() *Member { - for _, member := range s.Members { - if member.Self { - return member - } - } - return nil -} - -type Optime struct { - Timestamp primitive.Timestamp `bson:"ts" json:"ts"` - Term int64 `bson:"t" json:"t"` -} - -type StatusOptimes struct { - LastCommittedOpTime *Optime `bson:"lastCommittedOpTime" json:"lastCommittedOpTime"` - AppliedOpTime *Optime `bson:"appliedOpTime" json:"appliedOpTime"` - DurableOptime *Optime `bson:"durableOpTime" json:"durableOpTime"` -} - -type MemberHealth int -type MemberState int - -const ( - MemberHealthDown MemberHealth = iota - MemberHealthUp - MemberStateStartup MemberState = 0 - MemberStatePrimary MemberState = 1 - MemberStateSecondary MemberState = 2 - MemberStateRecovering MemberState = 3 - MemberStateStartup2 MemberState = 5 - MemberStateUnknown MemberState = 6 - MemberStateArbiter MemberState = 7 - MemberStateDown MemberState = 8 - MemberStateRollback MemberState = 9 - MemberStateRemoved MemberState = 10 -) - -var MemberStateStrings = map[MemberState]string{ - MemberStateStartup: "STARTUP", - MemberStatePrimary: "PRIMARY", - MemberStateSecondary: "SECONDARY", - MemberStateRecovering: "RECOVERING", - MemberStateStartup2: "STARTUP2", - MemberStateUnknown: "UNKNOWN", - MemberStateArbiter: "ARBITER", - MemberStateDown: "DOWN", - MemberStateRollback: "ROLLBACK", - MemberStateRemoved: "REMOVED", -} - -func (s *ReplSetStatus) GetMembersByState(state MemberState, limit int) []*Member { - members := make([]*Member, 0) - for _, member := range s.Members { - if member.State == state { - members = append(members, member) - if limit > 0 && len(members) == limit { - return members - } - } - } - return members -} - -func (s *ReplSetStatus) Primary() *Member { - primary := s.GetMembersByState(MemberStatePrimary, 1) - if len(primary) == 1 { - return primary[0] - } - return nil -} - -type RolePrivilege struct { - Resource map[string]interface{} `bson:"resource" json:"resource"` - Actions []string `bson:"actions" json:"actions"` -} - -type Role struct { - Role string `bson:"role" json:"role"` - DB string `bson:"db" json:"db"` - IsBuiltin string `bson:"isBuiltin" json:"isBuiltin"` - Roles []map[string]interface{} `bson:"roles" json:"roles"` - Privileges []RolePrivilege `bson:"privileges" json:"privileges"` -} - -type RoleInfo struct { - Roles []Role `bson:"roles" json:"roles"` - OKResponse `bson:",inline"` -} - -type User struct { - ID string `bson:"_id" json:"_id"` - User string `bson:"user" json:"user"` - DB string `bson:"db" json:"db"` - Roles []map[string]interface{} `bson:"roles" json:"roles"` -} - -type UsersInfo struct { - Users []User `bson:"users" json:"users"` - OKResponse `bson:",inline"` -} diff --git a/pkg/lorry/engines/mongodb/users.go b/pkg/lorry/engines/mongodb/users.go deleted file mode 100644 index 4dc4d45cb90..00000000000 --- a/pkg/lorry/engines/mongodb/users.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mongodb - -import ( - "context" - - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -func CreateUser(ctx context.Context, client *mongo.Client, user, pwd string, roles ...map[string]interface{}) error { - resp := OKResponse{} - - res := client.Database("admin").RunCommand(ctx, bson.D{ - {Key: "createUser", Value: user}, - {Key: "pwd", Value: pwd}, - {Key: "roles", Value: roles}, - }) - if res.Err() != nil { - return errors.Wrap(res.Err(), "failed to create user") - } - - err := res.Decode(&resp) - if err != nil { - return errors.Wrap(err, "failed to decode response") - } - - if resp.OK != 1 { - return errors.Errorf("mongo says: %s", resp.Errmsg) - } - - return nil -} - -func GetUser(ctx context.Context, client *mongo.Client, userName string) (*User, error) { - resp := UsersInfo{} - res := client.Database("admin").RunCommand(ctx, bson.D{{Key: "usersInfo", Value: userName}}) - if res.Err() != nil { - return nil, errors.Wrap(res.Err(), "run command") - } - - err := res.Decode(&resp) - if err != nil { - return nil, errors.Wrap(err, "failed to decode response") - } - if resp.OK != 1 { - return nil, errors.Errorf("mongo says: %s", resp.Errmsg) - } - if len(resp.Users) == 0 { - return nil, nil - } - return &resp.Users[0], nil -} - -func UpdateUserRoles(ctx context.Context, client *mongo.Client, userName string, roles []map[string]interface{}) error { - return client.Database("admin").RunCommand(ctx, bson.D{{Key: "updateUser", Value: userName}, {Key: "roles", Value: roles}}).Err() -} - -// UpdateUserPass updates user's password -func UpdateUserPass(ctx context.Context, client *mongo.Client, name, pass string) error { - return client.Database("admin").RunCommand(ctx, bson.D{{Key: "updateUser", Value: name}, {Key: "pwd", Value: pass}}).Err() -} - -// DropUser delete user -func DropUser(ctx context.Context, client *mongo.Client, userName string) error { - user, err := GetUser(ctx, client, userName) - if err != nil { - return errors.Wrap(err, "get user") - } - - if user == nil { - return errors.New(userName + " user not exists") - } - - err = client.Database("admin").RunCommand(ctx, bson.D{{Key: "dropUser", Value: userName}}).Err() - return errors.Wrap(err, "drop user") -} diff --git a/pkg/lorry/engines/mysql/commands.go b/pkg/lorry/engines/mysql/commands.go deleted file mode 100644 index 91345df16a9..00000000000 --- a/pkg/lorry/engines/mysql/commands.go +++ /dev/null @@ -1,302 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "fmt" - "strconv" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "mysql", - PasswordEnv: "$MYSQL_ROOT_PASSWORD", - UserEnv: "$MYSQL_ROOT_USER", - Database: "mysql", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# mysql client connection example -mysql -h %s -P %s -u %s -p%s -`, info.Host, info.Port, info.User, info.Password) - }, - - models.DJANGO: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DB_HOST=%s -DB_NAME=%s -DB_USER=%s -DB_PASSWORD=%s -DB_PORT=%s - -# settings.py -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DB_NAME'), - 'HOST': os.environ.get('DB_HOST'), - 'PORT': os.environ.get('DB_PORT'), - 'USER': os.environ.get('DB_USER'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - } -} -`, info.Host, info.Database, info.User, info.Password, info.Port) - }, - - models.DOTNET: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# appsettings.json -{ - "ConnectionStrings": { - "Default": "server=%s;port=%s;database=%s;user=%s;password=%s;SslMode=VerifyFull;" - }, -} - -# Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - services.AddTransient(_ => new MySqlConnection(Configuration["ConnectionStrings:Default"])); -} -`, info.Host, info.Port, info.Database, info.User, info.Password) - }, - - models.GO: func(info *engines.ConnectionInfo) string { - const goConnectExample = `# main.go -package main - -import ( - "database/sql" - "log" - "os" - - _ "github.com/go-sql-driver/mysql" -) - -func main() { - db, err := sql.Open("mysql", os.Getenv("DSN")) - if err != nil { - log.Fatalf("failed to connect: %v", err) - } - defer db.Close() - - if err := db.Ping(); err != nil { - log.Fatalf("failed to ping: %v", err) - } - - log.Println("Successfully connected!") -} -` - dsn := fmt.Sprintf(`# .env -DSN=%s:%s@tcp(%s:%s)/%s?tls=true -`, info.User, info.Password, info.Host, info.Port, info.Database) - return fmt.Sprintf("%s\n%s", dsn, goConnectExample) - }, - - models.JAVA: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`Class.forName("com.mysql.cj.jdbc.Driver"); -Connection conn = DriverManager.getConnection( - "jdbc:mysql://%s:%s/%s?sslMode=VERIFY_IDENTITY", - "%s", - "%s"); -`, info.Host, info.Port, info.Database, info.User, info.Password) - }, - - models.NODEJS: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='mysql://%s:%s@%s:%s/%s?ssl={"rejectUnauthorized":true}' - -# app.js -require('dotenv').config(); -const mysql = require('mysql2'); -const connection = mysql.createConnection(process.env.DATABASE_URL); -connection.end(); -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.PHP: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -HOST=%s -PORT=%s -USERNAME=%s -PASSWORD=%s -DATABASE=%s - -# index.php -real_connect($_ENV["HOST"], $_ENV["USERNAME"], $_ENV["PASSWORD"], $_ENV["DATABASE"], $_ENV["PORT"]); - $mysqli->close(); -?> -`, info.Host, info.Port, info.User, info.Password, info.Database) - }, - - models.PRISMA: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='mysql://%s:%s@%s:%s/%s?sslaccept=strict' - -# schema.prisma -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "mysql" - url = env("DATABASE_URL") - relationMode = "prisma" -} -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.PYTHON: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# run the following command in the terminal to install dependencies -pip install python-dotenv mysqlclient - -# .env -HOST=%s -PORT=%s -USERNAME=%s -PASSWORD=%s -DATABASE=%s - -# main.py -from dotenv import load_dotenv -load_dotenv() -import os -import MySQLdb - -connection = MySQLdb.connect( - host= os.getenv("HOST"), - port=os.getenv("PORT"), - user=os.getenv("USERNAME"), - passwd=os.getenv("PASSWORD"), - db=os.getenv("DATABASE"), - ssl_mode = "VERIFY_IDENTITY", -) -`, info.Host, info.Port, info.User, info.Password, info.Database) - }, - - models.RAILS: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# Gemfile -gem 'mysql2' - -# config/database.yml -development: - <<: *default - adapter: mysql2 - database: %s - username: %s - host: %s - password: %s - ssl_mode: verify_identity -`, info.Database, info.User, info.Host, info.Password) - }, - - models.RUST: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# run the following command in the terminal -export DATABASE_URL="mysql://%s:%s@%s:%s/%s" - -# src/main.rs -use std::env; - -fn main() { - let url = env::var("DATABASE_URL").expect("DATABASE_URL not found"); - let builder = mysql::OptsBuilder::from_opts(mysql::Opts::from_url(&url).unwrap()); - let pool = mysql::Pool::new(builder.ssl_opts(mysql::SslOpts::default())).unwrap(); - let _conn = pool.get_conn().unwrap(); - println!("Successfully connected!"); -} - -# Cargo.toml -[package] -name = "kubeblocks_hello_world" -version = "0.0.1" - -[dependencies] -mysql = "*" -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.SYMFONY: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='mysql://%s:%s@%s:%s/%s' -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - }, - } -} - -func (m *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := m.info.UserEnv - userPass := m.info.PasswordEnv - - if connectInfo != nil { - userName = engines.AddSingleQuote(connectInfo.UserName) - userPass = engines.AddSingleQuote(connectInfo.UserPasswd) - } - - // avoid using env variables - // MYSQL_PWD is deprecated as of MySQL 8.0; expect it to be removed in a future version of MySQL. - // ref to mysql manual for more details. - // https://dev.mysql.com/doc/refman/8.0/en/environment-variables.html - mysqlCmd := []string{fmt.Sprintf("%s -u%s -p%s", m.info.Client, userName, userPass)} - - return []string{"sh", "-c", strings.Join(mysqlCmd, " ")} -} - -func (m *Commands) Container() string { - return m.info.Container -} - -func (m *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - if len(info.Database) == 0 { - info.Database = m.info.Database - } - return engines.BuildExample(info, client, m.examples) -} - -func (m *Commands) ExecuteCommand(scripts []string) ([]string, []corev1.EnvVar, error) { - cmd := []string{} - cmd = append(cmd, "/bin/sh", "-c", "-ex") - cmd = append(cmd, fmt.Sprintf("%s -u%s -p%s -e %s", m.info.Client, - fmt.Sprintf("$%s", engines.EnvVarMap[engines.USER]), - fmt.Sprintf("$%s", engines.EnvVarMap[engines.PASSWORD]), - strconv.Quote(strings.Join(scripts, " ")))) - - envs := []corev1.EnvVar{ - { - Name: "MYSQL_HOST", - Value: fmt.Sprintf("$(%s)", engines.EnvVarMap[engines.HOST]), - }, - } - return cmd, envs, nil -} diff --git a/pkg/lorry/engines/mysql/commands_test.go b/pkg/lorry/engines/mysql/commands_test.go deleted file mode 100644 index dead8d05b14..00000000000 --- a/pkg/lorry/engines/mysql/commands_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Mysql Engine", func() { - It("connection command", func() { - mysql := NewCommands() - - Expect(mysql.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(mysql.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - mysql := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Database: "test-db", - Port: "1234", - } - for k := range mysql.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(mysql.ConnectExample(info, k.String())).ShouldNot(BeEmpty()) - } - - Expect(mysql.ConnectExample(info, "")).ShouldNot(BeEmpty()) - }) -}) diff --git a/pkg/lorry/engines/mysql/config.go b/pkg/lorry/engines/mysql/config.go deleted file mode 100644 index 27c6a605af4..00000000000 --- a/pkg/lorry/engines/mysql/config.go +++ /dev/null @@ -1,212 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "crypto/tls" - "crypto/x509" - "database/sql" - "fmt" - "net" - "strconv" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" -) - -const ( - // configurations to connect to MySQL, either a data source name represent by URL. - connectionURLKey = "url" - - // To connect to MySQL running over SSL you have to download a - // SSL certificate. If this is provided the driver will connect using - // SSL. If you have disabled SSL you can leave this empty. - // When the user provides a pem path their connection string must end with - // &tls=custom - // The connection string should be in the following format - // "%s:%s@tcp(%s:3306)/%s?allowNativePasswords=true&tls=custom",'myadmin@mydemoserver', 'yourpassword', 'mydemoserver.mysql.database.azure.com', 'targetdb'. - pemPathKey = "pemPath" - - // other general settings for DB connections. - maxIdleConnsKey = "maxIdleConns" - maxOpenConnsKey = "maxOpenConns" - connMaxLifetimeKey = "connMaxLifetime" - connMaxIdleTimeKey = "connMaxIdleTime" -) - -const ( - adminDatabase = "mysql" - defaultDBPort = 3306 -) - -type Config struct { - URL string - port string - Username string - Password string - pemPath string - maxIdleConns int - maxOpenConns int - connMaxLifetime time.Duration - connMaxIdletime time.Duration -} - -var fs = afero.NewOsFs() - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - config = &Config{} - - if val, ok := properties[connectionURLKey]; ok && val != "" { - config.URL = val - } else { - config.URL = "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true" - } - - if viper.IsSet(constant.KBEnvServiceUser) { - config.Username = viper.GetString(constant.KBEnvServiceUser) - } else if username, ok := properties["username"]; ok { - config.Username = username - } - - if viper.IsSet(constant.KBEnvServicePassword) { - config.Password = viper.GetString(constant.KBEnvServicePassword) - } - - if viper.IsSet(constant.KBEnvServicePort) { - config.port = viper.GetString(constant.KBEnvServicePort) - } - - if val, ok := properties[pemPathKey]; ok { - config.pemPath = val - } - - if val, ok := properties[maxIdleConnsKey]; ok { - if i, err := strconv.Atoi(val); err == nil { - config.maxIdleConns = i - } - } - - if val, ok := properties[maxOpenConnsKey]; ok { - if i, err := strconv.Atoi(val); err == nil { - config.maxOpenConns = i - } - } - - if val, ok := properties[connMaxLifetimeKey]; ok { - if d, err := time.ParseDuration(val); err == nil { - config.connMaxLifetime = d - } - } - - if val, ok := properties[connMaxIdleTimeKey]; ok { - if d, err := time.ParseDuration(val); err == nil { - config.connMaxIdletime = d - } - } - - if config.pemPath != "" { - rootCertPool := x509.NewCertPool() - pem, err := afero.ReadFile(fs, config.pemPath) - if err != nil { - return nil, errors.Wrapf(err, "Error reading PEM file from %s", config.pemPath) - } - - ok := rootCertPool.AppendCertsFromPEM(pem) - if !ok { - return nil, fmt.Errorf("failed to append PEM") - } - - err = mysql.RegisterTLSConfig("custom", &tls.Config{RootCAs: rootCertPool, MinVersion: tls.VersionTLS12}) - if err != nil { - return nil, errors.Wrap(err, "Error register TLS config") - } - } - return config, nil -} - -func (config *Config) GetLocalDBConn() (*sql.DB, error) { - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return nil, errors.Wrapf(err, "illegal Data Source Name (DNS) specified by %s", connectionURLKey) - } - mysqlConfig.User = config.Username - mysqlConfig.Passwd = config.Password - mysqlConfig.Timeout = time.Second * 5 - mysqlConfig.ReadTimeout = time.Second * 5 - mysqlConfig.WriteTimeout = time.Second * 5 - if config.port != "" { - mysqlConfig.Addr = "127.0.0.1:" + config.port - } - db, err := GetDBConnection(mysqlConfig.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "get DB connection failed") - } - - return db, nil -} - -func (config *Config) GetDBConnWithAddr(addr string) (*sql.DB, error) { - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return nil, errors.Wrapf(err, "illegal Data Source Name (DNS) specified by %s", connectionURLKey) - } - mysqlConfig.User = config.Username - mysqlConfig.Passwd = config.Password - mysqlConfig.Timeout = time.Second * 5 - mysqlConfig.ReadTimeout = time.Second * 5 - mysqlConfig.WriteTimeout = time.Second * 5 - mysqlConfig.Addr = addr - db, err := GetDBConnection(mysqlConfig.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "get DB connection failed") - } - - return db, nil -} - -func (config *Config) GetDBPort() int { - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return defaultDBPort - } - - _, portStr, err := net.SplitHostPort(mysqlConfig.Addr) - if err != nil { - return defaultDBPort - } - - port, err := strconv.Atoi(portStr) - if err != nil { - return defaultDBPort - } - - return port -} - -func GetConfig() *Config { - return config -} diff --git a/pkg/lorry/engines/mysql/config_test.go b/pkg/lorry/engines/mysql/config_test.go deleted file mode 100644 index b2bea02d7f5..00000000000 --- a/pkg/lorry/engines/mysql/config_test.go +++ /dev/null @@ -1,165 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "testing" - "time" - - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -const ( - fakeUser = "fake-user" - fakePassword = "fake-password" - fakePemPath = "fake-pem-path" - fakeAddr = "fake-addr" -) - -var ( - fakeProperties = engines.Properties{ - connectionURLKey: "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true", - maxOpenConnsKey: "5", - maxIdleConnsKey: "4", - connMaxLifetimeKey: "10m", - connMaxIdleTimeKey: "500s", - } - - fakePropertiesWithPem = engines.Properties{ - pemPathKey: fakePemPath, - } - - fakePropertiesWithWrongURL = engines.Properties{ - connectionURLKey: "fake-url", - } -) - -func TestNewConfig(t *testing.T) { - fs = afero.NewMemMapFs() - defer func() { - fs = afero.NewOsFs() - viper.Reset() - }() - - t.Run("with empty properties", func(t *testing.T) { - fakeConfig, err := NewConfig(map[string]string{}) - assert.Nil(t, err) - assert.NotNil(t, fakeConfig) - assert.Equal(t, "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true", fakeConfig.URL) - }) - - t.Run("with default properties", func(t *testing.T) { - viper.Set(constant.KBEnvServiceUser, fakeUser) - viper.Set(constant.KBEnvServicePassword, fakePassword) - - fakeConfig, err := NewConfig(fakeProperties) - assert.Nil(t, err) - assert.NotNil(t, fakeConfig) - assert.Equal(t, "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true", fakeConfig.URL) - assert.Equal(t, 5, fakeConfig.maxOpenConns) - assert.Equal(t, 4, fakeConfig.maxIdleConns) - assert.Equal(t, time.Minute*10, fakeConfig.connMaxLifetime) - assert.Equal(t, time.Second*500, fakeConfig.connMaxIdletime) - assert.Equal(t, fakeUser, fakeConfig.Username) - assert.Equal(t, fakePassword, fakeConfig.Password) - }) - - t.Run("can't open pem file", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithPem) - assert.Nil(t, fakeConfig) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Error reading PEM file from fake-pem-path") - }) - - f, err := fs.Create(fakePemPath) - assert.Nil(t, err) - _ = f.Close() - t.Run("", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithPem) - assert.Nil(t, fakeConfig) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "failed to append PEM") - }) -} - -func TestConfig_GetLocalDBConn(t *testing.T) { - t.Run("parse dsn failed", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithWrongURL) - assert.Nil(t, err) - - db, err := fakeConfig.GetLocalDBConn() - assert.Nil(t, db) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "illegal Data Source Name (DNS) specified by url") - }) - - t.Run("get DB connection with addr successfully", func(t *testing.T) { - fakeConfig, err := NewConfig(fakeProperties) - assert.Nil(t, err) - - db, err := fakeConfig.GetLocalDBConn() - assert.Nil(t, err) - assert.NotNil(t, db) - }) -} - -func TestConfig_GetDBConnWithAddr(t *testing.T) { - t.Run("parse dsn failed", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithWrongURL) - assert.Nil(t, err) - - db, err := fakeConfig.GetDBConnWithAddr(fakeAddr) - assert.Nil(t, db) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "illegal Data Source Name (DNS) specified by url") - }) - - t.Run("get local DB connection successfully", func(t *testing.T) { - fakeConfig, err := NewConfig(fakeProperties) - assert.Nil(t, err) - - db, err := fakeConfig.GetDBConnWithAddr(fakeAddr) - assert.Nil(t, err) - assert.NotNil(t, db) - }) -} - -func TestConfig_GetDBPort(t *testing.T) { - t.Run("parse dsn failed", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithWrongURL) - assert.Nil(t, err) - - port := fakeConfig.GetDBPort() - assert.Equal(t, 3306, port) - }) - - t.Run("get db port successfully", func(t *testing.T) { - fakeConfig, err := NewConfig(fakeProperties) - assert.Nil(t, err) - - port := fakeConfig.GetDBPort() - assert.Equal(t, 3306, port) - }) -} diff --git a/pkg/lorry/engines/mysql/conn.go b/pkg/lorry/engines/mysql/conn.go deleted file mode 100644 index 099b1e090e4..00000000000 --- a/pkg/lorry/engines/mysql/conn.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd -This file is part of KubeBlocks project -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 . -*/ - -package mysql - -import ( - "database/sql" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -var connectionPoolCache = make(map[string]*sql.DB) - -// GetDBConnection returns a DB Connection based on dsn. -func GetDBConnection(dsn string) (*sql.DB, error) { - if db, ok := connectionPoolCache[dsn]; ok { - return db, nil - } - - db, err := sql.Open(adminDatabase, dsn) - if err != nil { - return nil, err - } - - connectionPoolCache[dsn] = db - return db, nil -} - -func (mgr *Manager) GetMemberConnection(cluster *dcs.Cluster, member *dcs.Member) (db *sql.DB, err error) { - if member != nil && member.Name != mgr.CurrentMemberName { - addr := cluster.GetMemberAddrWithPort(*member) - db, err = config.GetDBConnWithAddr(addr) - if err != nil { - return nil, err - } - } else { - db = mgr.DB - } - - return db, nil -} diff --git a/pkg/lorry/engines/mysql/get_replica_role.go b/pkg/lorry/engines/mysql/get_replica_role.go deleted file mode 100644 index d264759573d..00000000000 --- a/pkg/lorry/engines/mysql/get_replica_role.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - if cluster == nil { - return "", errors.New("cluster not found") - } - - leader, err := dcs.GetStore().GetLeader() - if err != nil { - mgr.Logger.Info("get leader failed", "error", err.Error()) - } - cluster.Leader = leader - if !cluster.IsLocked() { - mgr.Logger.Info("cluster has no leader lease") - return models.SECONDARY, nil - } - - if cluster.Leader.Name != mgr.CurrentMemberName { - return models.SECONDARY, nil - } - - return models.PRIMARY, nil -} - -func (mgr *Manager) GetReplicaRoleFromDB(ctx context.Context) (string, error) { - slaveRunning, err := mgr.isSlaveRunning() - if err != nil { - return "", err - } - if slaveRunning { - return models.SECONDARY, nil - } - - hasSlave, err := mgr.hasSlaveHosts() - if err != nil { - return "", err - } - if hasSlave { - return models.PRIMARY, nil - } - - isReadonly, err := mgr.IsReadonly(ctx, nil, nil) - if err != nil { - return "", err - } - if isReadonly { - // TODO: in case of diskFull lock, database will be set readonly, - // how to deal with this situation - return models.SECONDARY, nil - } - - return models.PRIMARY, nil -} - -func (mgr *Manager) isSlaveRunning() (bool, error) { - var rowMap = mgr.slaveStatus - - if len(rowMap) == 0 { - return false, nil - } - ioRunning := rowMap.GetString("Slave_IO_Running") - sqlRunning := rowMap.GetString("Slave_SQL_Running") - if ioRunning == "Yes" || sqlRunning == "Yes" { - return true, nil - } - return false, nil -} - -func (mgr *Manager) hasSlaveHosts() (bool, error) { - sql := "show slave hosts" - var rowMap RowMap - - err := QueryRowsMap(mgr.DB, sql, func(rMap RowMap) error { - rowMap = rMap - return nil - }) - if err != nil { - mgr.Logger.Info(sql+" failed", "error", err) - return false, err - } - - if len(rowMap) == 0 { - return false, nil - } - - return true, nil -} diff --git a/pkg/lorry/engines/mysql/get_replica_role_test.go b/pkg/lorry/engines/mysql/get_replica_role_test.go deleted file mode 100644 index f09f3b27f02..00000000000 --- a/pkg/lorry/engines/mysql/get_replica_role_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "testing" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -func TestManager_GetRole(t *testing.T) { - ctx := context.TODO() - manager, _, _ := mockDatabase(t) - ctrl := gomock.NewController(t) - mockDCSStore = dcs.NewMockDCS(ctrl) - dcs.SetStore(mockDCSStore) - - t.Run("cluster not found", func(t *testing.T) { - role, err := manager.GetReplicaRole(ctx, nil) - assert.Empty(t, role) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "cluster not found") - }) - - t.Run("cluster has no leader lease", func(t *testing.T) { - cluster := &dcs.Cluster{} - - mockDCSStore.EXPECT().GetLeader().Return(nil, nil) - role, err := manager.GetReplicaRole(ctx, cluster) - assert.Equal(t, role, models.SECONDARY) - assert.Nil(t, err) - }) - - t.Run("get role successfully", func(t *testing.T) { - leaderNames := []string{fakePodName, "fake-mysql-1"} - expectedRoles := []string{models.PRIMARY, models.SECONDARY} - - for i, leaderName := range leaderNames { - cluster := &dcs.Cluster{Leader: &dcs.Leader{Name: leaderName}} - mockDCSStore.EXPECT().GetLeader().Return(cluster.Leader, nil) - role, err := manager.GetReplicaRole(ctx, cluster) - assert.Nil(t, err) - assert.Equal(t, expectedRoles[i], role) - } - }) -} diff --git a/pkg/lorry/engines/mysql/gtid.go b/pkg/lorry/engines/mysql/gtid.go deleted file mode 100644 index 1ee2891fd90..00000000000 --- a/pkg/lorry/engines/mysql/gtid.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "fmt" - "regexp" - "strconv" - "strings" -) - -var ( - singleValueInterval = regexp.MustCompile("^([0-9]+)$") - multiValueInterval = regexp.MustCompile("^([0-9]+)[-]([0-9]+)$") -) - -// GTIDItem represents an item in a set of GTID ranges, -// for example, the item: "ee194423-3040-11ee-9393-eab5dfc9b22a:1-5:8-10" -type GTIDItem struct { - ServerUUID string - Ranges string -} - -func NewGTIDItem(gtidString string) (*GTIDItem, error) { - gtidString = strings.TrimSpace(gtidString) - tokens := strings.SplitN(gtidString, ":", 2) - if len(tokens) != 2 { - return nil, fmt.Errorf("GTID wrong format: %s", gtidString) - } - if tokens[0] == "" { - return nil, fmt.Errorf("GTID no server UUID: %s", tokens[0]) - } - if tokens[1] == "" { - return nil, fmt.Errorf("GTID no range: %s", tokens[1]) - } - gtidItem := >IDItem{ServerUUID: tokens[0], Ranges: tokens[1]} - return gtidItem, nil -} - -func (gtid *GTIDItem) String() string { - return fmt.Sprintf("%s:%s", gtid.ServerUUID, gtid.Ranges) -} - -func (gtid *GTIDItem) Explode() (result []*GTIDItem) { - intervals := strings.Split(gtid.Ranges, ":") - for _, interval := range intervals { - if submatch := multiValueInterval.FindStringSubmatch(interval); submatch != nil { - intervalStart, _ := strconv.Atoi(submatch[1]) - intervalEnd, _ := strconv.Atoi(submatch[2]) - for i := intervalStart; i <= intervalEnd; i++ { - result = append(result, >IDItem{ServerUUID: gtid.ServerUUID, Ranges: fmt.Sprintf("%d", i)}) - } - } else if submatch := singleValueInterval.FindStringSubmatch(interval); submatch != nil { - result = append(result, >IDItem{ServerUUID: gtid.ServerUUID, Ranges: interval}) - } - } - return result -} - -type GTIDSet struct { - Items []*GTIDItem -} - -func NewOracleGtidSet(gtidSet string) (res *GTIDSet, err error) { - res = >IDSet{} - - gtidSet = strings.TrimSpace(gtidSet) - if gtidSet == "" { - return res, nil - } - gtids := strings.Split(gtidSet, ",") - for _, gtid := range gtids { - gtid = strings.TrimSpace(gtid) - if gtid == "" { - continue - } - if gtidRange, err := NewGTIDItem(gtid); err == nil { - res.Items = append(res.Items, gtidRange) - } else { - return res, err - } - } - return res, nil -} - -func (gtidSet *GTIDSet) RemoveUUID(uuid string) (removed bool) { - var filteredEntries []*GTIDItem - for _, item := range gtidSet.Items { - if item.ServerUUID == uuid { - removed = true - } else { - filteredEntries = append(filteredEntries, item) - } - } - if removed { - gtidSet.Items = filteredEntries - } - return removed -} - -func (gtidSet *GTIDSet) RetainUUID(uuid string) (anythingRemoved bool) { - return gtidSet.RetainUUIDs([]string{uuid}) -} - -func (gtidSet *GTIDSet) RetainUUIDs(uuids []string) (anythingRemoved bool) { - retainUUIDs := map[string]bool{} - for _, uuid := range uuids { - retainUUIDs[uuid] = true - } - var filteredEntries []*GTIDItem - for _, item := range gtidSet.Items { - if retainUUIDs[item.ServerUUID] { - filteredEntries = append(filteredEntries, item) - } else { - anythingRemoved = true - } - } - if anythingRemoved { - gtidSet.Items = filteredEntries - } - return anythingRemoved -} - -func (gtidSet *GTIDSet) SharedUUIDs(other *GTIDSet) (shared []string) { - gtidSetUUIDs := map[string]bool{} - for _, item := range gtidSet.Items { - gtidSetUUIDs[item.ServerUUID] = true - } - for _, item := range other.Items { - if gtidSetUUIDs[item.ServerUUID] { - shared = append(shared, item.ServerUUID) - } - } - return shared -} - -func (gtidSet *GTIDSet) Explode() (result []*GTIDItem) { - for _, entries := range gtidSet.Items { - result = append(result, entries.Explode()...) - } - return result -} - -func (gtidSet *GTIDSet) String() string { - var tokens []string - for _, item := range gtidSet.Items { - tokens = append(tokens, item.String()) - } - return strings.Join(tokens, ",") -} - -func (gtidSet *GTIDSet) IsEmpty() bool { - return len(gtidSet.Items) == 0 -} diff --git a/pkg/lorry/engines/mysql/gtid_test.go b/pkg/lorry/engines/mysql/gtid_test.go deleted file mode 100644 index a4bac1aba3c..00000000000 --- a/pkg/lorry/engines/mysql/gtid_test.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - fakeGTIDString = "ee194423-3040-11ee-9393-eab5dfc9b22a:1-5:7:9-10" - fakeGTIDSet = " ee194423-3040-11ee-9393-eab5dfc9b22a:1-5:7:9-10,b3512340-fc03-11ec-920f-000c29f6e7cf:1-4 " - fakeServerUUID = "ee194423-3040-11ee-9393-eab5dfc9b22a" -) - -func TestNewGTIDItem(t *testing.T) { - fakeGTIDStrings := []string{ - fakeServerUUID, - ":1-5", - "ee194423-3040-11ee-9393-eab5dfc9b22a:", - fakeGTIDString, - } - expectResults := []string{ - "GTID wrong format:", - "GTID no server UUID:", - "GTID no range:", - "", - } - - for i, s := range fakeGTIDStrings { - gtidItem, err := NewGTIDItem(s) - assert.Equal(t, expectResults[i] == "", err == nil) - if err != nil { - assert.ErrorContains(t, err, expectResults[i]) - } - assert.Equal(t, expectResults[i] != "", gtidItem == nil) - } -} - -func TestGTIDItem_Explode(t *testing.T) { - gtidItem, err := NewGTIDItem(fakeGTIDString) - assert.Nil(t, err) - - items := gtidItem.Explode() - assert.NotNil(t, items) - assert.Len(t, items, 8) -} - -func TestNewOracleGtidSet(t *testing.T) { - testCases := []struct { - gtidSets string - expectItemsLen int - expectErrorMsg string - }{ - {"", 0, ""}, - {" , :1-5, ", 0, "GTID no server UUID:"}, - {fakeGTIDSet, 2, ""}, - } - - for _, testCase := range testCases { - gtidSets, err := NewOracleGtidSet(testCase.gtidSets) - assert.Len(t, gtidSets.Items, testCase.expectItemsLen) - assert.Equal(t, err == nil, testCase.expectErrorMsg == "") - if err != nil { - assert.ErrorContains(t, err, testCase.expectErrorMsg) - } - } -} - -func TestGTIDSet_RemoveUUID(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - - testCases := []struct { - uuid string - expectRemoved bool - }{ - {fakeServerUUID, true}, - {"", false}, - } - - for _, testCase := range testCases { - assert.Equal(t, testCase.expectRemoved, gtidSets.RemoveUUID(testCase.uuid)) - } -} - -func TestGTIDSet_RetainUUID(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - - assert.True(t, gtidSets.RetainUUID(fakeServerUUID)) -} - -func TestGTIDSet_RetainUUIDs(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - - testCases := []struct { - uuids []string - expectAnythingRemoved bool - }{ - {[]string{fakeServerUUID}, true}, - {[]string{fakeServerUUID, "b3512340-fc03-11ec-920f-000c29f6e7cf"}, false}, - } - - for _, testCase := range testCases { - assert.Equal(t, testCase.expectAnythingRemoved, gtidSets.RetainUUIDs(testCase.uuids)) - } -} - -func TestGTIDSet_SharedUUIDs(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - assert.False(t, gtidSets.IsEmpty()) - - testCases := []struct { - other *GTIDSet - expectSharedItemsLen int - }{ - {>IDSet{ - Items: []*GTIDItem{ - { - ServerUUID: fakeServerUUID, - }, - }, - }, 1}, - {>IDSet{Items: make([]*GTIDItem, 0)}, 0}, - } - - for _, testCase := range testCases { - assert.Len(t, gtidSets.SharedUUIDs(testCase.other), testCase.expectSharedItemsLen) - } -} - -func TestGTIDSet_Explode(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - - items := gtidSets.Explode() - assert.NotNil(t, items) - assert.Len(t, items, 12) -} - -func TestGTIDSet_String(t *testing.T) { - gtidSets, err := NewOracleGtidSet(fakeGTIDSet) - assert.Nil(t, err) - - assert.Equal(t, "ee194423-3040-11ee-9393-eab5dfc9b22a:1-5:7:9-10,b3512340-fc03-11ec-920f-000c29f6e7cf:1-4", gtidSets.String()) -} diff --git a/pkg/lorry/engines/mysql/json.go b/pkg/lorry/engines/mysql/json.go deleted file mode 100644 index 3d0e9f99b38..00000000000 --- a/pkg/lorry/engines/mysql/json.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "database/sql" - "database/sql/driver" - "encoding/json" - "reflect" -) - -func jsonify(rows *sql.Rows) ([]byte, error) { - columnTypes, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - var ret []interface{} - for rows.Next() { - values := prepareValues(columnTypes) - err = rows.Scan(values...) - if err != nil { - return nil, err - } - r, err := convert(columnTypes, values) - if err != nil { - return nil, err - } - ret = append(ret, r) - } - return json.Marshal(ret) -} - -func prepareValues(columnTypes []*sql.ColumnType) []interface{} { - types := make([]reflect.Type, len(columnTypes)) - for i, tp := range columnTypes { - types[i] = tp.ScanType() - } - values := make([]interface{}, len(columnTypes)) - for i := range values { - switch types[i].Kind() { - case reflect.String, reflect.Interface: - values[i] = &sql.NullString{} - case reflect.Bool: - values[i] = &sql.NullBool{} - case reflect.Float64: - values[i] = &sql.NullFloat64{} - case reflect.Int16, reflect.Uint16: - values[i] = &sql.NullInt16{} - case reflect.Int32, reflect.Uint32: - values[i] = &sql.NullInt32{} - case reflect.Int64, reflect.Uint64: - values[i] = &sql.NullInt64{} - default: - values[i] = reflect.New(types[i]).Interface() - } - } - return values -} - -func convert(columnTypes []*sql.ColumnType, values []interface{}) (map[string]interface{}, error) { - r := map[string]interface{}{} - for i, ct := range columnTypes { - value := values[i] - switch v := values[i].(type) { - case driver.Valuer: - if vv, err := v.Value(); err != nil { - return nil, err - } else { - value = interface{}(vv) - } - case *sql.RawBytes: - // special case for sql.RawBytes, see https://github.com/go-sql-driver/mysql/blob/master/fields.go#L178 - switch ct.DatabaseTypeName() { - case "VARCHAR", "CHAR", "TEXT", "LONGTEXT": - value = string(*v) - } - } - if value != nil { - r[ct.Name()] = value - } - } - return r, nil -} diff --git a/pkg/lorry/engines/mysql/json_test.go b/pkg/lorry/engines/mysql/json_test.go deleted file mode 100644 index 6d3698072d4..00000000000 --- a/pkg/lorry/engines/mysql/json_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "database/sql" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" -) - -func TestJsonify(t *testing.T) { - manager, mock, _ := mockDatabase(t) - - t.Run("jsonify successfully with testCases", func(t *testing.T) { - fakeRows := sqlmock.NewRowsWithColumnDefinition([]*sqlmock.Column{ - sqlmock.NewColumn("name").OfType("VARCHAR", ""), - sqlmock.NewColumn("age").OfType("INT", 0), - sqlmock.NewColumn("sex").OfType("BOOL", false), - sqlmock.NewColumn("grade").OfType("FLOAT", 92.8), - sqlmock.NewColumn("height").OfType("INT", int16(142)), - sqlmock.NewColumn("weight").OfType("INT", int32(80)), - sqlmock.NewColumn("timestamp").OfType("TIMESTAMP", int64(1000000)), - sqlmock.NewColumn("raw").OfType("VARCHAR", sql.RawBytes{}), - }...).AddRow("bob", 8, true, 92.8, int16(142), int32(80), int64(1000000), sql.RawBytes("123")) - - mock.ExpectQuery("select").WillReturnRows(fakeRows) - rows, err := manager.DB.Query("select") - assert.Nil(t, err) - - ret, err := jsonify(rows) - assert.Equal(t, `[{"age":8,"grade":92.8,"height":142,"name":"bob","raw":"123","sex":true,"timestamp":1000000,"weight":80}]`, string(ret)) - assert.Nil(t, err) - }) - - t.Run("rows closed", func(t *testing.T) { - fakeRows := sqlmock.NewRows([]string{"fake"}).AddRow("1") - mock.ExpectQuery("select").WillReturnRows(fakeRows) - rows, err := manager.DB.Query("select") - assert.Nil(t, err) - - _ = rows.Close() - ret, err := jsonify(rows) - assert.Nil(t, ret) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Rows are closed") - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/mysql/manager.go b/pkg/lorry/engines/mysql/manager.go deleted file mode 100644 index 07eda373315..00000000000 --- a/pkg/lorry/engines/mysql/manager.go +++ /dev/null @@ -1,753 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "database/sql" - "fmt" - "strconv" - "strings" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -type Manager struct { - engines.DBManagerBase - DB *sql.DB - hostname string - serverID uint - version string - binlogFormat string - logbinEnabled bool - logReplicationUpdatesEnabled bool - opTimestamp int64 - globalState map[string]string - masterStatus RowMap - slaveStatus RowMap -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("MySQL") - config, err := NewConfig(properties) - if err != nil { - return nil, err - } - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - serverID, err := engines.GetIndex(managerBase.CurrentMemberName) - if err != nil { - return nil, err - } - - db, err := config.GetLocalDBConn() - if err != nil { - return nil, errors.Wrap(err, "connect to MySQL") - } - - mgr := &Manager{ - DBManagerBase: *managerBase, - serverID: uint(serverID) + 1, - DB: db, - } - - return mgr, nil -} - -func (mgr *Manager) InitializeCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) IsRunning() bool { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - // test if db is ready to connect or not - err := mgr.DB.PingContext(ctx) - if err != nil { - var driverErr *mysql.MySQLError - if errors.As(err, &driverErr) { - // Now the error number is accessible directly - if driverErr.Number == 1040 { - mgr.Logger.Info("connect failed: Too many connections") - return true - } - } - mgr.Logger.Info("DB is not ready", "error", err) - return false - } - - return true -} - -func (mgr *Manager) IsDBStartupReady() bool { - if mgr.DBStartupReady { - return true - } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - // test if db is ready to connect or not - err := mgr.DB.PingContext(ctx) - if err != nil { - mgr.Logger.Info("DB is not ready", "error", err) - return false - } - - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func (mgr *Manager) IsReadonly(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, error) { - db, err := mgr.GetMemberConnection(cluster, member) - if err != nil { - mgr.Logger.Info("Get Member conn failed", "error", err.Error()) - return false, err - } - - var readonly bool - err = db.QueryRowContext(ctx, "select @@global.hostname, @@global.version, "+ - "@@global.read_only, @@global.binlog_format, @@global.log_bin, @@global.log_slave_updates"). - Scan(&mgr.hostname, &mgr.version, &readonly, &mgr.binlogFormat, - &mgr.logbinEnabled, &mgr.logReplicationUpdatesEnabled) - if err != nil { - mgr.Logger.Info("Get global readonly failed", "error", err.Error()) - return false, err - } - return readonly, nil -} - -func (mgr *Manager) IsLeader(ctx context.Context, _ *dcs.Cluster) (bool, error) { - readonly, err := mgr.IsReadonly(ctx, nil, nil) - - if err != nil || readonly { - return false, err - } - - // if cluster.Leader != nil && cluster.Leader.Name != "" { - // if cluster.Leader.Name == mgr.CurrentMemberName { - // return true, nil - // } else { - // return false, nil - // } - // } - - // // During the initialization of cluster, there would be more than one leader, - // // in this case, the first member is chosen as the leader - // if mgr.CurrentMemberName == cluster.Members[0].Name { - // return true, nil - // } - // isFirstMemberLeader, err := mgr.IsLeaderMember(ctx, cluster, &cluster.Members[0]) - // if err == nil && isFirstMemberLeader { - // return false, nil - // } - - return true, err -} - -func (mgr *Manager) IsLeaderMember(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, error) { - readonly, err := mgr.IsReadonly(ctx, cluster, member) - if err != nil || readonly { - return false, err - } - - return true, err -} - -func (mgr *Manager) InitiateCluster(*dcs.Cluster) error { - return nil -} - -func (mgr *Manager) GetMemberAddrs(_ context.Context, cluster *dcs.Cluster) []string { - return cluster.GetMemberAddrs() -} - -func (mgr *Manager) IsCurrentMemberInCluster(context.Context, *dcs.Cluster) bool { - return true -} - -func (mgr *Manager) IsCurrentMemberHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - // _, _ = mgr.EnsureServerID(ctx) - member := cluster.GetMemberWithName(mgr.CurrentMemberName) - - return mgr.IsMemberHealthy(ctx, cluster, member) -} - -func (mgr *Manager) IsMemberLagging(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, int64) { - var leaderDBState *dcs.DBState - if cluster.Leader == nil || cluster.Leader.DBState == nil { - // In the event of leader initialization failure, there is no available database state information, - // just returning false allows other replicas to acquire the lease. - mgr.Logger.Info("No leader DBState info") - return false, 0 - } - leaderDBState = cluster.Leader.DBState - - db, err := mgr.GetMemberConnection(cluster, member) - if err != nil { - mgr.Logger.Info("Get Member conn failed", "error", err) - return true, 0 - } - - opTimestamp, err := mgr.GetOpTimestamp(ctx, db) - if err != nil { - mgr.Logger.Info("get op timestamp failed", "error", err) - return true, 0 - } - lag := leaderDBState.OpTimestamp - opTimestamp - if lag <= cluster.HaConfig.GetMaxLagOnSwitchover() { - return false, lag - } - mgr.Logger.Info(fmt.Sprintf("The member %s has lag: %d", member.Name, lag)) - return true, lag -} - -func (mgr *Manager) IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool { - db, err := mgr.GetMemberConnection(cluster, member) - if err != nil { - mgr.Logger.Info("Get Member conn failed", "error", err) - return false - } - - if cluster.Leader != nil && cluster.Leader.Name == member.Name { - if mgr.WriteCheck(ctx, db) != nil { - return false - } - } - if mgr.ReadCheck(ctx, db) != nil { - return false - } - - return true -} - -func (mgr *Manager) GetDBState(ctx context.Context, cluster *dcs.Cluster) *dcs.DBState { - mgr.DBState = nil - - globalState, err := mgr.GetGlobalState(ctx, mgr.DB) - if err != nil { - mgr.Logger.Info("select global failed", "error", err) - return nil - } - - masterStatus, err := mgr.GetMasterStatus(ctx, mgr.DB) - if err != nil { - mgr.Logger.Info("show master status failed", "error", err) - return nil - } - - slaveStatus, err := mgr.GetSlaveStatus(ctx, mgr.DB) - if err != nil { - mgr.Logger.Info("show slave status failed", "error", err) - return nil - } - - opTimestamp, err := mgr.GetOpTimestamp(ctx, mgr.DB) - if err != nil { - mgr.Logger.Info("get op timestamp failed", "error", err) - return nil - } - - dbState := &dcs.DBState{ - OpTimestamp: opTimestamp, - Extra: map[string]string{}, - } - for k, v := range globalState { - dbState.Extra[k] = v - } - - if cluster.Leader != nil && cluster.Leader.Name == mgr.CurrentMemberName { - dbState.Extra["Binlog_File"] = masterStatus.GetString("File") - dbState.Extra["Binlog_Pos"] = masterStatus.GetString("Pos") - } else { - dbState.Extra["Master_Host"] = slaveStatus.GetString("Master_Host") - dbState.Extra["Master_UUID"] = slaveStatus.GetString("Master_UUID") - dbState.Extra["Slave_IO_Running"] = slaveStatus.GetString("Slave_IO_Running") - dbState.Extra["Slave_SQL_Running"] = slaveStatus.GetString("Slave_SQL_Running") - dbState.Extra["Last_IO_Error"] = slaveStatus.GetString("Last_IO_Error") - dbState.Extra["Last_SQL_Error"] = slaveStatus.GetString("Last_SQL_Error") - dbState.Extra["Master_Log_File"] = slaveStatus.GetString("Master_Log_File") - dbState.Extra["Read_Master_Log_Pos"] = slaveStatus.GetString("Read_Master_Log_Pos") - dbState.Extra["Relay_Master_Log_File"] = slaveStatus.GetString("Relay_Master_Log_File") - dbState.Extra["Exec_Master_Log_Pos"] = slaveStatus.GetString("Exec_Master_Log_Pos") - } - - mgr.globalState = globalState - mgr.masterStatus = masterStatus - mgr.slaveStatus = slaveStatus - mgr.opTimestamp = opTimestamp - mgr.DBState = dbState - - return dbState -} - -func (mgr *Manager) GetSecondsBehindMaster(ctx context.Context) (int, error) { - slaveStatus, err := mgr.GetSlaveStatus(ctx, mgr.DB) - if err != nil { - mgr.Logger.Info("show slave status failed", "error", err) - return 0, err - } - if len(slaveStatus) == 0 { - return 0, nil - } - secondsBehindMaster := slaveStatus.GetString("Seconds_Behind_Master") - if secondsBehindMaster == "NULL" || secondsBehindMaster == "" { - return 0, nil - } - return strconv.Atoi(secondsBehindMaster) -} - -func (mgr *Manager) WriteCheck(ctx context.Context, db *sql.DB) error { - writeSQL := fmt.Sprintf(`BEGIN; -CREATE DATABASE IF NOT EXISTS kubeblocks; -CREATE TABLE IF NOT EXISTS kubeblocks.kb_health_check(type INT, check_ts BIGINT, PRIMARY KEY(type)); -INSERT INTO kubeblocks.kb_health_check VALUES(%d, UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE check_ts = UNIX_TIMESTAMP(); -COMMIT;`, engines.CheckStatusType) - _, err := db.ExecContext(ctx, writeSQL) - if err != nil { - mgr.Logger.Info(writeSQL+" executing failed", "error", err.Error()) - return err - } - return nil -} - -func (mgr *Manager) ReadCheck(ctx context.Context, db *sql.DB) error { - _, err := mgr.GetOpTimestamp(ctx, db) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - // no healthy check records, return true - return nil - } - var mysqlErr *mysql.MySQLError - if errors.As(err, &mysqlErr) && (mysqlErr.Number == 1049 || mysqlErr.Number == 1146) { - // error 1049: database does not exists - // error 1146: table does not exists - // no healthy database, return true - return nil - } - mgr.Logger.Info("Read check failed", "error", err) - return err - } - - return nil -} - -func (mgr *Manager) GetOpTimestamp(ctx context.Context, db *sql.DB) (int64, error) { - readSQL := fmt.Sprintf(`select check_ts from kubeblocks.kb_health_check where type=%d limit 1;`, engines.CheckStatusType) - var opTimestamp int64 - err := db.QueryRowContext(ctx, readSQL).Scan(&opTimestamp) - return opTimestamp, err -} - -func (mgr *Manager) GetGlobalState(ctx context.Context, db *sql.DB) (map[string]string, error) { - var hostname, serverUUID, gtidExecuted, gtidPurged, isReadonly, superReadonly string - err := db.QueryRowContext(ctx, "select @@global.hostname, @@global.server_uuid, @@global.gtid_executed, @@global.gtid_purged, @@global.read_only, @@global.super_read_only"). - Scan(&hostname, &serverUUID, >idExecuted, >idPurged, &isReadonly, &superReadonly) - if err != nil { - return nil, err - } - - return map[string]string{ - "hostname": hostname, - "server_uuid": serverUUID, - "gtid_executed": gtidExecuted, - "gtid_purged": gtidPurged, - "read_only": isReadonly, - "super_read_only": superReadonly, - }, nil -} - -func (mgr *Manager) GetSlaveStatus(context.Context, *sql.DB) (RowMap, error) { - sql := "show slave status" - var rowMap RowMap - - err := QueryRowsMap(mgr.DB, sql, func(rMap RowMap) error { - rowMap = rMap - return nil - }) - if err != nil { - mgr.Logger.Info("executing "+sql+" failed", "error", err.Error()) - return nil, err - } - return rowMap, nil -} - -func (mgr *Manager) GetMasterStatus(context.Context, *sql.DB) (RowMap, error) { - sql := "show master status" - var rowMap RowMap - - err := QueryRowsMap(mgr.DB, sql, func(rMap RowMap) error { - rowMap = rMap - return nil - }) - if err != nil { - mgr.Logger.Info("executing "+sql+" failed", "error", err.Error()) - return nil, err - } - return rowMap, nil -} - -func (mgr *Manager) Recover(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) LeaveMemberFromCluster(context.Context, *dcs.Cluster, string) error { - return nil -} - -// func (mgr *Manager) IsClusterHealthy(ctx context.Context, cluster *dcs.Cluster) bool { -// leaderMember := cluster.GetLeaderMember() -// if leaderMember == nil { -// mgr.Logger.Info("IsClusterHealthy: has no leader.") -// return true -// } - -// return mgr.IsMemberHealthy(ctx, cluster, leaderMember) -// } - -// IsClusterInitialized is a method to check if cluster is initialized or not -func (mgr *Manager) IsClusterInitialized(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - _, err := mgr.GetVersion(ctx) - if err != nil { - return false, err - } - - if cluster != nil { - isValid, err := mgr.ValidateAddr(ctx, cluster) - if err != nil || !isValid { - return isValid, err - } - } - - // err = mgr.EnableSemiSyncIfNeed(ctx) - // if err != nil { - // return false, err - // } - return mgr.EnsureServerID(ctx) -} - -func (mgr *Manager) GetVersion(ctx context.Context) (string, error) { - if mgr.version != "" { - return mgr.version, nil - } - err := mgr.DB.QueryRowContext(ctx, "select version()").Scan(&mgr.version) - if err != nil { - return "", errors.Wrap(err, "Get version failed") - } - return mgr.version, nil -} - -func (mgr *Manager) ValidateAddr(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - // The maximum length of the server addr is 255 characters. Before MySQL 8.0.17 it was 60 characters. - currentMemberName := mgr.GetCurrentMemberName() - member := cluster.GetMemberWithName(currentMemberName) - addr := cluster.GetMemberShortAddr(*member) - maxLength := 255 - if IsBeforeVersion(mgr.version, "8.0.17") { - maxLength = 60 - } - - if len(addr) > maxLength { - return false, errors.Errorf("The length of the member address must be less than or equal to %d", maxLength) - } - return true, nil -} - -func (mgr *Manager) EnsureServerID(ctx context.Context) (bool, error) { - var serverID uint - err := mgr.DB.QueryRowContext(ctx, "select @@global.server_id").Scan(&serverID) - if err != nil { - mgr.Logger.Info("Get global server id failed", "error", err) - return false, err - } - if serverID == mgr.serverID { - return true, nil - } - mgr.Logger.Info("Set global server id", "server_id", serverID) - - setServerID := fmt.Sprintf(`set global server_id = %d`, mgr.serverID) - mgr.Logger.Info("Set global server id", "server-id", setServerID) - _, err = mgr.DB.Exec(setServerID) - if err != nil { - mgr.Logger.Info("set server id failed", "error", err) - return false, err - } - - return true, nil -} - -func (mgr *Manager) EnableSemiSyncIfNeed(ctx context.Context) error { - var status string - err := mgr.DB.QueryRowContext(ctx, "SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS "+ //nolint:goconst - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").Scan(&status) //nolint:goconst - if err != nil { - if err == sql.ErrNoRows { - return nil - } - mgr.Logger.Info("Get rpl_semi_sync_source plugin status failed", "error", err.Error()) - return err - } - - // In MySQL 8.0, semi-sync configuration options should not be specified in my.cnf, - // as this may cause the database initialization process to fail: - // [Warning] [MY-013501] [Server] Ignoring --plugin-load[_add] list as the server is running with --initialize(-insecure). - // [ERROR] [MY-000067] [Server] unknown variable 'rpl_semi_sync_master_enabled=1'. - if status == "ACTIVE" { - setSourceEnable := "SET GLOBAL rpl_semi_sync_source_enabled = 1;" + - "SET GLOBAL rpl_semi_sync_source_timeout = 100000;" - _, err = mgr.DB.Exec(setSourceEnable) - if err != nil { - mgr.Logger.Info(setSourceEnable+" execute failed", "error", err.Error()) - return err - } - } - - err = mgr.DB.QueryRowContext(ctx, "SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS "+ - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").Scan(&status) - if err != nil { - if err == sql.ErrNoRows { - return nil - } - mgr.Logger.Info("Get rpl_semi_sync_replica plugin status failed", "error", err.Error()) - return err - } - - if status == "ACTIVE" { - setSourceEnable := "SET GLOBAL rpl_semi_sync_replica_enabled = 1;" - _, err = mgr.DB.Exec(setSourceEnable) - if err != nil { - mgr.Logger.Info(setSourceEnable+" execute failed", "error", err.Error()) - return err - } - } - return nil -} - -func (mgr *Manager) Promote(ctx context.Context, cluster *dcs.Cluster) error { - err := mgr.EnableSemiSyncSource(ctx) - if err != nil { - return err - } - - err = mgr.DisableSemiSyncReplica(ctx) - if err != nil { - return err - } - - if (mgr.globalState["super_read_only"] == "0" && mgr.globalState["read_only"] == "0") && - (len(mgr.slaveStatus) == 0 || (mgr.slaveStatus.GetString("Slave_IO_Running") == "No" && - mgr.slaveStatus.GetString("Slave_SQL_Running") == "No")) { - return nil - } - - // wait relay log to play - secondsBehindMaster, err := mgr.GetSecondsBehindMaster(ctx) - for err != nil || secondsBehindMaster != 0 { - time.Sleep(time.Second) - secondsBehindMaster, err = mgr.GetSecondsBehindMaster(ctx) - } - - stopReadOnly := `set global read_only=off;set global super_read_only=off;` - stopSlave := `stop slave;` - resp, err := mgr.DB.Exec(stopReadOnly + stopSlave) - if err != nil { - mgr.Logger.Info("promote failed", "error", err.Error()) - return err - } - - // fresh db state - mgr.GetDBState(ctx, cluster) - mgr.Logger.Info(fmt.Sprintf("promote success, resp:%v", resp)) - return nil -} - -func (mgr *Manager) Demote(context.Context) error { - setReadOnly := `set global read_only=on;set global super_read_only=on;` - - _, err := mgr.DB.Exec(setReadOnly) - if err != nil { - mgr.Logger.Info("demote failed", "error", err.Error()) - return err - } - return nil -} - -func (mgr *Manager) Follow(ctx context.Context, cluster *dcs.Cluster) error { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - return fmt.Errorf("cluster has no leader") - } - - if mgr.CurrentMemberName == cluster.Leader.Name { - mgr.Logger.Info("i get the leader key, don't need to follow") - return nil - } - - if !mgr.isRecoveryConfOutdated(cluster.Leader.Name) { - return nil - } - err := mgr.EnableSemiSyncReplica(ctx) - if err != nil { - return err - } - err = mgr.DisableSemiSyncSource(ctx) - if err != nil { - return err - } - - stopSlave := `stop slave;` - // MySQL 5.7 has a limitation where the length of the master_host cannot exceed 60 characters. - masterHost := cluster.GetMemberShortAddr(*leaderMember) - changeMaster := fmt.Sprintf(`change master to master_host='%s',master_user='%s',master_password='%s',master_port=%s,master_auto_position=1;`, - masterHost, config.Username, config.Password, leaderMember.DBPort) - mgr.Logger.Info("follow new leader", "changemaster", changeMaster) - startSlave := `start slave;` - - _, err = mgr.DB.Exec(stopSlave + changeMaster + startSlave) - if err != nil { - mgr.Logger.Info("Follow master failed", "error", err.Error()) - return err - } - - err = mgr.SetSemiSyncSourceTimeout(ctx, cluster, leaderMember) - if err != nil { - return err - } - - // fresh db state - mgr.GetDBState(ctx, cluster) - mgr.Logger.Info("successfully follow new leader", "leader-name", leaderMember.Name) - return nil -} - -func (mgr *Manager) isRecoveryConfOutdated(leader string) bool { - var rowMap = mgr.slaveStatus - - if len(rowMap) == 0 { - return true - } - - ioRunning := rowMap.GetString("Slave_IO_Running") - sqlRunning := rowMap.GetString("Slave_SQL_Running") - if ioRunning == "No" || sqlRunning == "No" { - mgr.Logger.Info("slave status error", "status", rowMap) - return true - } - - masterHost := rowMap.GetString("Master_Host") - return !strings.HasPrefix(masterHost, leader) -} - -func (mgr *Manager) GetHealthiestMember(*dcs.Cluster, string) *dcs.Member { - return nil -} - -func (mgr *Manager) HasOtherHealthyLeader(ctx context.Context, cluster *dcs.Cluster) *dcs.Member { - isLeader, err := mgr.IsLeader(ctx, cluster) - if err == nil && isLeader { - // if current member is leader, just return - return nil - } - - for _, member := range cluster.Members { - if member.Name == mgr.CurrentMemberName { - continue - } - - isLeader, err := mgr.IsLeaderMember(ctx, cluster, &member) - if err == nil && isLeader { - return &member - } - } - - return nil -} - -// HasOtherHealthyMembers checks if there are any healthy members, excluding the leader -func (mgr *Manager) HasOtherHealthyMembers(ctx context.Context, cluster *dcs.Cluster, leader string) []*dcs.Member { - members := make([]*dcs.Member, 0) - for _, member := range cluster.Members { - if member.Name == leader { - continue - } - if !mgr.IsMemberHealthy(ctx, cluster, &member) { - continue - } - members = append(members, &member) - } - - return members -} - -func (mgr *Manager) IsRootCreated(context.Context) (bool, error) { - return true, nil -} - -func (mgr *Manager) CreateRoot(context.Context) error { - return nil -} - -func (mgr *Manager) Lock(context.Context, string) error { - setReadOnly := `set global read_only=on;` - - _, err := mgr.DB.Exec(setReadOnly) - if err != nil { - mgr.Logger.Info("Lock failed", "error", err.Error()) - return err - } - mgr.IsLocked = true - return nil -} - -func (mgr *Manager) Unlock(context.Context) error { - setReadOnlyOff := `set global read_only=off;` - - _, err := mgr.DB.Exec(setReadOnlyOff) - if err != nil { - mgr.Logger.Info("Unlock failed", "error", err.Error()) - return err - } - mgr.IsLocked = false - return nil -} - -func (mgr *Manager) ShutDownWithWait() { - for _, db := range connectionPoolCache { - _ = db.Close() - } - connectionPoolCache = make(map[string]*sql.DB) -} diff --git a/pkg/lorry/engines/mysql/manager_test.go b/pkg/lorry/engines/mysql/manager_test.go deleted file mode 100644 index 644acf1cb30..00000000000 --- a/pkg/lorry/engines/mysql/manager_test.go +++ /dev/null @@ -1,920 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "database/sql" - "fmt" - "testing" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -const ( - fakePodName = "fake-mysql-0" - fakeClusterCompName = "test-mysql" - fakeNamespace = "fake-namespace" - fakeDBPort = "fake-port" -) - -func TestNewManager(t *testing.T) { - defer viper.Reset() - - t.Run("new config failed", func(t *testing.T) { - manager, err := NewManager(fakePropertiesWithPem) - - assert.Nil(t, manager) - assert.NotNil(t, err) - }) - - t.Run("new db manager base failed", func(t *testing.T) { - manager, err := NewManager(fakeProperties) - - assert.Nil(t, manager) - assert.NotNil(t, err) - assert.ErrorContains(t, err, fmt.Sprintf("%s is not set", constant.KBEnvPodName)) - }) - - viper.Set(constant.KBEnvPodName, "fake") - viper.Set(constant.KBEnvClusterCompName, fakeClusterCompName) - viper.Set(constant.KBEnvNamespace, fakeNamespace) - t.Run("get server id failed", func(t *testing.T) { - manager, err := NewManager(fakeProperties) - - assert.Nil(t, manager) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "the format of member name is wrong") - }) - - viper.Set(constant.KBEnvPodName, fakePodName) - t.Run("get local connection failed", func(t *testing.T) { - manager, err := NewManager(fakePropertiesWithWrongURL) - - assert.Nil(t, manager) - assert.NotNil(t, err) - }) - - t.Run("new manager successfully", func(t *testing.T) { - managerIFace, err := NewManager(fakeProperties) - assert.Nil(t, err) - - manager, ok := managerIFace.(*Manager) - assert.True(t, ok) - assert.Equal(t, fakePodName, manager.CurrentMemberName) - assert.Equal(t, fakeNamespace, manager.Namespace) - assert.Equal(t, fakeClusterCompName, manager.ClusterCompName) - assert.Equal(t, uint(1), manager.serverID) - }) -} - -func TestManager_IsRunning(t *testing.T) { - manager, mock, _ := mockDatabase(t) - - t.Run("Too many connections", func(t *testing.T) { - mock.ExpectPing(). - WillReturnError(&mysql.MySQLError{Number: 1040}) - - isRunning := manager.IsRunning() - assert.True(t, isRunning) - }) - - t.Run("DB is not ready", func(t *testing.T) { - mock.ExpectPing(). - WillReturnError(fmt.Errorf("some error")) - - isRunning := manager.IsRunning() - assert.False(t, isRunning) - }) - - t.Run("ping db overtime", func(t *testing.T) { - mock.ExpectPing().WillDelayFor(time.Second) - - isRunning := manager.IsRunning() - assert.False(t, isRunning) - }) - - t.Run("db is running", func(t *testing.T) { - mock.ExpectPing() - - isRunning := manager.IsRunning() - assert.True(t, isRunning) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsDBStartupReady(t *testing.T) { - manager, mock, _ := mockDatabase(t) - - t.Run("db has start up", func(t *testing.T) { - manager.DBStartupReady = true - defer func() { - manager.DBStartupReady = false - }() - - dbReady := manager.IsDBStartupReady() - assert.True(t, dbReady) - }) - - t.Run("ping db failed", func(t *testing.T) { - mock.ExpectPing().WillDelayFor(time.Second) - - dbReady := manager.IsDBStartupReady() - assert.False(t, dbReady) - }) - - t.Run("check db start up successfully", func(t *testing.T) { - mock.ExpectPing() - - dbReady := manager.IsDBStartupReady() - assert.True(t, dbReady) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsReadonly(t *testing.T) { - ctx := context.TODO() - manager, _, _ := mockDatabase(t) - cluster := &dcs.Cluster{} - - t.Run("Get Member conn failed", func(t *testing.T) { - _, _ = NewConfig(fakePropertiesWithWrongURL) - - readonly, err := manager.IsReadonly(ctx, cluster, &dcs.Member{}) - assert.False(t, readonly) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "illegal Data Source Name (DNS) specified by") - }) -} - -func TestManager_IsLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("check is read only failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.False(t, isLeader) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("current member is leader", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.version", "@@global.read_only", - "@@global.binlog_format", "@@global.log_bin", "@@global.log_slave_updates"}). - AddRow(fakePodName, "8.0.30", false, "MIXED", "1", "1")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsLeaderMember(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("check is read only failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - isLeader, err := manager.IsLeaderMember(ctx, nil, nil) - assert.False(t, isLeader) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("current member is leader", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.version", "@@global.read_only", - "@@global.binlog_format", "@@global.log_bin", "@@global.log_slave_updates"}). - AddRow(fakePodName, "8.0.30", false, "MIXED", "1", "1")) - - isLeader, err := manager.IsLeaderMember(ctx, nil, nil) - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_GetMemberAddrs(t *testing.T) { - ctx := context.TODO() - manager, _, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - { - Name: fakePodName, - DBPort: fakeDBPort, - }, - }, - Namespace: fakeNamespace, - } - - viper.Set(constant.KubernetesClusterDomainEnv, "cluster.local") - defer viper.Reset() - addrs := manager.GetMemberAddrs(ctx, cluster) - assert.Len(t, addrs, 1) - assert.Equal(t, "fake-mysql-0.fake-mysql-headless.fake-namespace.svc.cluster.local:fake-port", addrs[0]) -} - -func TestManager_IsMemberLagging(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{Leader: &dcs.Leader{}, HaConfig: &dcs.HaConfig{}} - - t.Run("No leader DBState info", func(t *testing.T) { - isMemberLagging, lags := manager.IsMemberLagging(ctx, cluster, nil) - assert.False(t, isMemberLagging) - assert.Zero(t, lags) - }) - - cluster.Leader.DBState = &dcs.DBState{} - t.Run("Get Member conn failed", func(t *testing.T) { - _, _ = NewConfig(fakePropertiesWithWrongURL) - - isMemberLagging, lags := manager.IsMemberLagging(ctx, cluster, &dcs.Member{}) - assert.True(t, isMemberLagging) - assert.Zero(t, lags) - }) - - _, _ = NewConfig(fakeProperties) - t.Run("get op timestamp failed", func(t *testing.T) { - mock.ExpectQuery("select check_ts"). - WillReturnError(fmt.Errorf("some error")) - - isMemberLagging, lags := manager.IsMemberLagging(ctx, cluster, &dcs.Member{Name: fakePodName}) - assert.True(t, isMemberLagging) - assert.Zero(t, lags) - }) - - cluster.Leader.DBState.OpTimestamp = 100 - t.Run("no lags", func(t *testing.T) { - - mock.ExpectQuery("select check_ts"). - WillReturnRows(sqlmock.NewRows([]string{"check_ts"}).AddRow(100)) - - isMemberLagging, lags := manager.IsMemberLagging(ctx, cluster, &dcs.Member{Name: fakePodName}) - assert.False(t, isMemberLagging) - assert.Zero(t, lags) - }) - - t.Run("member is lagging", func(t *testing.T) { - mock.ExpectQuery("select check_ts"). - WillReturnRows(sqlmock.NewRows([]string{"check_ts"}).AddRow(0)) - - isMemberLagging, lags := manager.IsMemberLagging(ctx, cluster, &dcs.Member{Name: fakePodName}) - assert.True(t, isMemberLagging) - assert.Equal(t, int64(100), lags) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsMemberHealthy(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - member := dcs.Member{Name: fakePodName} - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{Name: fakePodName}, - Members: []dcs.Member{member}, - } - - t.Run("Get Member conn failed", func(t *testing.T) { - _, _ = NewConfig(fakePropertiesWithWrongURL) - - isHealthy := manager.IsMemberHealthy(ctx, cluster, &dcs.Member{}) - assert.False(t, isHealthy) - }) - - _, _ = NewConfig(fakeProperties) - t.Run("write check failed", func(t *testing.T) { - mock.ExpectExec("CREATE DATABASE IF NOT EXISTS kubeblocks"). - WillReturnError(fmt.Errorf("some error")) - - isHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.False(t, isHealthy) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_WriteCheck(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("write check failed", func(t *testing.T) { - mock.ExpectExec("CREATE DATABASE IF NOT EXISTS kubeblocks;"). - WillReturnError(fmt.Errorf("some error")) - - canWrite := manager.WriteCheck(ctx, manager.DB) - assert.NotNil(t, canWrite) - }) - - t.Run("write check successfully", func(t *testing.T) { - mock.ExpectExec("CREATE DATABASE IF NOT EXISTS kubeblocks;"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - canWrite := manager.WriteCheck(ctx, manager.DB) - assert.Nil(t, canWrite) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_ReadCheck(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("no rows in result set", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(sql.ErrNoRows) - - canRead := manager.ReadCheck(ctx, manager.DB) - assert.Nil(t, canRead) - }) - - t.Run("no healthy database", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(&mysql.MySQLError{Number: 1049}) - - canRead := manager.ReadCheck(ctx, manager.DB) - assert.Nil(t, canRead) - }) - - t.Run("Read check failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - canRead := manager.ReadCheck(ctx, manager.DB) - assert.NotNil(t, canRead) - }) - - t.Run("Read check successfully", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"check_ts"}).AddRow(1)) - - canRead := manager.ReadCheck(ctx, manager.DB) - assert.Nil(t, canRead) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_GetGlobalState(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("get global state successfully", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - - globalState, err := manager.GetGlobalState(ctx, manager.DB) - assert.Nil(t, err) - assert.NotNil(t, globalState) - assert.Equal(t, fakePodName, globalState["hostname"]) - assert.Equal(t, fakeServerUUID, globalState["server_uuid"]) - assert.Equal(t, fakeGTIDString, globalState["gtid_executed"]) - assert.Equal(t, fakeGTIDSet, globalState["gtid_purged"]) - assert.Equal(t, "1", globalState["read_only"]) - assert.Equal(t, "1", globalState["super_read_only"]) - }) - - t.Run("get global state failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - globalState, err := manager.GetGlobalState(ctx, manager.DB) - assert.Nil(t, globalState) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_GetSlaveStatus(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("query rows map failed", func(t *testing.T) { - mock.ExpectQuery("show slave status"). - WillReturnError(fmt.Errorf("some error")) - - slaveStatus, err := manager.GetSlaveStatus(ctx, manager.DB) - assert.Nil(t, slaveStatus) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("get slave status successfully", func(t *testing.T) { - mock.ExpectQuery("show slave status"). - WillReturnRows(sqlmock.NewRows([]string{"Seconds_Behind_Master", "Slave_IO_Running"}).AddRow("249904", "Yes")) - - slaveStatus, err := manager.GetSlaveStatus(ctx, manager.DB) - assert.Nil(t, err) - assert.Equal(t, "249904", slaveStatus.GetString("Seconds_Behind_Master")) - assert.Equal(t, "Yes", slaveStatus.GetString("Slave_IO_Running")) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_GetMasterStatus(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("query rows map failed", func(t *testing.T) { - mock.ExpectQuery("show master status"). - WillReturnError(fmt.Errorf("some error")) - - slaveStatus, err := manager.GetMasterStatus(ctx, manager.DB) - assert.Nil(t, slaveStatus) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("get slave status successfully", func(t *testing.T) { - mock.ExpectQuery("show master status"). - WillReturnRows(sqlmock.NewRows([]string{"File", "Executed_Gtid_Set"}).AddRow("master-bin.000002", fakeGTIDSet)) - - slaveStatus, err := manager.GetMasterStatus(ctx, manager.DB) - assert.Nil(t, err) - assert.Equal(t, "master-bin.000002", slaveStatus.GetString("File")) - assert.Equal(t, fakeGTIDSet, slaveStatus.GetString("Executed_Gtid_Set")) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsClusterInitialized(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - manager.version = "5.7.42" - manager.serverID = 1 - - t.Run("query server id failed", func(t *testing.T) { - mock.ExpectQuery("select @@global.server_id"). - WillReturnError(fmt.Errorf("some error")) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isInitialized) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("server id equal to manager's server id", func(t *testing.T) { - mock.ExpectQuery("select @@global.server_id"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.server_id"}).AddRow(1)) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.True(t, isInitialized) - assert.Nil(t, err) - }) - - t.Run("set server id failed", func(t *testing.T) { - mock.ExpectQuery("select @@global.server_id"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.server_id"}).AddRow(2)) - mock.ExpectExec("set global server_id"). - WillReturnError(fmt.Errorf("some error")) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isInitialized) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("set server id successfully", func(t *testing.T) { - manager.version = "" - const version = "5.7.42" - mock.ExpectQuery("select version()"). - WillReturnRows(sqlmock.NewRows([]string{"version"}).AddRow(version)) - mock.ExpectQuery("select @@global.server_id"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.server_id"}).AddRow(2)) - mock.ExpectExec("set global server_id"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.True(t, isInitialized) - assert.Equal(t, manager.version, version) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Promote(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - manager.globalState = map[string]string{} - manager.slaveStatus = RowMap{} - - t.Run("execute promote failed", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnError(errors.New("some error")) - // mock.ExpectExec("set global read_only=off"). - // WillReturnError(fmt.Errorf("some error")) - - err := manager.Promote(ctx, nil) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("execute promote successfully", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - mock.ExpectQuery("show slave status"). - WillReturnRows(sqlmock.NewRows([]string{"Seconds_Behind_Master"}).AddRow(0)) - mock.ExpectExec("set global read_only=off"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.Promote(ctx, nil) - assert.Nil(t, err) - }) - - t.Run("current member has been promoted", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - manager.globalState["super_read_only"] = "0" - manager.globalState["read_only"] = "0" - - err := manager.Promote(ctx, nil) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Demote(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("execute promote failed", func(t *testing.T) { - mock.ExpectExec("set global read_only=on"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Demote(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("execute promote successfully", func(t *testing.T) { - mock.ExpectExec("set global read_only=on"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.Demote(ctx) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Follow(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - _, _ = NewConfig(fakeProperties) - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - {Name: fakePodName}, - {Name: "fake-pod-2"}, - {Name: "fake-pod-1"}, - }, - } - - t.Run("cluster has no leader", func(t *testing.T) { - err := manager.Follow(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "cluster has no leader") - }) - - t.Run("i get the leader key, don't need to follow", func(t *testing.T) { - cluster.Leader = &dcs.Leader{Name: manager.CurrentMemberName} - - err := manager.Follow(ctx, cluster) - assert.Nil(t, err) - }) - - cluster.Leader = &dcs.Leader{Name: "fake-pod-1"} - t.Run("recovery conf still right", func(t *testing.T) { - manager.slaveStatus = RowMap{ - "Master_Host": CellData{ - String: "fake-pod-1", - }, - } - - err := manager.Follow(ctx, cluster) - assert.Nil(t, err) - }) - - manager.slaveStatus = RowMap{ - "Master_Host": CellData{ - String: "fake-pod-2", - }, - } - t.Run("execute follow failed", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - mock.ExpectExec("stop slave"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Follow(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("execute follow successfully", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - mock.ExpectExec("stop slave"). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectExec("SET GLOBAL rpl_semi_sync_source_timeout = 4294967295"). - WillReturnResult(sqlmock.NewResult(1, 1)) - addr := cluster.GetMemberAddrWithPort(*cluster.GetLeaderMember()) - mysqlConfig, err := mysql.ParseDSN(config.URL) - assert.Nil(t, err) - mysqlConfig.User = config.Username - mysqlConfig.Passwd = config.Password - mysqlConfig.Addr = addr - mysqlConfig.Timeout = time.Second * 5 - mysqlConfig.ReadTimeout = time.Second * 5 - mysqlConfig.WriteTimeout = time.Second * 5 - connectionPoolCache[mysqlConfig.FormatDSN()] = manager.DB - - err = manager.Follow(ctx, cluster) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_isRecoveryConfOutdated(t *testing.T) { - manager, _, _ := mockDatabase(t) - manager.slaveStatus = RowMap{} - - t.Run("slaveStatus empty", func(t *testing.T) { - outdated := manager.isRecoveryConfOutdated(fakePodName) - assert.True(t, outdated) - }) - - t.Run("slave status error", func(t *testing.T) { - manager.slaveStatus = RowMap{ - "Last_IO_Error": CellData{String: "some error"}, - } - - outdated := manager.isRecoveryConfOutdated(fakePodName) - assert.True(t, outdated) - }) -} - -func TestManager_HasOtherHealthyMembers(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - { - Name: "fake-pod-0", - }, - { - Name: "fake-pod-1", - }, - { - Name: fakePodName, - }, - }, - } - mock.ExpectQuery("select check_ts from kubeblocks.kb_health_check where type=1 limit 1"). - WillReturnError(sql.ErrNoRows) - _, _ = NewConfig(fakeProperties) - - members := manager.HasOtherHealthyMembers(ctx, cluster, "fake-pod-0") - assert.Len(t, members, 1) - assert.Equal(t, fakePodName, members[0].Name) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Lock(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("lock failed", func(t *testing.T) { - mock.ExpectExec("set global read_only=on"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Lock(ctx, "") - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - assert.False(t, manager.IsLocked) - }) - - t.Run("lock successfully", func(t *testing.T) { - mock.ExpectExec("set global read_only=on"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.Lock(ctx, "") - assert.Nil(t, err) - assert.True(t, manager.IsLocked) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Unlock(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - manager.IsLocked = true - - t.Run("unlock failed", func(t *testing.T) { - mock.ExpectExec("set global read_only=off"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Unlock(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - assert.True(t, manager.IsLocked) - }) - - t.Run("lock successfully", func(t *testing.T) { - mock.ExpectExec("set global read_only=off"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.Unlock(ctx) - assert.Nil(t, err) - assert.False(t, manager.IsLocked) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_GetDBState(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{}, - } - - t.Run("select global failed", func(t *testing.T) { - mock.ExpectQuery("select @@global.hostname"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("show master status failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - mock.ExpectQuery("show master status"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("show slave status failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - mock.ExpectQuery("show master status"). - WillReturnRows(sqlmock.NewRows([]string{"Binlog_File", "Binlog_Pos"}).AddRow("master-bin.000002", 20)) - mock.ExpectQuery("show slave status"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get op timestamp failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - mock.ExpectQuery("show master status"). - WillReturnRows(sqlmock.NewRows([]string{"Binlog_File", "Binlog_Pos"}).AddRow("master-bin.000002", 20)) - mock.ExpectQuery("show slave status"). - WillReturnRows(sqlmock.NewRows([]string{"Master_UUID", "Slave_IO_Running"}).AddRow(fakeServerUUID, "Yes")) - mock.ExpectQuery("select check_ts"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("current member is leader", func(t *testing.T) { - cluster.Leader.Name = manager.CurrentMemberName - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - mock.ExpectQuery("show master status"). - WillReturnRows(sqlmock.NewRows([]string{"File", "Pos"}).AddRow("master-bin.000002", 20)) - mock.ExpectQuery("show slave status"). - WillReturnRows(sqlmock.NewRows([]string{"Master_UUID", "Slave_IO_Running"}).AddRow(fakeServerUUID, "Yes")) - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"check_ts"}).AddRow(1)) - - dbState := manager.GetDBState(ctx, cluster) - assert.NotNil(t, dbState) - assert.Equal(t, fakePodName, dbState.Extra["hostname"]) - assert.Equal(t, "master-bin.000002", dbState.Extra["Binlog_File"]) - }) - - t.Run("current member is not leader", func(t *testing.T) { - cluster.Leader.Name = "" - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"@@global.hostname", "@@global.server_uuid", "@@global.gtid_executed", "@@global.gtid_purged", "@@global.read_only", "@@global.super_read_only"}). - AddRow(fakePodName, fakeServerUUID, fakeGTIDString, fakeGTIDSet, 1, 1)) - mock.ExpectQuery("show master status"). - WillReturnRows(sqlmock.NewRows([]string{"File", "Pos"}).AddRow("master-bin.000002", 20)) - mock.ExpectQuery("show slave status"). - WillReturnRows(sqlmock.NewRows([]string{"Master_UUID", "Slave_IO_Running"}).AddRow(fakeServerUUID, "Yes")) - mock.ExpectQuery("select"). - WillReturnRows(sqlmock.NewRows([]string{"check_ts"}).AddRow(1)) - - dbState := manager.GetDBState(ctx, cluster) - assert.NotNil(t, dbState) - assert.Equal(t, fakePodName, dbState.Extra["hostname"]) - assert.Equal(t, fakeServerUUID, dbState.Extra["Master_UUID"]) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/mysql/query.go b/pkg/lorry/engines/mysql/query.go deleted file mode 100644 index 012cb732b6c..00000000000 --- a/pkg/lorry/engines/mysql/query.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "fmt" - - "github.com/pkg/errors" -) - -func (mgr *Manager) Query(ctx context.Context, sql string) ([]byte, error) { - mgr.Logger.Info(fmt.Sprintf("query: %s", sql)) - rows, err := mgr.DB.QueryContext(ctx, sql) - if err != nil { - return nil, errors.Wrapf(err, "error executing %s", sql) - } - defer func() { - _ = rows.Close() - _ = rows.Err() - }() - result, err := jsonify(rows) - if err != nil { - return nil, errors.Wrapf(err, "error marshalling query result for %s", sql) - } - return result, nil -} - -func (mgr *Manager) Exec(ctx context.Context, sql string) (int64, error) { - mgr.Logger.Info(fmt.Sprintf("exec: %s", sql)) - res, err := mgr.DB.ExecContext(ctx, sql) - if err != nil { - return 0, errors.Wrapf(err, "error executing %s", sql) - } - return res.RowsAffected() -} diff --git a/pkg/lorry/engines/mysql/query_test.go b/pkg/lorry/engines/mysql/query_test.go deleted file mode 100644 index c8aeb877c9f..00000000000 --- a/pkg/lorry/engines/mysql/query_test.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "encoding/json" - "testing" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/go-logr/zapr" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - "go.uber.org/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -func TestQuery(t *testing.T) { - manager, mock, _ := mockDatabase(t) - - t.Run("no dbType provided", func(t *testing.T) { - rows := sqlmock.NewRows([]string{"id", "value", "timestamp"}). - AddRow(1, "value-1", time.Now()). - AddRow(2, "value-2", time.Now().Add(1000)). - AddRow(3, "value-3", time.Now().Add(2000)) - - mock.ExpectQuery("SELECT \\* FROM foo WHERE id < 4").WillReturnRows(rows) - ret, err := manager.Query(context.Background(), `SELECT * FROM foo WHERE id < 4`) - assert.Nil(t, err) - t.Logf("query result: %s", ret) - assert.Contains(t, string(ret), "\"id\":\"1") - var result []interface{} - err = json.Unmarshal(ret, &result) - assert.Nil(t, err) - assert.Equal(t, 3, len(result)) - }) - - t.Run("dbType provided", func(t *testing.T) { - col1 := sqlmock.NewColumn("id").OfType("BIGINT", 1) - col2 := sqlmock.NewColumn("value").OfType("FLOAT", 1.0) - col3 := sqlmock.NewColumn("timestamp").OfType("TIME", time.Now()) - rows := sqlmock.NewRowsWithColumnDefinition(col1, col2, col3). - AddRow(1, 1.1, time.Now()). - AddRow(2, 2.2, time.Now().Add(1000)). - AddRow(3, 3.3, time.Now().Add(2000)) - mock.ExpectQuery("SELECT \\* FROM foo WHERE id < 4").WillReturnRows(rows) - ret, err := manager.Query(context.Background(), "SELECT * FROM foo WHERE id < 4") - assert.Nil(t, err) - t.Logf("query result: %s", ret) - - // verify number - assert.Contains(t, string(ret), "\"id\":1") - assert.Contains(t, string(ret), "\"value\":2.2") - - var result []interface{} - err = json.Unmarshal(ret, &result) - assert.Nil(t, err) - assert.Equal(t, 3, len(result)) - - // verify timestamp - ts, ok := result[0].(map[string]interface{})["timestamp"].(string) - assert.True(t, ok) - var tt time.Time - tt, err = time.Parse(time.RFC3339, ts) - assert.Nil(t, err) - t.Logf("time stamp is: %v", tt) - }) -} - -func TestExec(t *testing.T) { - manager, mock, _ := mockDatabase(t) - mock.ExpectExec("INSERT INTO foo \\(id, v1, ts\\) VALUES \\(.*\\)").WillReturnResult(sqlmock.NewResult(1, 1)) - i, err := manager.Exec(context.Background(), "INSERT INTO foo (id, v1, ts) VALUES (1, 'test-1', '2021-01-22')") - assert.Equal(t, int64(1), i) - assert.Nil(t, err) -} - -func mockDatabase(t *testing.T) (*Manager, sqlmock.Sqlmock, error) { - viper.SetDefault(constant.KBEnvServiceRoles, "{\"follower\":\"Readonly\",\"leader\":\"ReadWrite\"}") - db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - - manager := &Manager{ - DBManagerBase: engines.DBManagerBase{ - CurrentMemberName: fakePodName, - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - }, - } - development, _ := zap.NewDevelopment() - manager.Logger = zapr.NewLogger(development) - manager.DB = db - - return manager, mock, err -} diff --git a/pkg/lorry/engines/mysql/semi_sync.go b/pkg/lorry/engines/mysql/semi_sync.go deleted file mode 100644 index da9f897fded..00000000000 --- a/pkg/lorry/engines/mysql/semi_sync.go +++ /dev/null @@ -1,192 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -var semiSyncMaxTimeout int = 4294967295 -var semiSyncSourceVersion string = "8.0.26" - -func (mgr *Manager) GetSemiSyncSourcePlugin() string { - plugin := "rpl_semi_sync_source" - if IsBeforeVersion(mgr.version, semiSyncSourceVersion) { - plugin = "rpl_semi_sync_master" - } - return plugin -} - -func (mgr *Manager) GetSemiSyncReplicaPlugin() string { - plugin := "rpl_semi_sync_replica" - if IsBeforeVersion(mgr.version, semiSyncSourceVersion) { - plugin = "rpl_semi_sync_slave" - } - return plugin -} - -func (mgr *Manager) EnableSemiSyncSource(ctx context.Context) error { - plugin := mgr.GetSemiSyncSourcePlugin() - var status string - sql := fmt.Sprintf("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME ='%s';", plugin) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&status) - if err != nil { - return errors.Wrapf(err, "Get %s plugin status failed", plugin) - } - - // In MySQL 8.0, semi-sync configuration options should not be specified in my.cnf, - // as this may cause the database initialization process to fail: - // [Warning] [MY-013501] [Server] Ignoring --plugin-load[_add] list as the server is running with --initialize(-insecure). - // [ERROR] [MY-000067] [Server] unknown variable 'rpl_semi_sync_master_enabled=1'. - if status != "ACTIVE" { - return errors.Errorf("plugin %s is not active: %s", plugin, status) - } - - isSemiSyncSourceEnabled, err := mgr.IsSemiSyncSourceEnabled(ctx) - if err != nil { - return err - } - if isSemiSyncSourceEnabled { - return nil - } - setSourceEnable := fmt.Sprintf("SET GLOBAL %s_enabled = 1;", plugin) - setSourceTimeout := fmt.Sprintf("SET GLOBAL %s_timeout = 0;", plugin) - _, err = mgr.DB.Exec(setSourceEnable + setSourceTimeout) - if err != nil { - return errors.Wrap(err, setSourceEnable+setSourceTimeout+" execute failed") - } - return nil -} - -func (mgr *Manager) DisableSemiSyncSource(ctx context.Context) error { - isSemiSyncSourceEnabled, err := mgr.IsSemiSyncSourceEnabled(ctx) - if err != nil { - return err - } - if !isSemiSyncSourceEnabled { - return nil - } - plugin := mgr.GetSemiSyncSourcePlugin() - setSourceDisable := fmt.Sprintf("SET GLOBAL %s_enabled = 0;", plugin) - _, err = mgr.DB.Exec(setSourceDisable) - if err != nil { - return errors.Wrap(err, setSourceDisable+" execute failed") - } - return nil -} - -func (mgr *Manager) IsSemiSyncSourceEnabled(ctx context.Context) (bool, error) { - plugin := mgr.GetSemiSyncSourcePlugin() - var value int - sql := fmt.Sprintf("select @@global.%s_enabled", plugin) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&value) - if err != nil { - return false, errors.Wrapf(err, "exec %s failed", sql) - } - return value == 1, nil -} - -func (mgr *Manager) GetSemiSyncSourceTimeout(ctx context.Context) (int, error) { - plugin := mgr.GetSemiSyncSourcePlugin() - var value int - sql := fmt.Sprintf("select @@global.%s_timeout", plugin) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&value) - if err != nil { - return 0, errors.Wrapf(err, "exec %s failed", sql) - } - return value, nil -} - -func (mgr *Manager) SetSemiSyncSourceTimeout(ctx context.Context, cluster *dcs.Cluster, leader *dcs.Member) error { - db, err := mgr.GetMemberConnection(cluster, leader) - if err != nil { - mgr.Logger.Info("Get Member conn failed", "error", err.Error()) - return err - } - - plugin := mgr.GetSemiSyncSourcePlugin() - setSourceTimeout := fmt.Sprintf("SET GLOBAL %s_timeout = %d;", plugin, semiSyncMaxTimeout) - _, err = db.Exec(setSourceTimeout) - if err != nil { - return errors.Wrap(err, setSourceTimeout+" execute failed") - } - return nil -} - -func (mgr *Manager) EnableSemiSyncReplica(ctx context.Context) error { - plugin := mgr.GetSemiSyncReplicaPlugin() - var status string - sql := fmt.Sprintf("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME ='%s';", plugin) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&status) - if err != nil { - return errors.Wrap(err, "get "+plugin+" status failed") - } - if status != "ACTIVE" { - return errors.Errorf("plugin %s is not active: %s", plugin, status) - } - - isSemiSyncReplicaEnabled, err := mgr.IsSemiSyncReplicaEnabled(ctx) - if err != nil { - return err - } - if isSemiSyncReplicaEnabled { - return nil - } - - setReplicaEnable := fmt.Sprintf("SET GLOBAL %s_enabled = 1;", plugin) - _, err = mgr.DB.Exec(setReplicaEnable) - if err != nil { - return errors.Wrap(err, setReplicaEnable+" execute failed") - } - return nil -} - -func (mgr *Manager) IsSemiSyncReplicaEnabled(ctx context.Context) (bool, error) { - plugin := mgr.GetSemiSyncReplicaPlugin() - var value int - sql := fmt.Sprintf("select @@global.%s_enabled", plugin) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&value) - if err != nil { - return false, errors.Wrapf(err, "exec %s failed", sql) - } - return value == 1, nil -} - -func (mgr *Manager) DisableSemiSyncReplica(ctx context.Context) error { - isSemiSyncReplicaEnabled, err := mgr.IsSemiSyncReplicaEnabled(ctx) - if err != nil { - return err - } - if !isSemiSyncReplicaEnabled { - return nil - } - plugin := mgr.GetSemiSyncReplicaPlugin() - setReplicaDisable := fmt.Sprintf("SET GLOBAL %s_enabled = 0;", plugin) - _, err = mgr.DB.Exec(setReplicaDisable) - if err != nil { - return errors.Wrap(err, setReplicaDisable+" execute failed") - } - return nil -} diff --git a/pkg/lorry/engines/mysql/semi_sync_test.go b/pkg/lorry/engines/mysql/semi_sync_test.go deleted file mode 100644 index dfce52c27e4..00000000000 --- a/pkg/lorry/engines/mysql/semi_sync_test.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -# This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" -) - -func TestManager_SemiSync(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - _, _ = NewConfig(fakeProperties) - - t.Run("semi sync plugin", func(t *testing.T) { - t.Run("version before 8.0.26", func(t *testing.T) { - manager.version = "5.7.42" - semiSyncSourcePlugin := manager.GetSemiSyncSourcePlugin() - semiSyncReplicaPlugin := manager.GetSemiSyncReplicaPlugin() - assert.Equal(t, semiSyncSourcePlugin, "rpl_semi_sync_master") - assert.Equal(t, semiSyncReplicaPlugin, "rpl_semi_sync_slave") - }) - - t.Run("version after 8.0.26", func(t *testing.T) { - manager.version = "8.0.30" - semiSyncSourcePlugin := manager.GetSemiSyncSourcePlugin() - semiSyncReplicaPlugin := manager.GetSemiSyncReplicaPlugin() - assert.Equal(t, semiSyncSourcePlugin, "rpl_semi_sync_source") - assert.Equal(t, semiSyncReplicaPlugin, "rpl_semi_sync_replica") - }) - }) - - t.Run("enable semi sync source", func(t *testing.T) { - t.Run("failed if plugin not load", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"})) - err := manager.EnableSemiSyncSource(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "plugin status failed") - }) - t.Run("failed if plugin not active", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("NotActive")) - err := manager.EnableSemiSyncSource(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "is not active") - }) - - t.Run("already enabled", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - err := manager.EnableSemiSyncSource(ctx) - assert.Nil(t, err) - }) - - t.Run("enable", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_source';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - mock.ExpectExec("SET GLOBAL rpl_semi_sync_source_enabled = 1;" + - "SET GLOBAL rpl_semi_sync_source_timeout = 0;"). - WillReturnResult(sqlmock.NewResult(1, 1)) - err := manager.EnableSemiSyncSource(ctx) - assert.Nil(t, err) - }) - }) - - t.Run("disable semi sync source", func(t *testing.T) { - t.Run("already disabled", func(t *testing.T) { - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - err := manager.DisableSemiSyncSource(ctx) - assert.Nil(t, err) - }) - - t.Run("disable", func(t *testing.T) { - mock.ExpectQuery("select @@global.rpl_semi_sync_source_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectExec("SET GLOBAL rpl_semi_sync_source_enabled = 0;"). - WillReturnResult(sqlmock.NewResult(1, 1)) - err := manager.DisableSemiSyncSource(ctx) - assert.Nil(t, err) - }) - }) - - t.Run("enable semi sync replica", func(t *testing.T) { - t.Run("failed if plugin not load", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"})) - err := manager.EnableSemiSyncReplica(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "status failed") - }) - t.Run("failed if plugin not active", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("NotActive")) - err := manager.EnableSemiSyncReplica(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "is not active") - }) - - t.Run("already enabled", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - err := manager.EnableSemiSyncReplica(ctx) - assert.Nil(t, err) - }) - - t.Run("enable", func(t *testing.T) { - mock.ExpectQuery("SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS " + - "WHERE PLUGIN_NAME ='rpl_semi_sync_replica';").WillReturnRows(sqlmock.NewRows([]string{"PLUGIN_STATUS"}).AddRow("ACTIVE")) - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - mock.ExpectExec("SET GLOBAL rpl_semi_sync_replica_enabled = 1;"). - WillReturnResult(sqlmock.NewResult(1, 1)) - err := manager.EnableSemiSyncReplica(ctx) - assert.Nil(t, err) - }) - }) - - t.Run("disable semi sync replica", func(t *testing.T) { - t.Run("already disabled", func(t *testing.T) { - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(0)) - err := manager.DisableSemiSyncReplica(ctx) - assert.Nil(t, err) - }) - - t.Run("disable", func(t *testing.T) { - mock.ExpectQuery("select @@global.rpl_semi_sync_replica_enabled").WillReturnRows(sqlmock.NewRows([]string{"STATUS"}).AddRow(1)) - mock.ExpectExec("SET GLOBAL rpl_semi_sync_replica_enabled = 0;"). - WillReturnResult(sqlmock.NewResult(1, 1)) - err := manager.DisableSemiSyncReplica(ctx) - assert.Nil(t, err) - }) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/mysql/suite_test.go b/pkg/lorry/engines/mysql/suite_test.go deleted file mode 100644 index e55ccbd1845..00000000000 --- a/pkg/lorry/engines/mysql/suite_test.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -var ( - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test-0") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestMysqlDBManager(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "MySQL DBManager. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock dcs store - InitMockDCSStore() -}) - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} diff --git a/pkg/lorry/engines/mysql/user.go b/pkg/lorry/engines/mysql/user.go deleted file mode 100644 index 6817621c582..00000000000 --- a/pkg/lorry/engines/mysql/user.go +++ /dev/null @@ -1,196 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "context" - "fmt" - "strings" - - "golang.org/x/exp/slices" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - superUserPriv = "SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE ON *.*" - readWritePriv = "SELECT, INSERT, UPDATE, DELETE ON *.*" - readOnlyRPriv = "SELECT ON *.*" - noPriv = "USAGE ON *.*" - - listUserSQL = "SELECT user AS userName, CASE password_expired WHEN 'N' THEN 'F' ELSE 'T' END as expired FROM mysql.user WHERE host = '%' and user <> 'root' and user not like 'kb%';" - showGrantSQL = "SHOW GRANTS FOR '%s'@'%%';" - getUserSQL = ` - SELECT user AS userName, CASE password_expired WHEN 'N' THEN 'F' ELSE 'T' END as expired - FROM mysql.user - WHERE host = '%%' and user <> 'root' and user not like 'kb%%' and user ='%s';" - ` - createUserSQL = "CREATE USER '%s'@'%%' IDENTIFIED BY '%s';" - deleteUserSQL = "DROP USER IF EXISTS '%s'@'%%';" - grantSQL = "GRANT %s TO '%s'@'%%';" - revokeSQL = "REVOKE %s FROM '%s'@'%%';" - listSystemAccountsSQL = "SELECT user AS userName FROM mysql.user WHERE host = '%' and user like 'kb%';" -) - -func (mgr *Manager) ListUsers(ctx context.Context) ([]models.UserInfo, error) { - users := []models.UserInfo{} - - err := QueryRowsMap(mgr.DB, listUserSQL, func(rMap RowMap) error { - user := models.UserInfo{ - UserName: rMap.GetString("userName"), - Expired: rMap.GetString("expired"), - } - users = append(users, user) - return nil - }) - if err != nil { - mgr.Logger.Error(err, "error executing %s") - return nil, err - } - return users, nil -} - -func (mgr *Manager) ListSystemAccounts(ctx context.Context) ([]models.UserInfo, error) { - users := []models.UserInfo{} - - err := QueryRowsMap(mgr.DB, listSystemAccountsSQL, func(rMap RowMap) error { - user := models.UserInfo{ - UserName: rMap.GetString("userName"), - } - users = append(users, user) - return nil - }) - if err != nil { - mgr.Logger.Error(err, "error executing %s") - return nil, err - } - return users, nil -} - -func (mgr *Manager) DescribeUser(ctx context.Context, userName string) (*models.UserInfo, error) { - user := &models.UserInfo{} - // only keep one role name of the highest privilege - userRoles := make([]models.RoleType, 0) - - sql := fmt.Sprintf(showGrantSQL, userName) - - err := QueryRowsMap(mgr.DB, sql, func(rMap RowMap) error { - for k, v := range rMap { - if user.UserName == "" { - user.UserName = strings.TrimPrefix(strings.TrimSuffix(k, "@%"), "Grants for ") - } - mysqlRoleType := priv2Role(strings.TrimPrefix(v.String, "GRANT ")) - userRoles = append(userRoles, mysqlRoleType) - } - - return nil - }) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return nil, err - } - - slices.SortFunc(userRoles, models.SortRoleByWeight) - if len(userRoles) > 0 { - user.RoleName = (string)(userRoles[0]) - } - return user, nil -} - -func (mgr *Manager) CreateUser(ctx context.Context, userName, password, _ string) error { - sql := fmt.Sprintf(createUserSQL, userName, password) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Info("execute sql failed", "sql", sql, "error", err.Error()) - return err - } - - return nil -} - -func (mgr *Manager) DeleteUser(ctx context.Context, userName string) error { - sql := fmt.Sprintf(deleteUserSQL, userName) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) GrantUserRole(ctx context.Context, userName, roleName string) error { - // render sql stmts - roleDesc, _ := role2Priv(roleName) - // update privilege - sql := fmt.Sprintf(grantSQL, roleDesc, userName) - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) RevokeUserRole(ctx context.Context, userName, roleName string) error { - // render sql stmts - roleDesc, _ := role2Priv(roleName) - // update privilege - sql := fmt.Sprintf(revokeSQL, roleDesc, userName) - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func role2Priv(roleName string) (string, error) { - roleType := models.String2RoleType(roleName) - switch roleType { - case models.SuperUserRole: - return superUserPriv, nil - case models.ReadWriteRole: - return readWritePriv, nil - case models.ReadOnlyRole: - return readOnlyRPriv, nil - } - return "", fmt.Errorf("role name: %s is not supported", roleName) -} - -func priv2Role(priv string) models.RoleType { - if strings.HasPrefix(priv, readOnlyRPriv) { - return models.ReadOnlyRole - } - if strings.HasPrefix(priv, readWritePriv) { - return models.ReadWriteRole - } - if strings.HasPrefix(priv, superUserPriv) { - return models.SuperUserRole - } - if strings.HasPrefix(priv, noPriv) { - return models.NoPrivileges - } - return models.CustomizedRole -} diff --git a/pkg/lorry/engines/mysql/util.go b/pkg/lorry/engines/mysql/util.go deleted file mode 100644 index a8960d1267e..00000000000 --- a/pkg/lorry/engines/mysql/util.go +++ /dev/null @@ -1,255 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "database/sql" - "encoding/json" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" -) - -const DateTimeFormat = "2006-01-02 15:04:05.999999" - -// RowMap represents one row in a result set. Its objective is to allow -// for easy, typed getters by column name. -type RowMap map[string]CellData - -// CellData is the result of a single (atomic) column in a single row -type CellData sql.NullString - -func (cd *CellData) MarshalJSON() ([]byte, error) { - if cd.Valid { - return json.Marshal(cd.String) - } else { - return json.Marshal(nil) - } -} - -// UnmarshalJSON reds this object from JSON -func (cd *CellData) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - cd.String = s - cd.Valid = true - - return nil -} - -func (cd *CellData) NullString() *sql.NullString { - return (*sql.NullString)(cd) -} - -// RowData is the result of a single row, in positioned array format -type RowData []CellData - -// MarshalJSON will marshal this map as JSON -func (rd *RowData) MarshalJSON() ([]byte, error) { - cells := make([]*CellData, len(*rd)) - for i, val := range *rd { - d := val - cells[i] = &d - } - return json.Marshal(cells) -} - -func (rd *RowData) Args() []interface{} { - result := make([]interface{}, len(*rd)) - for i := range *rd { - result[i] = *(*rd)[i].NullString() - } - return result -} - -// ResultData is an ordered row set of RowData -type ResultData []RowData -type NamedResultData struct { - Columns []string - Data ResultData -} - -var EmptyResultData = ResultData{} - -func (rm *RowMap) GetString(key string) string { - return (*rm)[key].String -} - -// GetStringD returns a string from the map, or a default value if the key does not exist -func (rm *RowMap) GetStringD(key string, def string) string { - if cell, ok := (*rm)[key]; ok { - return cell.String - } - return def -} - -func (rm *RowMap) GetInt64(key string) int64 { - res, _ := strconv.ParseInt(rm.GetString(key), 10, 0) - return res -} - -func (rm *RowMap) GetNullInt64(key string) sql.NullInt64 { - i, err := strconv.ParseInt(rm.GetString(key), 10, 0) - if err == nil { - return sql.NullInt64{Int64: i, Valid: true} - } else { - return sql.NullInt64{Valid: false} - } -} - -func (rm *RowMap) GetInt(key string) int { - res, _ := strconv.Atoi(rm.GetString(key)) - return res -} - -func (rm *RowMap) GetIntD(key string, def int) int { - res, err := strconv.Atoi(rm.GetString(key)) - if err != nil { - return def - } - return res -} - -func (rm *RowMap) GetUint(key string) uint { - res, _ := strconv.ParseUint(rm.GetString(key), 10, 0) - return uint(res) -} - -func (rm *RowMap) GetUintD(key string, def uint) uint { - res, err := strconv.Atoi(rm.GetString(key)) - if err != nil { - return def - } - return uint(res) -} - -func (rm *RowMap) GetUint64(key string) uint64 { - res, _ := strconv.ParseUint(rm.GetString(key), 10, 0) - return res -} - -func (rm *RowMap) GetUint64D(key string, def uint64) uint64 { - res, err := strconv.ParseUint(rm.GetString(key), 10, 0) - if err != nil { - return def - } - return res -} - -func (rm *RowMap) GetBool(key string) bool { - return rm.GetInt(key) != 0 -} - -func (rm *RowMap) GetTime(key string) time.Time { - if t, err := time.Parse(DateTimeFormat, rm.GetString(key)); err == nil { - return t - } - return time.Time{} -} - -func RowToArray(rows *sql.Rows, columns []string) []CellData { - buff := make([]interface{}, len(columns)) - data := make([]CellData, len(columns)) - for i := range buff { - buff[i] = data[i].NullString() - } - _ = rows.Scan(buff...) - return data -} - -// ScanRowsToArrays is a convenience function, typically not called directly, which maps rows -// already read from the database into arrays of NullString -func ScanRowsToArrays(rows *sql.Rows, onRow func([]CellData) error) error { - columns, _ := rows.Columns() - for rows.Next() { - arr := RowToArray(rows, columns) - err := onRow(arr) - if err != nil { - return err - } - } - return nil -} - -func rowToMap(row []CellData, columns []string) map[string]CellData { - m := make(map[string]CellData) - for k, dataCol := range row { - m[columns[k]] = dataCol - } - return m -} - -// ScanRowsToMaps is a convenience function, typically not called directly, which maps rows -// already read from the database into RowMap entries. -func ScanRowsToMaps(rows *sql.Rows, onRow func(RowMap) error) error { - columns, _ := rows.Columns() - err := ScanRowsToArrays(rows, func(arr []CellData) error { - m := rowToMap(arr, columns) - err := onRow(m) - if err != nil { - return err - } - return nil - }) - return err -} - -// QueryRowsMap is a convenience function allowing querying a result set while providing a callback -// function activated per read row. -func QueryRowsMap(db *sql.DB, query string, onRow func(RowMap) error, args ...interface{}) (err error) { - var rows *sql.Rows - rows, err = db.Query(query, args...) - if rows != nil { - defer rows.Close() - } - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - err = ScanRowsToMaps(rows, onRow) - return -} - -func VersionParts(version string) []string { - return strings.Split(version, ".") -} - -func IsBeforeVersion(version string, otherVersion string) bool { - thisVersions := VersionParts(version) - otherVersions := VersionParts(otherVersion) - if len(thisVersions) < len(otherVersions) { - return false - } - - for i := 0; i < len(thisVersions); i++ { - thisToken, _ := strconv.Atoi(thisVersions[i]) - otherToken, _ := strconv.Atoi(otherVersions[i]) - if thisToken < otherToken { - return true - } - if thisToken > otherToken { - return false - } - } - return false -} diff --git a/pkg/lorry/engines/mysql/util_test.go b/pkg/lorry/engines/mysql/util_test.go deleted file mode 100644 index 44ab59180c9..00000000000 --- a/pkg/lorry/engines/mysql/util_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package mysql - -import ( - "database/sql" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const ( - fakeCellDataString = "fake-cell-data" -) - -func TestCellData_MarshalJSON(t *testing.T) { - testCases := []struct { - fakeString string - valid bool - expectRet string - expectError error - }{ - {fakeCellDataString, true, `"fake-cell-data"`, nil}, - {fakeCellDataString, false, "null", nil}, - } - - for _, testCase := range testCases { - fakeCellData := &CellData{ - String: testCase.fakeString, - Valid: testCase.valid, - } - - ret, err := fakeCellData.MarshalJSON() - assert.ErrorIs(t, err, testCase.expectError) - assert.Equal(t, testCase.expectRet, string(ret)) - } -} - -func TestCellData_UnmarshalJSON(t *testing.T) { - testCases := []struct { - fakeJSON string - expectErrorMsg string - expectCellDataString string - }{ - {`"fake"`, "", `fake`}, - {`{"data": "test"}`, "json: cannot unmarshal object into Go value of type string", ""}, - } - - for _, testCase := range testCases { - fakeCellData := &CellData{} - - err := fakeCellData.UnmarshalJSON([]byte(testCase.fakeJSON)) - assert.Equal(t, testCase.expectErrorMsg == "", err == nil) - if err != nil { - assert.ErrorContains(t, err, testCase.expectErrorMsg) - } - assert.Equal(t, testCase.expectCellDataString, fakeCellData.String) - } -} - -func TestRowData(t *testing.T) { - fakeRowData := &RowData{ - { - String: fakeCellDataString, - Valid: true, - }, - } - - ret, err := fakeRowData.MarshalJSON() - assert.Nil(t, err) - assert.Equal(t, `["fake-cell-data"]`, string(ret)) - - args := fakeRowData.Args() - cellData, ok := args[0].(sql.NullString) - assert.True(t, ok) - assert.Equal(t, sql.NullString{ - String: fakeCellDataString, - Valid: true, - }, cellData) -} - -func TestRowMap(t *testing.T) { - fakeRowMap := &RowMap{ - "fake": CellData{ - String: DateTimeFormat, - Valid: true, - }, - "num": CellData{ - String: "20", - }, - } - - getStringDTestCases := []struct { - key string - ret string - timeRet time.Time - }{ - {"fake", DateTimeFormat, time.Date(2006, time.January, 2, 15, 4, 5, 999999000, time.UTC)}, - {"test", "test", time.Time{}}, - } - for _, testCase := range getStringDTestCases { - assert.Equal(t, testCase.ret, fakeRowMap.GetStringD(testCase.key, "test")) - assert.Equal(t, testCase.timeRet, fakeRowMap.GetTime(testCase.key)) - } - - assert.Equal(t, int64(20), fakeRowMap.GetInt64("num")) - assert.Equal(t, 20, fakeRowMap.GetInt("num")) - assert.False(t, fakeRowMap.GetBool("fake")) - assert.Equal(t, uint(20), fakeRowMap.GetUint("num")) - assert.Equal(t, uint64(20), fakeRowMap.GetUint64("num")) - - getNullInt64TestCases := []struct { - key string - nullRet sql.NullInt64 - intDRet int - uintDRet uint - uint64DRet uint64 - }{ - {"num", sql.NullInt64{Int64: 20, Valid: true}, 20, uint(20), uint64(20)}, - {"test", sql.NullInt64{Valid: false}, 30, uint(30), uint64(30)}, - } - for _, testCase := range getNullInt64TestCases { - assert.Equal(t, testCase.nullRet, fakeRowMap.GetNullInt64(testCase.key)) - assert.Equal(t, testCase.intDRet, fakeRowMap.GetIntD(testCase.key, 30)) - assert.Equal(t, testCase.uintDRet, fakeRowMap.GetUintD(testCase.key, 30)) - assert.Equal(t, testCase.uint64DRet, fakeRowMap.GetUint64D(testCase.key, 30)) - } - -} diff --git a/pkg/lorry/engines/nebula/commands.go b/pkg/lorry/engines/nebula/commands.go deleted file mode 100644 index 4b0bbcf7183..00000000000 --- a/pkg/lorry/engines/nebula/commands.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package nebula - -import ( - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "nebula-console", - Container: "nebula-console", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# nebula client connection example -nebula --addr %s --port %s --user %s -port%s -`, info.Host, info.Port, info.User, info.Password) - }, - }, - } -} - -func (r *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := "root" - userPass := "nebula" - - if connectInfo != nil { - userName = connectInfo.UserName - userPass = connectInfo.UserPasswd - } - - nebulaCmd := []string{fmt.Sprintf("%s --addr $GRAPHD_SVC_NAME --port $GRAPHD_SVC_PORT --user %s --password %s", r.info.Client, userName, engines.AddSingleQuote(userPass))} - - return []string{"sh", "-c", strings.Join(nebulaCmd, " ")} -} - -func (r *Commands) Container() string { - return r.info.Container -} - -func (r *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, r.examples) -} - -func (r *Commands) ExecuteCommand([]string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("%s not implemented", r.info.Client) -} diff --git a/pkg/lorry/engines/nebula/commands_test.go b/pkg/lorry/engines/nebula/commands_test.go deleted file mode 100644 index 7c931c8042f..00000000000 --- a/pkg/lorry/engines/nebula/commands_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package nebula - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Nebula Engine", func() { - It("connection command", func() { - nebula := NewCommands() - - Expect(nebula.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(nebula.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - nebula := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range nebula.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(nebula.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(nebula.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/nebula/suite_test.go b/pkg/lorry/engines/nebula/suite_test.go deleted file mode 100644 index b03c027af5d..00000000000 --- a/pkg/lorry/engines/nebula/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package nebula - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "nebula Suite") -} diff --git a/pkg/lorry/engines/oceanbase/commands.go b/pkg/lorry/engines/oceanbase/commands.go deleted file mode 100644 index f04926d1402..00000000000 --- a/pkg/lorry/engines/oceanbase/commands.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "fmt" - "strconv" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "mysql", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# oceanbase client connection example -mysql -h %s -P $COMP_MYSQL_PORT -u %s -`, info.Host, info.User) - }, - }, - } -} - -func (r *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := "root" - userPass := "" - - if connectInfo != nil { - userName = connectInfo.UserName - userPass = connectInfo.UserPasswd - } - - var obCmd []string - - if userPass != "" { - obCmd = []string{fmt.Sprintf("%s -h127.0.0.1 -P $OB_SERVICE_PORT -u%s -A -p%s", r.info.Client, userName, engines.AddSingleQuote(userPass))} - } else { - obCmd = []string{fmt.Sprintf("%s -h127.0.0.1 -P $OB_SERVICE_PORT -u%s -A", r.info.Client, userName)} - } - - return []string{"bash", "-c", strings.Join(obCmd, " ")} -} - -func (r *Commands) Container() string { - return r.info.Container -} - -func (r *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, r.examples) -} - -func (r *Commands) ExecuteCommand(scripts []string) ([]string, []corev1.EnvVar, error) { - cmd := []string{} - cmd = append(cmd, "/bin/bash", "-c", "-ex") - if engines.EnvVarMap[engines.PASSWORD] == "" { - cmd = append(cmd, fmt.Sprintf("%s -h127.0.0.1 -P $COMP_MYSQL_PORT -u%s -e %s", r.info.Client, engines.EnvVarMap[engines.USER], strconv.Quote(strings.Join(scripts, " ")))) - } else { - cmd = append(cmd, fmt.Sprintf("%s -h127.0.0.1 -P $COMP_MYSQL_PORT -u%s -p%s -e %s", r.info.Client, - fmt.Sprintf("$%s", engines.EnvVarMap[engines.USER]), - fmt.Sprintf("$%s", engines.EnvVarMap[engines.PASSWORD]), - strconv.Quote(strings.Join(scripts, " ")))) - } - - return cmd, nil, nil -} diff --git a/pkg/lorry/engines/oceanbase/commands_test.go b/pkg/lorry/engines/oceanbase/commands_test.go deleted file mode 100644 index 1c948620567..00000000000 --- a/pkg/lorry/engines/oceanbase/commands_test.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Oceanbase Engine", func() { - It("connection command", func() { - oceanbase := NewCommands() - - Expect(oceanbase.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(oceanbase.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - oceanbase := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range oceanbase.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(oceanbase.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(oceanbase.ConnectExample(info, "")).ShouldNot(BeZero()) - }) - - It("execute command", func() { - oceanbase := NewCommands() - - cmd, _, err := oceanbase.ExecuteCommand(nil) - Expect(err).Should(BeNil()) - Expect(cmd).ShouldNot(BeNil()) - engines.EnvVarMap[engines.PASSWORD] = "" - cmd, _, err = oceanbase.ExecuteCommand(nil) - Expect(err).Should(BeNil()) - Expect(cmd).ShouldNot(BeNil()) - }) -}) diff --git a/pkg/lorry/engines/oceanbase/config.go b/pkg/lorry/engines/oceanbase/config.go deleted file mode 100644 index 5aa2dbf01ed..00000000000 --- a/pkg/lorry/engines/oceanbase/config.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "database/sql" - "strings" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - mysqlengine "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -type Config struct { - *mysqlengine.Config -} - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - mysqlConfig, err := mysqlengine.NewConfig(properties) - if err != nil { - return nil, err - } - if mysqlConfig.Username == "" { - mysqlConfig.Username = "root" - } - - config = &Config{ - Config: mysqlConfig, - } - return config, nil -} - -func getRootPassword(compName string) string { - rootPasswordEnv := "OB_ROOT_PASSWD" - - // to support different root password for different components - rootPasswordEnvPerCmp := "" - if compName == "" { - compName = viper.GetString("KB_COMP_NAME") - } - if compName != "" { - compName = strings.ToUpper(compName) - compName = strings.ReplaceAll(compName, "-", "_") - rootPasswordEnvPerCmp = rootPasswordEnv + "_" + compName - } - - if viper.IsSet(rootPasswordEnvPerCmp) { - rootPasswordEnv = rootPasswordEnvPerCmp - } - - return viper.GetString(rootPasswordEnv) -} - -func (config *Config) GetMemberRootDBConn(cluster *dcs.Cluster, member *dcs.Member) (*sql.DB, error) { - addr := cluster.GetMemberAddrWithPort(*member) - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return nil, errors.Wrapf(err, "illegal Data Source Name (DNS) specified by %s", config.URL) - } - mysqlConfig.User = config.Username - mysqlConfig.Passwd = getRootPassword(member.ComponentName) - mysqlConfig.Addr = addr - mysqlConfig.Timeout = time.Second * 5 - mysqlConfig.ReadTimeout = time.Second * 5 - mysqlConfig.WriteTimeout = time.Second * 5 - db, err := mysqlengine.GetDBConnection(mysqlConfig.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "get DB connection failed") - } - - return db, nil -} diff --git a/pkg/lorry/engines/oceanbase/config_test.go b/pkg/lorry/engines/oceanbase/config_test.go deleted file mode 100644 index 1069d3f6354..00000000000 --- a/pkg/lorry/engines/oceanbase/config_test.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var ( - fakeProperties = engines.Properties{ - "url": "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true", - "maxOpenConns": "5", - } - fakePropertiesWithWrongPem = engines.Properties{ - "pemPath": "fake-path", - } -) - -func TestNewConfig(t *testing.T) { - t.Run("new config failed", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithWrongPem) - - assert.Nil(t, fakeConfig) - assert.NotNil(t, err) - }) - - t.Run("new config successfully", func(t *testing.T) { - fakeConfig, err := NewConfig(fakeProperties) - - assert.NotNil(t, fakeConfig) - assert.Nil(t, err) - }) -} diff --git a/pkg/lorry/engines/oceanbase/conn.go b/pkg/lorry/engines/oceanbase/conn.go deleted file mode 100644 index 3cfbef3922f..00000000000 --- a/pkg/lorry/engines/oceanbase/conn.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "database/sql" - "fmt" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - mysqlengine "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -// GetDBConnWithMember retrieves a database connection for a specific member of a cluster. -func (mgr *Manager) GetDBConnWithMember(cluster *dcs.Cluster, member *dcs.Member) (db *sql.DB, err error) { - if member != nil && member.Name != mgr.CurrentMemberName { - addr := cluster.GetMemberAddrWithPort(*member) - db, err = config.GetDBConnWithAddr(addr) - if err != nil { - return nil, errors.Wrap(err, "new db connection failed") - } - } else { - db = mgr.DB - } - return db, nil -} - -func (mgr *Manager) GetMySQLDBConn() (*sql.DB, error) { - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return nil, errors.Wrapf(err, "illegal Data Source Name (DNS) specified by %s", config.URL) - } - mysqlConfig.User = fmt.Sprintf("%s@%s", "root", mgr.ReplicaTenant) - mysqlConfig.Passwd = "" - db, err := mysqlengine.GetDBConnection(mysqlConfig.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "get DB connection failed") - } - - return db, nil -} - -func (mgr *Manager) GetMySQLDBConnWithAddr(addr string) (*sql.DB, error) { - mysqlConfig, err := mysql.ParseDSN(config.URL) - if err != nil { - return nil, errors.Wrapf(err, "illegal Data Source Name (DNS) specified by %s", config.URL) - } - mysqlConfig.User = fmt.Sprintf("%s@%s", "root", mgr.ReplicaTenant) - // mysqlConfig.Passwd = config.Password - mysqlConfig.Passwd = "" - mysqlConfig.Addr = addr - db, err := mysqlengine.GetDBConnection(mysqlConfig.FormatDSN()) - if err != nil { - return nil, errors.Wrap(err, "get DB connection failed") - } - - return db, nil -} diff --git a/pkg/lorry/engines/oceanbase/get_replica_role.go b/pkg/lorry/engines/oceanbase/get_replica_role.go deleted file mode 100644 index 8a183036e60..00000000000 --- a/pkg/lorry/engines/oceanbase/get_replica_role.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - return mgr.GetReplicaRoleForMember(ctx, cluster, nil) -} - -func (mgr *Manager) GetReplicaRoleForMember(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (string, error) { - if mgr.ReplicaTenant == "" { - mgr.Logger.V(1).Info("the cluster has no replica tenant set") - return "", nil - } - - var zoneCount int - zoneSQL := `select count(distinct(zone)) as count from oceanbase.__all_zone where zone!=''` - err := mgr.DB.QueryRowContext(ctx, zoneSQL).Scan(&zoneCount) - if err != nil { - mgr.Logger.Info("query zone info failed", "error", err) - return "", err - } - - if zoneCount > 1 { - mgr.Logger.Info("zone count is more than 1, return no role", "zone count", zoneCount) - return "", nil - } - - sql := fmt.Sprintf("SELECT TENANT_ROLE FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='%s'", mgr.ReplicaTenant) - - db := mgr.DB - if member != nil && member.Name != mgr.CurrentMemberName { - db, err = config.GetMemberRootDBConn(cluster, member) - if err != nil { - return "", errors.Wrap(err, "new db connection failed") - } - } - - rows, err := db.QueryContext(ctx, sql) - if err != nil { - mgr.Logger.Info("error executing", "sql", sql, "error", err.Error()) - return "", errors.Wrapf(err, "error executing %s", sql) - } - - defer func() { - _ = rows.Close() - _ = rows.Err() - }() - - var role string - var isReady bool - for rows.Next() { - if err = rows.Scan(&role); err != nil { - mgr.Logger.Info("Role query failed", "error", err.Error()) - return role, err - } - isReady = true - } - if isReady { - return role, nil - } - mgr.Logger.Info("no data returned", "sql", sql) - return "", nil -} diff --git a/pkg/lorry/engines/oceanbase/get_replica_role_test.go b/pkg/lorry/engines/oceanbase/get_replica_role_test.go deleted file mode 100644 index 016d28461cf..00000000000 --- a/pkg/lorry/engines/oceanbase/get_replica_role_test.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "context" - "fmt" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" - "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const ( - fakePodName = "test-ob-0" - fakeClusterCompName = "test-ob" - fakeNamespace = "fake-namespace" -) - -func mockDatabase(t *testing.T) (*Manager, sqlmock.Sqlmock, error) { - manager := &Manager{ - Manager: mysql.Manager{ - DBManagerBase: engines.DBManagerBase{ - CurrentMemberName: fakePodName, - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - Logger: ctrl.Log.WithName("ob-TEST"), - }, - }, - } - - manager.ReplicaTenant = viperx.GetString("TENANT_NAME") - db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - manager.DB = db - - return manager, mock, err -} - -func TestGetRole(t *testing.T) { - ctx := context.TODO() - viperx.SetDefault("TENANT_NAME", "alice") - manager, mock, _ := mockDatabase(t) - - t.Run("error executing sql", func(t *testing.T) { - mock.ExpectQuery(`select count\(distinct\(zone\)\) as count from oceanbase.__all_zone where zone!=''`). - WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) - mock.ExpectQuery("SELECT TENANT_ROLE FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='alice'"). - WillReturnError(fmt.Errorf("some error")) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "", role) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("scan rows failed", func(t *testing.T) { - mock.ExpectQuery(`select count\(distinct\(zone\)\) as count from oceanbase.__all_zone where zone!=''`). - WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) - mock.ExpectQuery("SELECT TENANT_ROLE FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='alice'"). - WillReturnRows(sqlmock.NewRows([]string{"TENANT_ROLE"}).AddRow(PRIMARY)) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, PRIMARY, role) - assert.Nil(t, err) - }) - - t.Run("no data returned", func(t *testing.T) { - mock.ExpectQuery(`select count\(distinct\(zone\)\) as count from oceanbase.__all_zone where zone!=''`). - WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) - mock.ExpectQuery("SELECT TENANT_ROLE FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='alice'"). - WillReturnRows(sqlmock.NewRows([]string{"ROLE"})) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "", role) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/oceanbase/manager.go b/pkg/lorry/engines/oceanbase/manager.go deleted file mode 100644 index 2eeeb853f5b..00000000000 --- a/pkg/lorry/engines/oceanbase/manager.go +++ /dev/null @@ -1,454 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "context" - "database/sql" - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -const ( - Role = "ROLE" - CurrentLeader = "CURRENT_LEADER" - PRIMARY = "PRIMARY" - STANDBY = "STANDBY" - - repUser = "rep_user" - repPassword = "rep_user" - normalStatus = "NORMAL" - MYSQL = "MYSQL" - ORACLE = "ORACLE" -) - -type Manager struct { - mysql.Manager - ReplicaTenant string - CompatibilityMode string - Members []dcs.Member - MaxLag int64 -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("Oceanbase") - config, err := NewConfig(properties) - if err != nil { - return nil, err - } - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - - db, err := config.GetLocalDBConn() - if err != nil { - return nil, errors.Wrap(err, "connect to Oceanbase failed") - } - - mgr := &Manager{ - Manager: mysql.Manager{ - DBManagerBase: *managerBase, - DB: db, - }, - } - mgr.ReplicaTenant = viper.GetString("TENANT_NAME") - if mgr.ReplicaTenant == "" { - return nil, errors.New("replica tenant is not set") - } - return mgr, nil -} - -func (mgr *Manager) IsClusterInitialized(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - time.Sleep(120 * time.Second) - return true, nil -} - -func (mgr *Manager) InitializeCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) IsLeader(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - return mgr.IsLeaderMember(ctx, cluster, nil) -} - -func (mgr *Manager) IsLeaderMember(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, error) { - role, err := mgr.GetReplicaRoleForMember(ctx, cluster, member) - - if err != nil { - return false, err - } - - if strings.EqualFold(role, PRIMARY) { - return true, nil - } - - return false, nil -} - -func (mgr *Manager) HasOtherHealthyLeader(ctx context.Context, cluster *dcs.Cluster) *dcs.Member { - isLeader, err := mgr.IsLeader(ctx, cluster) - if err == nil && isLeader { - // if current member is leader, just return - return nil - } - - for _, member := range cluster.Members { - if member.Name == mgr.CurrentMemberName { - continue - } - - isLeader, err := mgr.IsLeaderMember(ctx, cluster, &member) - if err == nil && isLeader { - return &member - } - } - - return nil -} - -func (mgr *Manager) GetCompatibilityMode(ctx context.Context) (string, error) { - if mgr.CompatibilityMode != "" { - return mgr.CompatibilityMode, nil - } - sql := fmt.Sprintf("SELECT COMPATIBILITY_MODE FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='%s'", mgr.ReplicaTenant) - err := mgr.DB.QueryRowContext(ctx, sql).Scan(&mgr.CompatibilityMode) - if err != nil { - return "", errors.Wrap(err, "query compatibility mode failed") - } - return mgr.CompatibilityMode, nil -} - -func (mgr *Manager) MemberHealthyCheck(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) error { - compatibilityMode, err := mgr.GetCompatibilityMode(ctx) - if err != nil { - return errors.Wrap(err, "compatibility mode unknown") - } - switch compatibilityMode { - case MYSQL: - return mgr.HealthyCheckForMySQLMode(ctx, cluster, member) - case ORACLE: - return mgr.HealthyCheckForOracleMode(ctx, cluster, member) - default: - return errors.Errorf("compatibility mode not supported: [%s]", compatibilityMode) - } -} - -func (mgr *Manager) IsCurrentMemberHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - err := mgr.CurrentMemberHealthyCheck(ctx, cluster) - if err != nil { - mgr.Logger.Info("current member is unhealthy", "error", err.Error()) - return false - } - return true -} - -func (mgr *Manager) CurrentMemberHealthyCheck(ctx context.Context, cluster *dcs.Cluster) error { - member := cluster.GetMemberWithName(mgr.CurrentMemberName) - return mgr.MemberHealthyCheck(ctx, cluster, member) -} - -func (mgr *Manager) LeaderHealthyCheck(ctx context.Context, cluster *dcs.Cluster) error { - members := cluster.Members - for _, member := range members { - if isLeader, _ := mgr.IsLeaderMember(ctx, cluster, &member); isLeader { - return mgr.MemberHealthyCheck(ctx, cluster, &member) - } - } - - return errors.New("no leader found") -} - -func (mgr *Manager) HealthyCheckForMySQLMode(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) error { - isLeader, err := mgr.IsLeaderMember(ctx, cluster, member) - if err != nil { - return err - } - addr := cluster.GetMemberAddrWithPort(*member) - db, err := mgr.GetMySQLDBConnWithAddr(addr) - if err != nil { - return err - } - if isLeader { - err = mgr.WriteCheck(ctx, db) - if err != nil { - return err - } - } - err = mgr.ReadCheck(ctx, db) - if err != nil { - return err - } - - return nil -} - -func (mgr *Manager) WriteCheck(ctx context.Context, db *sql.DB) error { - writeSQL := fmt.Sprintf(`BEGIN; -CREATE DATABASE IF NOT EXISTS kubeblocks; -CREATE TABLE IF NOT EXISTS kubeblocks.kb_health_check(type INT, check_ts BIGINT, PRIMARY KEY(type)); -INSERT INTO kubeblocks.kb_health_check VALUES(%d, UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE check_ts = UNIX_TIMESTAMP(); -COMMIT;`, engines.CheckStatusType) - opTimestamp, _ := mgr.GetOpTimestamp(ctx, db) - if opTimestamp != 0 { - // if op timestamp is not 0, it means the table is ready created - writeSQL = fmt.Sprintf(` - INSERT INTO kubeblocks.kb_health_check VALUES(%d, UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE check_ts = UNIX_TIMESTAMP(); - `, engines.CheckStatusType) - } - _, err := db.ExecContext(ctx, writeSQL) - if err != nil { - return errors.Wrap(err, "Write check failed") - } - return nil -} - -func (mgr *Manager) HealthyCheckForOracleMode(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) error { - // there is no golang driver for oceanbase oracle mode, so use mysql client to check - isLeader, err := mgr.IsLeaderMember(ctx, cluster, member) - if err != nil { - return err - } - mgr.Logger.Info("check member", "isLeader", isLeader) - // lorry has no mysql client - // if isLeader { - // cmd := []string{"mysql", "-h", member.PodIP, "-P", member.DBPort, "-u", "SYS@" + mgr.ReplicaTenant, "-e", "SELECT t.table_name tablename FROM user_tables t WHERE table_name = 'KB_HEALTH_CHECK'"} - // output, err := util.ExecCommand(ctx, cmd, os.Environ()) - // if err != nil { - // return errors.Wrap(err, "check table failed") - // } - // if !strings.Contains(output, "KB_HEALTH_CHECK") { - // sql := "create table kb_health_check (type int primary key, check_ts NUMBER);" - // sql += fmt.Sprintf("INSERT INTO kb_health_check (type, check_ts) VALUES (1, %d);", time.Now().Unix()) - // sql += "commit;" - // cmd = []string{"mysql", "-h", member.PodIP, "-P", member.DBPort, "-u", "SYS@" + mgr.ReplicaTenant, "-e", sql} - // _, err = util.ExecCommand(ctx, cmd, os.Environ()) - // if err != nil { - // return errors.Wrap(err, "create table failed") - // } - // } - // sql := fmt.Sprintf("UPDATE kb_health_check SET check_ts = %d WHERE type=1;", time.Now().Unix()) - // sql += "commit;" - // cmd = []string{"mysql", "-h", member.PodIP, "-P", member.DBPort, "-u", "SYS@" + mgr.ReplicaTenant, "-e", sql} - // _, err = util.ExecCommand(ctx, cmd, os.Environ()) - // if err != nil { - // return errors.Wrap(err, "create table failed") - // } - // } - - // sql := "SELECT check_ts from kb_health_check WHERE type=1;" - // cmd := []string{"mysql", "-h", member.PodIP, "-P", member.DBPort, "-u", "SYS@" + mgr.ReplicaTenant, "-e", sql} - // _, err = util.ExecCommand(ctx, cmd, os.Environ()) - // if err != nil { - // return errors.Wrap(err, "create table failed") - // } - return nil -} - -func (mgr *Manager) IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool { - err := mgr.MemberHealthyCheck(ctx, cluster, member) - if err != nil { - mgr.Logger.Info("member is unhealthy", "error", err.Error()) - return false - } - return true -} - -func (mgr *Manager) IsMemberLagging(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, int64) { - var leaderOpTimestamp int64 - if cluster.Leader == nil || cluster.Leader.DBState == nil { - mgr.Logger.Info("leader's db state is nil, maybe leader is not ready yet") - return false, 0 - } - leaderOpTimestamp = cluster.Leader.DBState.OpTimestamp - if leaderOpTimestamp == 0 { - mgr.Logger.Info("leader's op timestamp is 0") - return true, 0 - } - - opTimestamp, err := mgr.GetMemberOpTimestamp(ctx, cluster, member) - if err != nil { - mgr.Logger.Info("get op timestamp failed", "error", err.Error()) - return true, 0 - } - lag := leaderOpTimestamp - opTimestamp - if lag > mgr.MaxLag { - mgr.Logger.Info("member is lagging", "opTimestamp", opTimestamp, "leaderOpTimestamp", leaderOpTimestamp) - return true, lag - } - return false, lag -} - -func (mgr *Manager) GetDBState(ctx context.Context, cluster *dcs.Cluster) *dcs.DBState { - mgr.DBState = nil - member := cluster.GetMemberWithName(mgr.CurrentMemberName) - opTimestamp, err := mgr.GetMemberOpTimestamp(ctx, cluster, member) - if err != nil { - mgr.Logger.Info("get op timestamp failed", "error", err) - return nil - } - mgr.DBState = &dcs.DBState{ - OpTimestamp: opTimestamp, - } - return mgr.DBState -} - -func (mgr *Manager) GetMemberOpTimestamp(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (int64, error) { - compatibilityMode, err := mgr.GetCompatibilityMode(ctx) - if err != nil { - return 0, errors.Wrap(err, "compatibility mode unknown") - } - if compatibilityMode == ORACLE { - sql := "SELECT check_ts from kb_health_check WHERE type=1;" - cmd := []string{"mysql", "-h", member.PodIP, "-P", member.DBPort, "-u", "SYS@" + mgr.ReplicaTenant, "-e", sql} - output, err := util.ExecCommand(ctx, cmd, os.Environ()) - if err != nil { - return 0, errors.Wrap(err, "get timestamp failed") - } - stimeStamp := strings.Split(output, "\n") - if len(stimeStamp) < 2 { - return 0, nil - } - return strconv.ParseInt(stimeStamp[1], 10, 64) - } - addr := cluster.GetMemberAddrWithPort(*member) - db, err := mgr.GetMySQLDBConnWithAddr(addr) - if err != nil { - mgr.Logger.Info("get db connection failed", "error", err.Error()) - return 0, err - } - return mgr.GetOpTimestamp(ctx, db) -} - -func (mgr *Manager) Promote(ctx context.Context, cluster *dcs.Cluster) error { - db := mgr.DB - isLeader, err := mgr.IsLeader(ctx, nil) - if err != nil { - return errors.Wrap(err, "leader check failed") - } - if isLeader { - return nil - } - // if there is no switchover, it's a failover: old leader is down, we need to promote a new leader, and the old leader can't be used anymore. - primaryTenant := "ALTER SYSTEM ACTIVATE STANDBY TENANT = " + mgr.ReplicaTenant - if cluster.Switchover != nil { - // it's a manual switchover - mgr.Logger.Info("manual switchover") - primaryTenant = "ALTER SYSTEM SWITCHOVER TO PRIMARY TENANT = " + mgr.ReplicaTenant - } else { - mgr.Logger.Info("unexpected switchover, promote to primary directly") - } - - _, err = db.Exec(primaryTenant) - if err != nil { - mgr.Logger.Info("activate standby tenant failed", "error", err) - return err - } - - var tenantRole, roleStatus string - queryTenant := fmt.Sprintf("SELECT TENANT_ROLE, SWITCHOVER_STATUS FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='%s'", mgr.ReplicaTenant) - for { - err := db.QueryRowContext(ctx, queryTenant).Scan(&tenantRole, &roleStatus) - if err != nil { - return errors.Wrap(err, "query tenant role failed") - } - - if tenantRole == PRIMARY && roleStatus == normalStatus { - break - } - time.Sleep(time.Second) - } - - return nil -} - -func (mgr *Manager) Demote(ctx context.Context) error { - db := mgr.DB - standbyTenant := "ALTER SYSTEM SWITCHOVER TO STANDBY TENANT = " + mgr.ReplicaTenant - _, err := db.Exec(standbyTenant) - if err != nil { - return errors.Wrap(err, "standby primary tenant failed") - } - - var tenantRole, roleStatus string - queryTenant := fmt.Sprintf("SELECT TENANT_ROLE, SWITCHOVER_STATUS FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='%s'", mgr.ReplicaTenant) - for { - err := db.QueryRowContext(ctx, queryTenant).Scan(&tenantRole, &roleStatus) - if err != nil { - return errors.Wrap(err, "query tenant role failed") - } - - if tenantRole == STANDBY && roleStatus == normalStatus { - break - } - time.Sleep(time.Second) - } - - return nil -} - -func (mgr *Manager) Follow(ctx context.Context, cluster *dcs.Cluster) error { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - return errors.New("no leader found") - } - sourceAddr := leaderMember.PodIP + ":" + leaderMember.DBPort - db := mgr.DB - - sql := fmt.Sprintf("ALTER SYSTEM SET LOG_RESTORE_SOURCE = 'SERVICE=%s USER=%s@%s PASSWORD=%s' TENANT = %s", - sourceAddr, repUser, mgr.ReplicaTenant, repPassword, mgr.ReplicaTenant) - _, err := db.Exec(sql) - if err != nil { - mgr.Logger.Info(sql+" failed", "error", err) //nolint:goconst - return err - } - - time.Sleep(time.Second) - var scn int64 - queryTenant := fmt.Sprintf("SELECT RECOVERY_UNTIL_SCN FROM oceanbase.DBA_OB_TENANTS where TENANT_NAME='%s'", mgr.ReplicaTenant) - for { - err := db.QueryRowContext(ctx, queryTenant).Scan(&scn) - if err != nil { - mgr.Logger.Info("query zone info failed", "error", err) - return err - } - - if scn == 4611686018427387903 { - break - } - time.Sleep(time.Second) - } - return nil -} diff --git a/pkg/lorry/engines/oceanbase/suite_test.go b/pkg/lorry/engines/oceanbase/suite_test.go deleted file mode 100644 index 94b55875798..00000000000 --- a/pkg/lorry/engines/oceanbase/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oceanbase - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "oceanbase Suite") -} diff --git a/pkg/lorry/engines/opengauss/commands.go b/pkg/lorry/engines/opengauss/commands.go deleted file mode 100644 index 14edec371eb..00000000000 --- a/pkg/lorry/engines/opengauss/commands.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package opengauss - -import ( - "fmt" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "gsql", - PasswordEnv: "$GS_PASSWORD", - UserEnv: "$GS_USERNAME", - Database: "$GS_DB", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# opengauss client connection example -gsql -U %s -W %s -d %s`, info.User, info.Password, info.Database) - }, - }, - } -} - -func (c *Commands) ConnectCommand(info *engines.AuthInfo) []string { - userName := c.info.UserEnv - userPass := c.info.PasswordEnv - dataBase := c.info.Database - if info != nil { - userName = engines.AddSingleQuote(info.UserName) - userPass = engines.AddSingleQuote(info.UserPasswd) - } - return []string{"sh", "-c", fmt.Sprintf("%s -U%s -W%s -d%s", c.info.Client, userName, userPass, dataBase)} -} - -func (c *Commands) Container() string { - return c.info.Container -} - -func (c *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, c.examples) -} - -func (c *Commands) ExecuteCommand(strings []string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("opengauss execute cammand interface do not implement") -} diff --git a/pkg/lorry/engines/opengauss/commands_test.go b/pkg/lorry/engines/opengauss/commands_test.go deleted file mode 100644 index 37f4a74e6fd..00000000000 --- a/pkg/lorry/engines/opengauss/commands_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package opengauss - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("OpenGauss Engine", func() { - It("connection command", func() { - opengauss := NewCommands() - - Expect(opengauss.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(opengauss.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - opengauss := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range opengauss.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(opengauss.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(opengauss.ConnectExample(info, "")).ShouldNot(BeZero()) - }) - -}) diff --git a/pkg/lorry/engines/opengauss/suite_test.go b/pkg/lorry/engines/opengauss/suite_test.go deleted file mode 100644 index 6d52482a157..00000000000 --- a/pkg/lorry/engines/opengauss/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package opengauss - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "oepengauss Suite") -} diff --git a/pkg/lorry/engines/oracle/commands.go b/pkg/lorry/engines/oracle/commands.go deleted file mode 100644 index abbe021ae05..00000000000 --- a/pkg/lorry/engines/oracle/commands.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oracle - -import ( - "fmt" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "sqlplus", - PasswordEnv: "$ORACLE_PWD", - UserEnv: "sys", - Database: "$ORACLE_SID", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# oracle client connection example -sqlplus sys/%s@//localhost:1521/%s as sysdba`, info.Password, info.Database) - }, - }, - } -} - -func (c *Commands) ConnectCommand(info *engines.AuthInfo) []string { - userName := c.info.UserEnv - userPass := c.info.PasswordEnv - dataBase := c.info.Database - if info != nil { - userPass = engines.AddSingleQuote(info.UserPasswd) - } - return []string{"sh", "-c", fmt.Sprintf("%s %s/%s@//localhost:1521/%s as sysdba", c.info.Client, userName, userPass, dataBase)} -} - -func (c *Commands) Container() string { - return c.info.Container -} - -func (c *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, c.examples) -} - -func (c *Commands) ExecuteCommand(strings []string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("oracle execute cammand interface do not implement") -} diff --git a/pkg/lorry/engines/oracle/commands_test.go b/pkg/lorry/engines/oracle/commands_test.go deleted file mode 100644 index 103ca3f577d..00000000000 --- a/pkg/lorry/engines/oracle/commands_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oracle - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("OpenGauss Engine", func() { - It("connection command", func() { - opengauss := NewCommands() - - Expect(opengauss.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(opengauss.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - opengauss := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range opengauss.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(opengauss.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(opengauss.ConnectExample(info, "")).ShouldNot(BeZero()) - }) - -}) diff --git a/pkg/lorry/engines/oracle/suite_test.go b/pkg/lorry/engines/oracle/suite_test.go deleted file mode 100644 index 6ef304ee8b1..00000000000 --- a/pkg/lorry/engines/oracle/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package oracle - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "oracle Suite") -} diff --git a/pkg/lorry/engines/polardbx/config.go b/pkg/lorry/engines/polardbx/config.go deleted file mode 100644 index f2b51093127..00000000000 --- a/pkg/lorry/engines/polardbx/config.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package polardbx - -import ( - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -type Config struct { - *mysql.Config -} - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - mysqlConfig, err := mysql.NewConfig(properties) - if err != nil { - return nil, err - } - config = &Config{ - Config: mysqlConfig, - } - return config, nil -} diff --git a/pkg/lorry/engines/polardbx/get_replica_role.go b/pkg/lorry/engines/polardbx/get_replica_role.go deleted file mode 100644 index 464c56b924a..00000000000 --- a/pkg/lorry/engines/polardbx/get_replica_role.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package polardbx - -import ( - "context" - - "github.com/pkg/errors" -) - -func (mgr *Manager) GetReplcaRole(ctx context.Context) (string, error) { - sql := "select role from information_schema.alisql_cluster_local" - - rows, err := mgr.DB.QueryContext(ctx, sql) - if err != nil { - mgr.Logger.Info("error executing sql", "sql", sql, "error", err.Error()) - return "", errors.Wrapf(err, "error executing %s", sql) - } - - defer func() { - _ = rows.Close() - _ = rows.Err() - }() - - var role string - var isReady bool - for rows.Next() { - if err = rows.Scan(&role); err != nil { - mgr.Logger.Info("Role query failed", "error", err.Error()) - return role, err - } - isReady = true - } - if isReady { - return role, nil - } - return "", errors.Errorf("exec sql %s failed: no data returned", sql) -} diff --git a/pkg/lorry/engines/polardbx/manager.go b/pkg/lorry/engines/polardbx/manager.go deleted file mode 100644 index 62498c7bc59..00000000000 --- a/pkg/lorry/engines/polardbx/manager.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package polardbx - -import ( - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -const ( - PolardbXServiceType = "polardbx" -) - -type Manager struct { - mysql.Manager -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("PolarDBX") - mysqlMgr, err := mysql.NewManager(properties) - if err != nil { - return nil, err - } - mgr := &Manager{ - Manager: *mysqlMgr.(*mysql.Manager), - } - mgr.Logger = logger - - return mgr, nil -} diff --git a/pkg/lorry/engines/postgres/apecloudpostgres/get_replica_role.go b/pkg/lorry/engines/postgres/apecloudpostgres/get_replica_role.go deleted file mode 100644 index 88098dc4622..00000000000 --- a/pkg/lorry/engines/postgres/apecloudpostgres/get_replica_role.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package apecloudpostgres - -import ( - "context" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - return mgr.GetMemberRoleWithHost(ctx, "") -} diff --git a/pkg/lorry/engines/postgres/apecloudpostgres/manager.go b/pkg/lorry/engines/postgres/apecloudpostgres/manager.go deleted file mode 100644 index 7309e616134..00000000000 --- a/pkg/lorry/engines/postgres/apecloudpostgres/manager.go +++ /dev/null @@ -1,415 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package apecloudpostgres - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/spf13/cast" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -type Manager struct { - postgres.Manager - memberAddrs []string - healthStatus *postgres.ConsensusMemberHealthStatus -} - -var _ engines.DBManager = &Manager{} - -var Mgr *Manager - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - Mgr = &Manager{} - - baseManager, err := postgres.NewManager(properties) - if err != nil { - return nil, errors.Errorf("new base manager failed, err: %v", err) - } - - Mgr.Manager = *baseManager.(*postgres.Manager) - return Mgr, nil -} - -func (mgr *Manager) GetDBState(ctx context.Context, cluster *dcs.Cluster) *dcs.DBState { - mgr.DBState = nil - mgr.UnsetIsLeader() - dbState := &dcs.DBState{ - Extra: map[string]string{}, - } - - isLeader, err := mgr.IsLeader(ctx, cluster) - if err != nil { - mgr.Logger.Error(err, "check is leader failed") - return nil - } - mgr.SetIsLeader(isLeader) - - memberAddrs := mgr.GetMemberAddrs(ctx, cluster) - if memberAddrs == nil { - mgr.Logger.Error(nil, "get member addrs failed") - return nil - } - mgr.memberAddrs = memberAddrs - - healthStatus, err := mgr.getMemberHealthStatus(ctx, cluster, cluster.GetMemberWithName(mgr.CurrentMemberName)) - if err != nil { - mgr.Logger.Error(err, "get member health status failed") - return nil - } - mgr.healthStatus = healthStatus - - mgr.DBState = dbState - return dbState -} - -func (mgr *Manager) IsLeader(ctx context.Context, _ *dcs.Cluster) (bool, error) { - isSet, isLeader := mgr.GetIsLeader() - if isSet { - return isLeader, nil - } - - return mgr.IsLeaderWithHost(ctx, "") -} - -func (mgr *Manager) IsLeaderWithHost(ctx context.Context, host string) (bool, error) { - role, err := mgr.GetMemberRoleWithHost(ctx, host) - if err != nil { - return false, errors.Errorf("check is leader with host:%s failed, err:%v", host, err) - } - - return role == strings.ToLower(models.LEADER), nil -} - -func (mgr *Manager) IsDBStartupReady() bool { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - if mgr.DBStartupReady { - return true - } - - if !mgr.IsPgReady(ctx) { - return false - } - - if !mgr.isConsensusReadyUp(ctx) { - return false - } - - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func (mgr *Manager) isConsensusReadyUp(ctx context.Context) bool { - sql := `SELECT extname FROM pg_extension WHERE extname = 'consensus';` - resp, err := mgr.Query(ctx, sql) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("query sql:%s failed", sql)) - return false - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query response:%s failed", string(resp))) - return false - } - - return resMap[0]["extname"] != nil -} - -func (mgr *Manager) IsClusterInitialized(ctx context.Context, _ *dcs.Cluster) (bool, error) { - if !mgr.IsFirstMember() { - mgr.Logger.Info("I am not the first member, just skip and wait for the first member to initialize the cluster.") - return true, nil - } - - if !mgr.IsDBStartupReady() { - return false, nil - } - - sql := `SELECT usename FROM pg_user WHERE usename = 'replicator';` - resp, err := mgr.Query(ctx, sql) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("query sql:%s failed", sql)) - return false, err - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query response:%s failed", string(resp))) - return false, err - } - - return resMap[0]["usename"] != nil, nil -} - -func (mgr *Manager) InitializeCluster(ctx context.Context, _ *dcs.Cluster) error { - sql := "create role replicator with superuser login password 'replicator';" + - "create extension if not exists consensus_monitor;" - - _, err := mgr.Exec(ctx, sql) - return err -} - -func (mgr *Manager) GetMemberRoleWithHost(ctx context.Context, host string) (string, error) { - sql := `select role from consensus_member_status;` - - resp, err := mgr.QueryWithHost(ctx, sql, host) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("query sql:%s failed", sql)) - return "", err - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query response:%s failed", string(resp))) - return "", err - } - - return strings.ToLower(cast.ToString(resMap[0]["role"])), nil -} - -func (mgr *Manager) GetMemberAddrs(ctx context.Context, cluster *dcs.Cluster) []string { - if mgr.DBState != nil && mgr.memberAddrs != nil { - return mgr.memberAddrs - } - - sql := `select ip_port from consensus_cluster_status;` - resp, err := mgr.QueryLeader(ctx, sql, cluster) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("query %s with leader failed", sql)) - return nil - } - - result, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query response:%s failed", string(resp))) - return nil - } - - var addrs []string - for _, m := range result { - addrs = append(addrs, strings.Split(cast.ToString(m["ip_port"]), ":")[0]) - } - - return addrs -} - -func (mgr *Manager) GetMemberAddrWithName(ctx context.Context, cluster *dcs.Cluster, memberName string) string { - addrs := mgr.GetMemberAddrs(ctx, cluster) - for _, addr := range addrs { - if strings.HasPrefix(addr, memberName) { - return addr - } - } - return "" -} - -func (mgr *Manager) IsCurrentMemberInCluster(ctx context.Context, cluster *dcs.Cluster) bool { - return mgr.GetMemberAddrWithName(ctx, cluster, mgr.CurrentMemberName) != "" -} - -func (mgr *Manager) IsCurrentMemberHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - return mgr.IsMemberHealthy(ctx, cluster, cluster.GetMemberWithName(mgr.CurrentMemberName)) -} - -// IsMemberHealthy firstly get the leader's connection pool, -// because only leader can get the cluster healthy view -func (mgr *Manager) IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool { - healthStatus, err := mgr.getMemberHealthStatus(ctx, cluster, member) - if errors.Is(err, postgres.ClusterHasNoLeader) { - mgr.Logger.Info("cluster has no leader, will compete the leader lock") - return true - } else if err != nil { - mgr.Logger.Error(err, "check member healthy failed") - return false - } - - return healthStatus.Connected -} - -func (mgr *Manager) getMemberHealthStatus(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (*postgres.ConsensusMemberHealthStatus, error) { - if mgr.DBState != nil && mgr.healthStatus != nil { - return mgr.healthStatus, nil - } - res := &postgres.ConsensusMemberHealthStatus{} - - IPPort := mgr.Config.GetConsensusIPPort(cluster, member.Name) - sql := fmt.Sprintf(`select connected, log_delay_num from consensus_cluster_health where ip_port = '%s';`, IPPort) - resp, err := mgr.QueryLeader(ctx, sql, cluster) - if err != nil { - return nil, err - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - return nil, err - } - - if resMap[0]["connected"] != nil { - res.Connected = cast.ToBool(resMap[0]["connected"]) - } - if resMap[0]["log_delay_num"] != nil { - res.LogDelayNum = cast.ToInt64(resMap[0]["log_delay_num"]) - } - - return res, nil -} - -func (mgr *Manager) IsMemberLagging(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, int64) { - healthStatus, err := mgr.getMemberHealthStatus(ctx, cluster, member) - if errors.Is(err, postgres.ClusterHasNoLeader) { - mgr.Logger.Info("cluster has no leader, so member has no lag") - return false, 0 - } else if err != nil { - mgr.Logger.Error(err, "check member lag failed") - return true, cluster.HaConfig.GetMaxLagOnSwitchover() + 1 - } - - return healthStatus.LogDelayNum > cluster.HaConfig.GetMaxLagOnSwitchover(), healthStatus.LogDelayNum -} - -func (mgr *Manager) JoinCurrentMemberToCluster(ctx context.Context, cluster *dcs.Cluster) error { - // use the env KB_POD_FQDN consistently with the startup script - sql := fmt.Sprintf(`alter system consensus add follower '%s:%d';`, - viper.GetString("KB_POD_FQDN"), mgr.Config.GetDBPort()) - - _, err := mgr.ExecLeader(ctx, sql, cluster) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("exec sql:%s failed", sql)) - return err - } - - return nil -} - -func (mgr *Manager) LeaveMemberFromCluster(ctx context.Context, cluster *dcs.Cluster, memberName string) error { - addr := mgr.GetMemberAddrWithName(ctx, cluster, memberName) - if addr == "" { - mgr.Logger.Info(fmt.Sprintf("member %s already deleted", memberName)) - return nil - } - - sql := fmt.Sprintf(`alter system consensus drop follower '%s:%d';`, addr, mgr.Config.GetDBPort()) - - _, err := mgr.ExecLeader(ctx, sql, cluster) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("exec sql:%s failed", sql)) - return err - } - - return nil -} - -// IsClusterHealthy considers the health status of the cluster equivalent to the health status of the leader -func (mgr *Manager) IsClusterHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - mgr.Logger.Info("cluster has no leader, wait for leader to take the lock") - // when cluster has no leader, the health status of the cluster is assumed to be true by default, - // in order to proceed with the logic of competing for the leader lock - return true - } - - if leaderMember.Name == mgr.CurrentMemberName { - // if the member is leader, then its health status will check in IsMemberHealthy later - return true - } - - return mgr.IsMemberHealthy(ctx, cluster, leaderMember) -} - -func (mgr *Manager) Promote(ctx context.Context, cluster *dcs.Cluster) error { - if isLeader, err := mgr.IsLeader(ctx, nil); isLeader && err == nil { - mgr.Logger.Info("i am already the leader, don't need to promote") - return nil - } - - currentLeaderAddr, err := mgr.GetLeaderAddr(ctx) - if err != nil { - return err - } - - currentMemberAddr := mgr.GetMemberAddrWithName(ctx, cluster, mgr.CurrentMemberName) - if currentMemberAddr == "" { - return errors.New("get current member addr failed") - } - - promoteSQL := fmt.Sprintf(`alter system consensus CHANGE LEADER TO '%s:%d';`, currentMemberAddr, mgr.Config.GetDBPort()) - _, err = mgr.ExecWithHost(ctx, promoteSQL, currentLeaderAddr) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("exec sql:%s failed", promoteSQL)) - return err - } - - return nil -} - -func (mgr *Manager) IsPromoted(ctx context.Context) bool { - isLeader, _ := mgr.IsLeader(ctx, nil) - return isLeader -} - -func (mgr *Manager) Follow(_ context.Context, cluster *dcs.Cluster) error { - mgr.Logger.Info("current member still follow the leader", "leader name", cluster.Leader.Name) - return nil -} - -func (mgr *Manager) HasOtherHealthyLeader(ctx context.Context, cluster *dcs.Cluster) *dcs.Member { - if isLeader, err := mgr.IsLeader(ctx, cluster); isLeader && err == nil { - // I am the leader, just return nil - return nil - } - - host, err := mgr.GetLeaderAddr(ctx) - if err != nil { - mgr.Logger.Error(err, "get leader addr failed") - return nil - } - - leaderName := strings.Split(host, ".")[0] - if len(leaderName) > 0 { - return cluster.GetMemberWithName(leaderName) - } - - return nil -} - -func (mgr *Manager) HasOtherHealthyMembers(ctx context.Context, cluster *dcs.Cluster, leader string) []*dcs.Member { - members := make([]*dcs.Member, 0) - - for i, m := range cluster.Members { - if m.Name != leader && mgr.IsMemberHealthy(ctx, cluster, &m) { - members = append(members, &cluster.Members[i]) - } - } - - return members -} diff --git a/pkg/lorry/engines/postgres/apecloudpostgres/manager_test.go b/pkg/lorry/engines/postgres/apecloudpostgres/manager_test.go deleted file mode 100644 index 002c4e0c5fd..00000000000 --- a/pkg/lorry/engines/postgres/apecloudpostgres/manager_test.go +++ /dev/null @@ -1,846 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package apecloudpostgres - -import ( - "context" - "fmt" - "testing" - - "github.com/pashagolub/pgxmock/v2" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -func MockDatabase(t *testing.T) (*Manager, pgxmock.PgxPoolIface, error) { - properties := map[string]string{ - postgres.ConnectionURLKey: "user=test password=test host=localhost port=5432 dbname=postgres", - } - testConfig, err := postgres.NewConfig(properties) - assert.NotNil(t, testConfig) - assert.Nil(t, err) - - viper.Set(constant.KBEnvPodName, "test-pod-0") - viper.Set(constant.KBEnvClusterCompName, "test") - viper.Set(constant.KBEnvNamespace, "default") - viper.Set(postgres.PGDATA, "test") - mock, err := pgxmock.NewPool(pgxmock.MonitorPingsOption(true)) - if err != nil { - t.Fatal(err) - } - - dbManager, err := NewManager(engines.Properties(properties)) - if err != nil { - t.Fatal(err) - } - - manager := dbManager.(*Manager) - manager.Pool = mock - - return manager, mock, err -} - -func TestIsConsensusReadyUp(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("consensus has been ready up", func(t *testing.T) { - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"}).AddRow("consensus_monitor")) - - isReadyUp := manager.isConsensusReadyUp(ctx) - assert.True(t, isReadyUp) - }) - - t.Run("consensus has not been ready up", func(t *testing.T) { - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"})) - - isReadyUp := manager.isConsensusReadyUp(ctx) - assert.False(t, isReadyUp) - }) - - t.Run("query pg_extension error", func(t *testing.T) { - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnError(fmt.Errorf("some errors")) - - isReadyUp := manager.isConsensusReadyUp(ctx) - assert.False(t, isReadyUp) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsDBStartupReady(t *testing.T) { - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("db start up has been set", func(t *testing.T) { - manager.DBStartupReady = true - - isReady := manager.IsDBStartupReady() - assert.True(t, isReady) - }) - - t.Run("ping db failed", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing(). - WillReturnError(fmt.Errorf("some error")) - - isReady := manager.IsDBStartupReady() - assert.False(t, isReady) - }) - - t.Run("ping db success but consensus not ready up", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing() - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"})) - - isReady := manager.IsDBStartupReady() - assert.False(t, isReady) - }) - - t.Run("db is startup ready", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing() - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"}).AddRow("consensus_monitor")) - - isReady := manager.IsDBStartupReady() - assert.True(t, isReady) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsClusterInitialized(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("is not first member", func(t *testing.T) { - manager.CurrentMemberName = "test-pod-1" - - isClusterInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.True(t, isClusterInitialized) - assert.Nil(t, err) - manager.CurrentMemberName = "test-pod-0" - }) - - t.Run("db is not startup ready", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing(). - WillReturnError(fmt.Errorf("some error")) - - isClusterInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isClusterInitialized) - assert.Nil(t, err) - }) - - t.Run("query db user error", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing() - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"}).AddRow("consensus_monitor")) - mock.ExpectQuery("SELECT usename FROM pg_user"). - WillReturnError(fmt.Errorf("some error")) - - isClusterInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isClusterInitialized) - assert.NotNil(t, err) - }) - - t.Run("parse query error", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing() - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"}).AddRow("consensus_monitor")) - mock.ExpectQuery("SELECT usename FROM pg_user"). - WillReturnRows(pgxmock.NewRows([]string{"usename"})) - - isClusterInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isClusterInitialized) - assert.NotNil(t, err) - }) - - t.Run("cluster is initialized", func(t *testing.T) { - manager.DBStartupReady = false - mock.ExpectPing() - mock.ExpectQuery("SELECT extname FROM pg_extension"). - WillReturnRows(pgxmock.NewRows([]string{"extname"}).AddRow("consensus_monitor")) - mock.ExpectQuery("SELECT usename FROM pg_user"). - WillReturnRows(pgxmock.NewRows([]string{"usename"}).AddRow("replicator")) - - isClusterInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.True(t, isClusterInitialized) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestInitializeCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("exec create role and extension failed", func(t *testing.T) { - mock.ExpectExec("create role replicator"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.InitializeCluster(ctx, nil) - assert.NotNil(t, err) - }) - - t.Run("exec create role and extension failed", func(t *testing.T) { - mock.ExpectExec("create role replicator"). - WillReturnResult(pgxmock.NewResult("create", 1)) - - err := manager.InitializeCluster(ctx, nil) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetMemberRoleWithHost(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - roles := []string{constant.Follower, constant.Candidate, constant.Leader, constant.Learner, ""} - - t.Run("query paxos role failed", func(t *testing.T) { - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnError(fmt.Errorf("some error")) - - role, err := manager.GetMemberRoleWithHost(ctx, "") - assert.Equal(t, "", role) - assert.NotNil(t, err) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnRows(pgxmock.NewRows([]string{"role"})) - - role, err := manager.GetMemberRoleWithHost(ctx, "") - assert.Equal(t, "", role) - assert.NotNil(t, err) - }) - - t.Run("get member role with host success", func(t *testing.T) { - for _, r := range roles { - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow(r)) - - role, err := manager.GetMemberRoleWithHost(ctx, "") - assert.Equal(t, r, role) - assert.Nil(t, err) - } - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsLeaderWithHost(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("get member role with host failed", func(t *testing.T) { - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnError(fmt.Errorf("some error")) - - isLeader, err := manager.IsLeaderWithHost(ctx, "") - assert.False(t, isLeader) - assert.NotNil(t, err) - }) - - t.Run("check is leader success", func(t *testing.T) { - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow("Leader")) - - isLeader, err := manager.IsLeaderWithHost(ctx, "") - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("is leader has been set", func(t *testing.T) { - manager.SetIsLeader(true) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - t.Run("is leader has not been set", func(t *testing.T) { - manager.UnsetIsLeader() - mock.ExpectQuery("select role from consensus_member_status;"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow("Leader")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetMemberAddrs(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - Name: manager.CurrentMemberName, - }, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("query ip port failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status;"). - WillReturnError(fmt.Errorf("some errors")) - - addrs := manager.GetMemberAddrs(ctx, cluster) - assert.Nil(t, addrs) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status;"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"})) - - addrs := manager.GetMemberAddrs(ctx, cluster) - assert.Nil(t, addrs) - }) - - t.Run("get member addrs success", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status;"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRows([][]any{{"a"}, {"b"}, {"c"}}...)) - - addrs := manager.GetMemberAddrs(ctx, cluster) - assert.Equal(t, []string{"a", "b", "c"}, addrs) - }) - - t.Run("has set addrs", func(t *testing.T) { - manager.DBState = &dcs.DBState{} - manager.memberAddrs = []string{"a", "b", "c"} - - addrs := manager.GetMemberAddrs(ctx, cluster) - assert.Equal(t, []string{"a", "b", "c"}, addrs) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsCurrentMemberInCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - manager.DBState = &dcs.DBState{} - cluster := &dcs.Cluster{ - Namespace: manager.Namespace, - ClusterCompName: manager.ClusterCompName, - } - - t.Run("currentMember is in cluster", func(t *testing.T) { - manager.memberAddrs = []string{"test-pod-0.test-headless"} - - inCluster := manager.IsCurrentMemberInCluster(ctx, cluster) - assert.True(t, inCluster) - }) - - t.Run("currentMember is not in cluster", func(t *testing.T) { - manager.memberAddrs[0] = "test-pod-1.test-headless" - - inCluster := manager.IsCurrentMemberInCluster(ctx, cluster) - assert.False(t, inCluster) - }) -} - -func TestIsCurrentMemberHealthy(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("cluster has no leader", func(t *testing.T) { - isHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.True(t, isHealthy) - }) - - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - - t.Run("get member health status failed", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnError(fmt.Errorf("some error")) - - isHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.False(t, isHealthy) - }) - - t.Run("member is healthy", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnRows(pgxmock.NewRows([]string{"connected", "log_delay_num"}).AddRow(true, 0)) - - isHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.True(t, isHealthy) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetMemberHealthyStatus(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnError(fmt.Errorf("some error")) - - healthStatus, err := manager.getMemberHealthStatus(ctx, cluster, cluster.GetMemberWithName(manager.CurrentMemberName)) - assert.NotNil(t, err) - assert.Nil(t, healthStatus) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnRows(pgxmock.NewRows([]string{"connected, log_delay_num"})) - - healthStatus, err := manager.getMemberHealthStatus(ctx, cluster, cluster.GetMemberWithName(manager.CurrentMemberName)) - assert.NotNil(t, err) - assert.Nil(t, healthStatus) - }) - - t.Run("get member health status success", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnRows(pgxmock.NewRows([]string{"connected", "log_delay_num"}).AddRow(true, 0)) - - healthStatus, err := manager.getMemberHealthStatus(ctx, cluster, cluster.GetMemberWithName(manager.CurrentMemberName)) - assert.Nil(t, err) - assert.True(t, healthStatus.Connected) - assert.Equal(t, int64(0), healthStatus.LogDelayNum) - }) - - t.Run("health status has been set", func(t *testing.T) { - manager.healthStatus = &postgres.ConsensusMemberHealthStatus{ - Connected: false, - LogDelayNum: 200, - } - manager.DBState = &dcs.DBState{} - - healthStatus, err := manager.getMemberHealthStatus(ctx, cluster, cluster.GetMemberWithName(manager.CurrentMemberName)) - assert.Nil(t, err) - assert.False(t, healthStatus.Connected) - assert.Equal(t, int64(200), healthStatus.LogDelayNum) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsMemberLagging(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - currentMember := cluster.GetMemberWithName(manager.CurrentMemberName) - - t.Run("cluster has no leader", func(t *testing.T) { - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.False(t, isLagging) - assert.Equal(t, int64(0), lag) - }) - - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - - t.Run("get member health status failed", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnError(fmt.Errorf("some error")) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("member is not lagging", func(t *testing.T) { - mock.ExpectQuery("select connected, log_delay_num from consensus_cluster_health"). - WillReturnRows(pgxmock.NewRows([]string{"connected", "log_delay_num"}).AddRow(true, 0)) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.False(t, isLagging) - assert.Equal(t, int64(0), lag) - }) - - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } -} - -func TestJoinCurrentMemberToCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("exec alter system failed", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.JoinCurrentMemberToCluster(ctx, cluster) - assert.NotNil(t, err) - }) - - t.Run("exec alter system success", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter system", 1)) - - err := manager.JoinCurrentMemberToCluster(ctx, cluster) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestLeaveMemberFromCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - {Name: manager.CurrentMemberName}, - }, - } - - t.Run("cluster has no leader now ", func(t *testing.T) { - mock.ExpectQuery("select ip_port"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.LeaveMemberFromCluster(ctx, cluster, "") - assert.Nil(t, err, nil) - }) - - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - t.Run("exec alter system success", func(t *testing.T) { - mock.ExpectQuery("select ip_port"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRow("test-pod-0")) - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter system", 1)) - - err := manager.LeaveMemberFromCluster(ctx, cluster, "") - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsClusterHealthy(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("cluster has no leader", func(t *testing.T) { - isClusterHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.True(t, isClusterHealthy) - }) - - cluster.Leader = &dcs.Leader{} - - t.Run("current member is leader", func(t *testing.T) { - cluster.Leader.Name = manager.CurrentMemberName - isClusterHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.True(t, isClusterHealthy) - }) - - t.Run("cluster is healthy", func(t *testing.T) { - cluster.Leader.Name = "test" - cluster.Members[0].Name = "test" - manager.DBState = &dcs.DBState{} - manager.healthStatus = &postgres.ConsensusMemberHealthStatus{ - Connected: true, - } - - isClusterHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.True(t, isClusterHealthy) - }) -} - -func TestPromote(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - Namespace: manager.Namespace, - ClusterCompName: manager.ClusterCompName, - } - - t.Run("get leader addr failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("get current member addr failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRow(":")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "get current member addr failed") - }) - - manager.DBState = &dcs.DBState{} - manager.memberAddrs = []string{"test-pod-0.test-headless"} - t.Run("exec promote failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRow(":")) - mock.ExpectExec("alter system"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("exec promote success", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRow(":")) - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter system", 1)) - - err := manager.Promote(ctx, cluster) - assert.Nil(t, err) - }) - - t.Run("current member is already the leader", func(t *testing.T) { - manager.SetIsLeader(true) - - err := manager.Promote(ctx, cluster) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsPromoted(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("is promoted", func(t *testing.T) { - manager.SetIsLeader(true) - isPromoted := manager.IsPromoted(ctx) - - assert.True(t, isPromoted) - }) -} - -func TestHasOtherHealthyLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - - t.Run("get leader addr failed", func(t *testing.T) { - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnError(fmt.Errorf("some error")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - t.Run("has other healthy leader", func(t *testing.T) { - cluster.Members = append(cluster.Members, dcs.Member{ - Name: "test", - }) - mock.ExpectQuery("select ip_port from consensus_cluster_status"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRow("test:5432")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.NotNil(t, member) - }) - - t.Run("current member is leader", func(t *testing.T) { - manager.SetIsLeader(true) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestHasOtherHealthyMembers(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("", func(t *testing.T) { - members := manager.HasOtherHealthyMembers(ctx, cluster, manager.CurrentMemberName) - assert.Equal(t, 0, len(members)) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetDBState(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - - t.Run("check is leader failed", func(t *testing.T) { - mock.ExpectQuery("select role"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get member addrs failed", func(t *testing.T) { - mock.ExpectQuery("select role"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow("Leader")) - mock.ExpectQuery("select ip_port"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get member health status failed", func(t *testing.T) { - mock.ExpectQuery("select role"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow("Leader")) - mock.ExpectQuery("select ip_port"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRows([][]any{{"a"}, {"b"}, {"c"}}...)) - mock.ExpectQuery("select connected, log_delay_num"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get db state success", func(t *testing.T) { - mock.ExpectQuery("select role"). - WillReturnRows(pgxmock.NewRows([]string{"role"}).AddRow("Leader")) - mock.ExpectQuery("select ip_port"). - WillReturnRows(pgxmock.NewRows([]string{"ip_port"}).AddRows([][]any{{"a"}, {"b"}, {"c"}}...)) - mock.ExpectQuery("select connected, log_delay_num"). - WillReturnRows(pgxmock.NewRows([]string{"connected", "log_delay_num"}).AddRow(true, 20)) - - dbState := manager.GetDBState(ctx, cluster) - assert.NotNil(t, dbState) - assert.Equal(t, []string{"a", "b", "c"}, manager.memberAddrs) - assert.Equal(t, int64(20), manager.healthStatus.LogDelayNum) - assert.True(t, manager.healthStatus.Connected) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/postgres/commands.go b/pkg/lorry/engines/postgres/commands.go deleted file mode 100644 index b35d40d63d4..00000000000 --- a/pkg/lorry/engines/postgres/commands.go +++ /dev/null @@ -1,297 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "fmt" - "strconv" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "psql", - Container: "postgresql", - PasswordEnv: "$PGPASSWORD", - UserEnv: "$PGUSER", - Database: "postgres", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# psql client connection example -PGPASSWORD=%s psql -h %s -p %s -U %s %s -`, info.Password, info.Host, info.Port, info.User, info.Database) - }, - - models.DJANGO: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DB_HOST=%s -DB_NAME=%s -DB_USER=%s -DB_PASSWORD=%s -DB_PORT=%s - -# settings.py -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get('DB_NAME'), - 'HOST': os.environ.get('DB_HOST'), - 'PORT': os.environ.get('DB_PORT'), - 'USER': os.environ.get('DB_USER'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - } -} -`, info.Host, info.Database, info.User, info.Password, info.Port) - }, - - models.DOTNET: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# Startup.cs -var connectionString = "Host=%s;Port=%s;Username=%s;Password=%s;Database=%s"; -await using var dataSource = NpgsqlDataSource.Create(connectionString); -`, info.Host, info.Port, info.User, info.Password, info.Database) - }, - - models.GO: func(info *engines.ConnectionInfo) string { - const goConnectExample = `# main.go -package main - -import ( - "database/sql" - "log" - "os" - - _ "github.com/lib/pq" -) - -func main() { - db, err := sql.Open("postgres", os.Getenv("DSN")) - if err != nil { - log.Fatalf("failed to connect: %v", err) - } - defer db.Close() - - if err := db.Ping(); err != nil { - log.Fatalf("failed to ping: %v", err) - } - - log.Println("Successfully connected!") -} -` - dsn := fmt.Sprintf(`# .env -DSN=%s:%s@tcp(%s:%s)/%s -`, info.User, info.Password, info.Host, info.Port, info.Database) - return fmt.Sprintf("%s\n%s", dsn, goConnectExample) - }, - - models.JAVA: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`Class.forName("org.postgresql.Driver"); -Connection conn = DriverManager.getConnection( - "jdbc:postgresql://%s:%s/%s?user=%s&password=%s"); -`, info.Host, info.Port, info.Database, info.User, info.Password) - }, - - models.NODEJS: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='postgres://%s:%s@%s:%s/%s' - -# app.js -require('dotenv').config(); -const postgres = require('postgres'); -const connection = postgres(process.env.DATABASE_URL); -connection.end(); -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.PHP: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -HOST=%s -PORT=%s -USERNAME=%s -PASSWORD=%s -DATABASE=%s - -# index.php - -`, info.Host, info.Port, info.User, info.Password, info.Database) - }, - - models.PRISMA: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='postgres://%s:%s@%s:%s/%s' - -# schema.prisma -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - relationMode = "prisma" -} -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.PYTHON: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# run the following command in the terminal to install dependencies -pip install python-dotenv psycopg2 - -# .env -HOST=%s -PORT=%s -USERNAME=%s -PASSWORD=%s -DATABASE=%s - -# main.py -from dotenv import load_dotenv -load_dotenv() -import os -import MySQLdb - -connection = psycopg2.connect( - host= os.getenv("HOST"), - port=os.getenv("PORT"), - user=os.getenv("USERNAME"), - password=os.getenv("PASSWORD"), - database=os.getenv("DATABASE"), -) -`, info.Host, info.Port, info.User, info.Password, info.Database) - }, - - models.RAILS: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# Gemfile -gem 'pg' - -# config/database.yml -development: - <<: *default - adapter: postgresql - database: %s - username: %s - host: %s - password: %s -`, info.Database, info.User, info.Host, info.Password) - }, - - models.RUST: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# run the following command in the terminal -export DATABASE_URL="postgresql://%s:%s@%s:%s/%s" - -# src/main.rs -use std::env; - -fn main() { - let url = env::var("DATABASE_URL").expect("DATABASE_URL not found"); - let conn = Connection::connect(url, TlsMode::None).unwrap(); - println!("Successfully connected!"); -} - -# Cargo.toml -[package] -name = "kubeblocks_hello_world" -version = "0.0.1" -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - - models.SYMFONY: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# .env -DATABASE_URL='postgresql://%s:%s@%s:%s/%s' -`, info.User, info.Password, info.Host, info.Port, info.Database) - }, - }, - } -} - -func (m *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - userName := m.info.UserEnv - userPass := m.info.PasswordEnv - - if connectInfo != nil { - userName = engines.AddSingleQuote(connectInfo.UserName) - userPass = engines.AddSingleQuote(connectInfo.UserPasswd) - } - - // please refer to PostgreSQL documentation for more details - // https://www.postgresql.org/docs/current/libpq-envars.html - cmd := []string{fmt.Sprintf("PGUSER=%s PGPASSWORD=%s PGDATABASE=%s %s", userName, userPass, m.info.Database, m.info.Client)} - return []string{"sh", "-c", strings.Join(cmd, " ")} -} - -func (m *Commands) Container() string { - return m.info.Container -} - -func (m *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - if len(info.Database) == 0 { - info.Database = m.info.Database - } - return engines.BuildExample(info, client, m.examples) -} - -func (m *Commands) ExecuteCommand(scripts []string) ([]string, []corev1.EnvVar, error) { - cmd := []string{} - cmd = append(cmd, "/bin/sh", "-c", "-ex") - args := []string{} - for _, script := range scripts { - // split each script with a new line - lines := strings.Split(script, "\n") - for _, line := range lines { - args = append(args, fmt.Sprintf("-c %s", strconv.Quote(line))) - } - } - cmd = append(cmd, fmt.Sprintf("%s %s", m.info.Client, strings.Join(args, " "))) - envVars := []corev1.EnvVar{ - { - Name: "PGHOST", - Value: fmt.Sprintf("$(%s)", engines.EnvVarMap[engines.HOST]), - }, - { - Name: "PGUSER", - Value: fmt.Sprintf("$(%s)", engines.EnvVarMap[engines.USER]), - }, - { - Name: "PGPASSWORD", - Value: fmt.Sprintf("$(%s)", engines.EnvVarMap[engines.PASSWORD]), - }, - { - Name: "PGDATABASE", - Value: m.info.Database, - }, - } - return cmd, envVars, nil -} diff --git a/pkg/lorry/engines/postgres/commands_test.go b/pkg/lorry/engines/postgres/commands_test.go deleted file mode 100644 index afe5934e2c0..00000000000 --- a/pkg/lorry/engines/postgres/commands_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Postgres Engine", func() { - It("connection command", func() { - postgres := NewCommands() - - Expect(postgres.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(postgres.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - postgres := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range postgres.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(postgres.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(postgres.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/postgres/config.go b/pkg/lorry/engines/postgres/config.go deleted file mode 100644 index d01c762789a..00000000000 --- a/pkg/lorry/engines/postgres/config.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "fmt" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const ( - ConnectionURLKey = "url" - DefaultPort = 5432 -) - -type Config struct { - URL string - Username string - Password string - Host string - Port int - Database string - MaxConnections int32 - MinConnections int32 - pgxConfig *pgxpool.Config -} - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - config = &Config{} - - url, ok := properties[ConnectionURLKey] - if !ok || url == "" { - return nil, errors.Errorf("required metadata not set: %s", ConnectionURLKey) - } - - poolConfig, err := pgxpool.ParseConfig(url) - if err != nil { - return nil, errors.Errorf("error opening DB connection: %v", err) - } - - config.Username = poolConfig.ConnConfig.User - config.Password = poolConfig.ConnConfig.Password - config.Host = poolConfig.ConnConfig.Host - config.Port = int(poolConfig.ConnConfig.Port) - config.Database = poolConfig.ConnConfig.Database - config.MaxConnections = poolConfig.MaxConns - config.MinConnections = poolConfig.MinConns - - if viper.IsSet(constant.KBEnvServiceUser) { - config.Username = viper.GetString(constant.KBEnvServiceUser) - } - if viper.IsSet(constant.KBEnvServicePassword) { - config.Password = viper.GetString(constant.KBEnvServicePassword) - } - - config.URL = config.GetConnectURLWithHost(config.Host) - pgxConfig, _ := pgxpool.ParseConfig(config.URL) - config.pgxConfig = pgxConfig - - return config, nil -} - -func (config *Config) GetDBPort() int { - if config.Port == 0 { - return DefaultPort - } - - return config.Port -} - -func (config *Config) GetConnectURLWithHost(host string) string { - return fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s", - config.Username, config.Password, host, config.Port, config.Database) -} - -func (config *Config) GetConsensusIPPort(cluster *dcs.Cluster, name string) string { - clusterDomain := viper.GetString(constant.KubernetesClusterDomainEnv) - return fmt.Sprintf("%s.%s-headless.%s.svc.%s:1%d", name, cluster.ClusterCompName, cluster.Namespace, clusterDomain, config.GetDBPort()) -} diff --git a/pkg/lorry/engines/postgres/config_test.go b/pkg/lorry/engines/postgres/config_test.go deleted file mode 100644 index 1b1754e9575..00000000000 --- a/pkg/lorry/engines/postgres/config_test.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -func TestGetPostgresqlMetadata(t *testing.T) { - t.Run("With defaults", func(t *testing.T) { - properties := map[string]string{ - ConnectionURLKey: "user=postgres password=docker host=localhost port=5432 dbname=postgres pool_min_conns=1 pool_max_conns=10", - } - - metadata, err := NewConfig(properties) - assert.Nil(t, err) - assert.Equal(t, "postgres", metadata.Username) - assert.Equal(t, "docker", metadata.Password) - assert.Equal(t, "localhost", metadata.Host) - assert.Equal(t, 5432, metadata.Port) - assert.Equal(t, "postgres", metadata.Database) - assert.Equal(t, int32(1), metadata.MinConnections) - assert.Equal(t, int32(10), metadata.MaxConnections) - }) - - t.Run("url not set", func(t *testing.T) { - properties := map[string]string{} - - _, err := NewConfig(properties) - assert.NotNil(t, err) - }) - - t.Run("pool max connection too small", func(t *testing.T) { - properties := map[string]string{ - ConnectionURLKey: "user=postgres password=docker host=localhost port=5432 dbname=postgres pool_min_conns=1 pool_max_conns=0", - } - - _, err := NewConfig(properties) - assert.NotNil(t, err) - }) - - t.Run("set env", func(t *testing.T) { - viper.Set(constant.KBEnvServiceUser, "test") - viper.Set(constant.KBEnvServicePassword, "test_pwd") - properties := map[string]string{ - ConnectionURLKey: "user=postgres password=docker host=localhost port=5432 dbname=postgres pool_min_conns=1 pool_max_conns=10", - } - metadata, err := NewConfig(properties) - assert.Nil(t, err) - - assert.Equal(t, metadata.Username, "test") - assert.Equal(t, metadata.Password, "test_pwd") - }) -} - -func TestConfigFunc(t *testing.T) { - properties := map[string]string{ - ConnectionURLKey: "user=postgres password=docker host=localhost port=5432 dbname=postgres pool_min_conns=1 pool_max_conns=10", - } - metadata, err := NewConfig(properties) - assert.NotNil(t, metadata) - assert.Nil(t, err) - - t.Run("get db port", func(t *testing.T) { - port := metadata.GetDBPort() - assert.Equal(t, port, 5432) - - metadata.Port = 0 - port = metadata.GetDBPort() - assert.Equal(t, port, 5432) - }) - - t.Run("get consensus IP port", func(t *testing.T) { - cluster := &dcs.Cluster{ - ClusterCompName: "test", - Namespace: "default", - } - - consensusIPPort := metadata.GetConsensusIPPort(cluster, "test") - assert.Equal(t, consensusIPPort, "test.test-headless.default.svc.cluster.local:15432") - }) -} diff --git a/pkg/lorry/engines/postgres/local_command.go b/pkg/lorry/engines/postgres/local_command.go deleted file mode 100644 index 8a2bbef600b..00000000000 --- a/pkg/lorry/engines/postgres/local_command.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "bytes" - "os/exec" - - "github.com/pkg/errors" -) - -type execCommand struct { - *exec.Cmd - Stdout *bytes.Buffer - Stderr *bytes.Buffer -} - -func NewExecCommander(name string, args ...string) LocalCommand { - execCmd := exec.Command(name, args...) - - var stdout, stderr bytes.Buffer - execCmd.Stdout = &stdout - execCmd.Stderr = &stderr - return &execCommand{ - Cmd: execCmd, - Stdout: &stdout, - Stderr: &stderr, - } -} - -func (cmd *execCommand) GetStdout() *bytes.Buffer { - return cmd.Stdout -} - -func (cmd *execCommand) GetStderr() *bytes.Buffer { - return cmd.Stderr -} - -var LocalCommander = NewExecCommander - -func ExecCommand(name string, args ...string) (string, error) { - cmd := LocalCommander(name, args...) - - err := cmd.Run() - if err != nil || cmd.GetStderr().String() != "" { - return "", errors.Errorf("exec command %s failed, err:%v, stderr:%s", name, err, cmd.GetStderr().String()) - } - - return cmd.GetStdout().String(), nil -} - -func Psql(args ...string) (string, error) { - return ExecCommand("psql", args...) -} - -func PgCtl(arg string) (string, error) { - args := []string{"-c"} - args = append(args, "pg_ctl "+arg) - args = append(args, "postgres") - - return ExecCommand("su", args...) -} - -func PgWalDump(args ...string) (string, error) { - return ExecCommand("pg_waldump", args...) -} - -func PgRewind(args ...string) (string, error) { - return ExecCommand("pg_rewind", args...) -} - -type FakeCommand struct { - Stdout *bytes.Buffer - Stderr *bytes.Buffer - RunnerFunc func() error -} - -func NewFakeCommander(f func() error, stdout, stderr *bytes.Buffer) func(name string, args ...string) LocalCommand { - return func(name string, args ...string) LocalCommand { - return &FakeCommand{ - RunnerFunc: f, - Stdout: stdout, - Stderr: stderr, - } - } -} - -func (cmd *FakeCommand) Run() error { - return cmd.RunnerFunc() -} - -func (cmd *FakeCommand) GetStdout() *bytes.Buffer { - return cmd.Stdout -} - -func (cmd *FakeCommand) GetStderr() *bytes.Buffer { - return cmd.Stderr -} diff --git a/pkg/lorry/engines/postgres/manager.go b/pkg/lorry/engines/postgres/manager.go deleted file mode 100644 index 0ae293ae9f5..00000000000 --- a/pkg/lorry/engines/postgres/manager.go +++ /dev/null @@ -1,228 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "context" - "fmt" - - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/pkg/errors" - "github.com/shirou/gopsutil/v3/process" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -type Manager struct { - engines.DBManagerBase - MajorVersion int - Pool PgxPoolIFace - Proc *process.Process - Config *Config - isLeader int -} - -func NewManager(properties map[string]string) (engines.DBManager, error) { - logger := ctrl.Log.WithName("PostgreSQL") - config, err := NewConfig(properties) - if err != nil { - return nil, err - } - - pool, err := pgxpool.NewWithConfig(context.Background(), config.pgxConfig) - if err != nil { - return nil, errors.Errorf("unable to ping the DB: %v", err) - } - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - managerBase.DataDir = viper.GetString(PGDATA) - - mgr := &Manager{ - DBManagerBase: *managerBase, - Pool: pool, - Config: config, - MajorVersion: viper.GetInt(PGMAJOR), - } - - return mgr, nil -} - -func (mgr *Manager) IsRunning() bool { - if mgr.Proc != nil { - if isRunning, err := mgr.Proc.IsRunning(); isRunning && err == nil { - return true - } - mgr.Proc = nil - } - - return mgr.newProcessFromPidFile() == nil -} - -func (mgr *Manager) newProcessFromPidFile() error { - pidFile, err := readPidFile(mgr.DataDir) - if err != nil { - mgr.Logger.Error(err, "read pid file failed, err") - return err - } - - proc, err := process.NewProcess(pidFile.pid) - if err != nil { - mgr.Logger.Error(err, "new process failed, err") - return err - } - - mgr.Proc = proc - return nil -} - -func (mgr *Manager) Recover(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) GetHealthiestMember(*dcs.Cluster, string) *dcs.Member { - return nil -} - -func (mgr *Manager) SetIsLeader(isLeader bool) { - if isLeader { - mgr.isLeader = 1 - } else { - mgr.isLeader = -1 - } -} - -func (mgr *Manager) UnsetIsLeader() { - mgr.isLeader = 0 -} - -// GetIsLeader returns whether the "isLeader" is set or not and whether current member is leader or not -func (mgr *Manager) GetIsLeader() (bool, bool) { - return mgr.isLeader != 0, mgr.isLeader == 1 -} - -func (mgr *Manager) IsLeaderMember(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, error) { - if member == nil { - return false, errors.Errorf("member is nil, can't check is leader member or not") - } - - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - return false, errors.Errorf("leader member is nil, can't check is leader member or not") - } - - if leaderMember.Name != member.Name { - return false, nil - } - - return true, nil -} - -func (mgr *Manager) ReadCheck(ctx context.Context, host string) bool { - readSQL := fmt.Sprintf(`select check_ts from kb_health_check where type=%d limit 1;`, engines.CheckStatusType) - _, err := mgr.QueryWithHost(ctx, readSQL, host) - if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == "42P01" { - // no healthy check records, return true - return true - } - mgr.Logger.Error(err, "read check failed") - return false - } - return true -} - -func (mgr *Manager) WriteCheck(ctx context.Context, host string) bool { - writeSQL := fmt.Sprintf(` - create table if not exists kb_health_check(type int, check_ts timestamp, primary key(type)); - insert into kb_health_check values(%d, CURRENT_TIMESTAMP) on conflict(type) do update set check_ts = CURRENT_TIMESTAMP; - `, engines.CheckStatusType) - _, err := mgr.ExecWithHost(ctx, writeSQL, host) - if err != nil { - mgr.Logger.Error(err, "write check failed") - return false - } - return true -} - -func (mgr *Manager) PgReload(ctx context.Context) error { - reload := "select pg_reload_conf();" - - _, err := mgr.Exec(ctx, reload) - - return err -} - -func (mgr *Manager) IsPgReady(ctx context.Context) bool { - err := mgr.Pool.Ping(ctx) - if err != nil { - mgr.Logger.Error(err, "DB is not ready, ping failed") - return false - } - - return true -} - -func (mgr *Manager) Lock(ctx context.Context, reason string) error { - sql := "alter system set default_transaction_read_only=on;" - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("exec sql:%s failed", sql)) - return err - } - - if err = mgr.PgReload(ctx); err != nil { - mgr.Logger.Error(err, "reload conf failed") - return err - } - - mgr.Logger.Info(fmt.Sprintf("Lock db success: %s", reason)) - return nil -} - -func (mgr *Manager) Unlock(ctx context.Context) error { - sql := "alter system set default_transaction_read_only=off;" - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("exec sql:%s failed", sql)) - return err - } - - if err = mgr.PgReload(ctx); err != nil { - mgr.Logger.Error(err, "reload conf failed") - return err - } - - mgr.Logger.Info("UnLock db success") - return nil -} - -func (mgr *Manager) ShutDownWithWait() { - mgr.Pool.Close() -} diff --git a/pkg/lorry/engines/postgres/manager_test.go b/pkg/lorry/engines/postgres/manager_test.go deleted file mode 100644 index f28b330784d..00000000000 --- a/pkg/lorry/engines/postgres/manager_test.go +++ /dev/null @@ -1,343 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "context" - "fmt" - "testing" - - "github.com/pashagolub/pgxmock/v2" - "github.com/shirou/gopsutil/v3/process" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -func MockDatabase(t *testing.T) (*Manager, pgxmock.PgxPoolIface, error) { - properties := map[string]string{ - ConnectionURLKey: "user=test password=test host=localhost port=5432 dbname=postgres", - } - testConfig, err := NewConfig(properties) - assert.NotNil(t, testConfig) - assert.Nil(t, err) - - viper.Set(constant.KBEnvPodName, "test-pod-0") - viper.Set(constant.KBEnvClusterCompName, "test") - viper.Set(constant.KBEnvNamespace, "default") - viper.Set(PGDATA, "test") - mock, err := pgxmock.NewPool(pgxmock.MonitorPingsOption(true)) - if err != nil { - t.Fatal(err) - } - - dbManager, err := NewManager(engines.Properties(properties)) - if err != nil { - t.Fatal(err) - } - - manager := dbManager.(*Manager) - manager.Pool = mock - - return manager, mock, err -} - -func TestIsRunning(t *testing.T) { - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("proc is nil, can't read file", func(t *testing.T) { - isRunning := manager.IsRunning() - assert.False(t, isRunning) - }) - - t.Run("proc is not nil ,process is not exist", func(t *testing.T) { - manager.Proc = &process.Process{ - Pid: 100000, - } - - isRunning := manager.IsRunning() - assert.False(t, isRunning) - }) -} - -func TestNewProcessFromPidFile(t *testing.T) { - fs = afero.NewMemMapFs() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("file is not exist", func(t *testing.T) { - err := manager.newProcessFromPidFile() - assert.NotNil(t, err) - assert.ErrorContains(t, err, "file does not exist") - }) - - t.Run("process is not exist", func(t *testing.T) { - data := "100000\n/postgresql/data\n1692770488\n5432\n/var/run/postgresql\n*\n 2388960 4\nready" - err := afero.WriteFile(fs, manager.DataDir+"/postmaster.pid", []byte(data), 0644) - if err != nil { - t.Fatal(err) - } - - err = manager.newProcessFromPidFile() - assert.NotNil(t, err) - assert.ErrorContains(t, err, "process does not exist") - }) -} - -func TestReadWrite(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("write check success", func(t *testing.T) { - mock.ExpectExec(`create table if not exists`). - WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) - - ok := manager.WriteCheck(ctx, "") - assert.True(t, ok) - }) - - t.Run("write check failed", func(t *testing.T) { - mock.ExpectExec(`create table if not exists`). - WillReturnError(fmt.Errorf("some error")) - - ok := manager.WriteCheck(ctx, "") - assert.False(t, ok) - }) - - t.Run("read check success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"check_ts"}).AddRow(1)) - - ok := manager.ReadCheck(ctx, "") - assert.True(t, ok) - }) - - t.Run("read check failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - ok := manager.ReadCheck(ctx, "") - assert.False(t, ok) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestPgIsReady(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("pg is ready", func(t *testing.T) { - mock.ExpectPing() - - if isReady := manager.IsPgReady(ctx); !isReady { - t.Errorf("test pg is ready failed") - } - }) - - t.Run("pg is not ready", func(t *testing.T) { - mock.ExpectPing().WillReturnError(fmt.Errorf("can't ping to db")) - if isReady := manager.IsPgReady(ctx); isReady { - t.Errorf("expect pg is not ready, but get ready") - } - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestSetAndUnsetIsLeader(t *testing.T) { - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("set is leader", func(t *testing.T) { - manager.SetIsLeader(true) - isSet, isLeader := manager.GetIsLeader() - assert.True(t, isSet) - assert.True(t, isLeader) - }) - - t.Run("set is not leader", func(t *testing.T) { - manager.SetIsLeader(false) - isSet, isLeader := manager.GetIsLeader() - assert.True(t, isSet) - assert.False(t, isLeader) - }) - - t.Run("unset is leader", func(t *testing.T) { - manager.UnsetIsLeader() - isSet, isLeader := manager.GetIsLeader() - assert.False(t, isSet) - assert.False(t, isLeader) - }) -} - -func TestIsLeaderMember(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - currentMember := dcs.Member{ - Name: manager.CurrentMemberName, - } - - t.Run("member is nil", func(t *testing.T) { - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, nil) - assert.False(t, isLeaderMember) - assert.NotNil(t, err) - }) - - t.Run("leader member is nil", func(t *testing.T) { - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, ¤tMember) - assert.False(t, isLeaderMember) - assert.NotNil(t, err) - }) - - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - cluster.Members = append(cluster.Members, currentMember) - t.Run("is leader member", func(t *testing.T) { - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, ¤tMember) - assert.True(t, isLeaderMember) - assert.Nil(t, err) - }) - - member := &dcs.Member{ - Name: "test", - } - t.Run("is not leader member", func(t *testing.T) { - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, member) - assert.False(t, isLeaderMember) - assert.Nil(t, err) - }) -} - -func TestPgReload(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("pg reload success", func(t *testing.T) { - mock.ExpectExec("select pg_reload_conf()"). - WillReturnResult(pgxmock.NewResult("select", 1)) - - err := manager.PgReload(ctx) - assert.Nil(t, err) - }) - - t.Run("pg reload failed", func(t *testing.T) { - mock.ExpectExec("select pg_reload_conf()"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.PgReload(ctx) - assert.NotNil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestLock(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("alter system failed", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnError(fmt.Errorf("alter system failed")) - - err := manager.Lock(ctx, "test") - assert.NotNil(t, err) - assert.ErrorContains(t, err, "alter system failed") - }) - - t.Run("pg reload failed", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter", 1)) - mock.ExpectExec("select pg_reload_conf()"). - WillReturnError(fmt.Errorf("pg reload failed")) - err := manager.Lock(ctx, "test") - assert.NotNil(t, err) - assert.ErrorContains(t, err, "pg reload failed") - }) - - t.Run("lock success", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter", 1)) - mock.ExpectExec("select pg_reload_conf()"). - WillReturnResult(pgxmock.NewResult("select", 1)) - err := manager.Lock(ctx, "test") - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestUnlock(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("alter system failed", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnError(fmt.Errorf("alter system failed")) - - err := manager.Unlock(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "alter system failed") - }) - - t.Run("pg reload failed", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter", 1)) - mock.ExpectExec("select pg_reload_conf()"). - WillReturnError(fmt.Errorf("pg reload failed")) - err := manager.Unlock(ctx) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "pg reload failed") - }) - - t.Run("unlock success", func(t *testing.T) { - mock.ExpectExec("alter system"). - WillReturnResult(pgxmock.NewResult("alter", 1)) - mock.ExpectExec("select pg_reload_conf()"). - WillReturnResult(pgxmock.NewResult("select", 1)) - err := manager.Unlock(ctx) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/postgres/officalpostgres/get_replica_role.go b/pkg/lorry/engines/postgres/officalpostgres/get_replica_role.go deleted file mode 100644 index 1cbc3040136..00000000000 --- a/pkg/lorry/engines/postgres/officalpostgres/get_replica_role.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package officalpostgres - -import ( - "context" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, cluster *dcs.Cluster) (string, error) { - return mgr.GetMemberRoleWithHost(ctx, "") -} diff --git a/pkg/lorry/engines/postgres/officalpostgres/manager.go b/pkg/lorry/engines/postgres/officalpostgres/manager.go deleted file mode 100644 index 029f9c2ff6a..00000000000 --- a/pkg/lorry/engines/postgres/officalpostgres/manager.go +++ /dev/null @@ -1,942 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package officalpostgres - -import ( - "bufio" - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/cast" - "golang.org/x/exp/slices" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres" -) - -type Manager struct { - postgres.Manager - syncStandbys *postgres.PGStandby - recoveryParams map[string]map[string]string - pgControlData map[string]string -} - -var _ engines.DBManager = &Manager{} - -var Mgr *Manager - -var fs = afero.NewOsFs() - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - Mgr = &Manager{} - - baseManager, err := postgres.NewManager(properties) - if err != nil { - return nil, errors.Errorf("new base manager failed, err: %v", err) - } - - Mgr.Manager = *baseManager.(*postgres.Manager) - return Mgr, nil -} - -func (mgr *Manager) InitializeCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) IsCurrentMemberInCluster(context.Context, *dcs.Cluster) bool { - return true -} - -func (mgr *Manager) JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) LeaveMemberFromCluster(context.Context, *dcs.Cluster, string) error { - return nil -} - -func (mgr *Manager) IsClusterInitialized(context.Context, *dcs.Cluster) (bool, error) { - // for replication, the setup script imposes a constraint where the successful startup of the primary database (db0) - // is a prerequisite for the successful launch of the remaining databases. - return mgr.IsDBStartupReady(), nil -} - -func (mgr *Manager) cleanDBState() { - mgr.UnsetIsLeader() - mgr.recoveryParams = nil - mgr.syncStandbys = nil - mgr.pgControlData = nil - mgr.DBState = &dcs.DBState{ - Extra: map[string]string{}, - } -} - -func (mgr *Manager) GetDBState(ctx context.Context, cluster *dcs.Cluster) *dcs.DBState { - mgr.cleanDBState() - - isLeader, err := mgr.IsLeader(ctx, cluster) - if err != nil { - mgr.Logger.Error(err, "check is leader failed") - return nil - } - mgr.SetIsLeader(isLeader) - - replicationMode, err := mgr.getReplicationMode(ctx) - if err != nil { - mgr.Logger.Error(err, "get replication mode failed") - return nil - } - mgr.DBState.Extra[postgres.ReplicationMode] = replicationMode - - if replicationMode == postgres.Synchronous && cluster.Leader != nil && cluster.Leader.Name == mgr.CurrentMemberName { - syncStandbys := mgr.getSyncStandbys(ctx) - if syncStandbys != nil { - mgr.syncStandbys = syncStandbys - mgr.DBState.Extra[postgres.SyncStandBys] = strings.Join(syncStandbys.Members.ToSlice(), ",") - } - } - - walPosition, err := mgr.getWalPositionWithHost(ctx, "") - if err != nil { - mgr.Logger.Error(err, "get wal position failed") - return nil - } - mgr.DBState.OpTimestamp = walPosition - - timeLine := mgr.getTimeLineWithHost(ctx, "") - if timeLine == 0 { - mgr.Logger.Error(err, "get received timeLine failed") - return nil - } - mgr.DBState.Extra[postgres.TimeLine] = strconv.FormatInt(timeLine, 10) - - if !isLeader { - recoveryParams, err := mgr.readRecoveryParams(ctx) - if err != nil { - mgr.Logger.Error(nil, "get recoveryParams failed", "err", err) - return nil - } - mgr.recoveryParams = recoveryParams - } - - pgControlData := mgr.getPgControlData() - if pgControlData == nil { - mgr.Logger.Error(err, "get pg controlData failed") - return nil - } - mgr.pgControlData = pgControlData - - return mgr.DBState -} - -func (mgr *Manager) IsLeader(ctx context.Context, _ *dcs.Cluster) (bool, error) { - isSet, isLeader := mgr.GetIsLeader() - if isSet { - return isLeader, nil - } - - return mgr.IsLeaderWithHost(ctx, "") -} - -func (mgr *Manager) IsLeaderWithHost(ctx context.Context, host string) (bool, error) { - role, err := mgr.GetMemberRoleWithHost(ctx, host) - if err != nil { - return false, errors.Errorf("check is leader with host:%s failed, err:%v", host, err) - } - - mgr.Logger.Info(fmt.Sprintf("get member:%s role:%s", host, role)) - return role == models.PRIMARY, nil -} - -func (mgr *Manager) IsDBStartupReady() bool { - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - if mgr.DBStartupReady { - return true - } - - if !mgr.IsPgReady(ctx) { - return false - } - - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func (mgr *Manager) GetMemberRoleWithHost(ctx context.Context, host string) (string, error) { - sql := "select pg_is_in_recovery();" - - resp, err := mgr.QueryWithHost(ctx, sql, host) - if err != nil { - mgr.Logger.Error(err, "get member role failed") - return "", err - } - - result, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, "parse member role failed") - return "", err - } - - if cast.ToBool(result[0]["pg_is_in_recovery"]) { - return models.SECONDARY, nil - } else { - return models.PRIMARY, nil - } -} - -func (mgr *Manager) GetMemberAddrs(_ context.Context, cluster *dcs.Cluster) []string { - return cluster.GetMemberAddrs() -} - -func (mgr *Manager) IsCurrentMemberHealthy(ctx context.Context, cluster *dcs.Cluster) bool { - return mgr.IsMemberHealthy(ctx, cluster, cluster.GetMemberWithName(mgr.CurrentMemberName)) -} - -func (mgr *Manager) IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool { - var host string - if member.Name != mgr.CurrentMemberName { - host = cluster.GetMemberAddr(*member) - } - - if cluster.Leader != nil && cluster.Leader.Name == member.Name { - if !mgr.WriteCheck(ctx, host) { - return false - } - } - if !mgr.ReadCheck(ctx, host) { - return false - } - - return true -} - -func (mgr *Manager) IsMemberLagging(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, int64) { - if cluster.Leader == nil || cluster.Leader.DBState == nil { - mgr.Logger.Info("No leader DBState info") - return false, 0 - } - maxLag := cluster.HaConfig.GetMaxLagOnSwitchover() - - var host string - if member.Name != mgr.CurrentMemberName { - host = cluster.GetMemberAddr(*member) - } - - replicationMode, err := mgr.getReplicationMode(ctx) - if err != nil { - mgr.Logger.Error(err, "get db replication mode failed") - return true, maxLag + 1 - } - - if replicationMode == postgres.Synchronous { - if !mgr.checkStandbySynchronizedToLeader(true, cluster) { - return true, maxLag + 1 - } - } - - timeLine := mgr.getTimeLineWithHost(ctx, host) - if timeLine == 0 { - mgr.Logger.Error(err, "get timeline with host:%s failed") - return true, maxLag + 1 - } - clusterTimeLine := cast.ToInt64(cluster.Leader.DBState.Extra[postgres.TimeLine]) - if clusterTimeLine != 0 && clusterTimeLine != timeLine { - return true, maxLag + 1 - } - - walPosition, err := mgr.getWalPositionWithHost(ctx, host) - if err != nil { - mgr.Logger.Error(err, "check member lagging failed") - return true, maxLag + 1 - } - - return cluster.Leader.DBState.OpTimestamp-walPosition > maxLag, cluster.Leader.DBState.OpTimestamp - walPosition -} - -// Typically, the synchronous_commit parameter remains consistent between the primary and standby -func (mgr *Manager) getReplicationMode(ctx context.Context) (string, error) { - if mgr.DBState != nil && mgr.DBState.Extra[postgres.ReplicationMode] != "" { - return mgr.DBState.Extra[postgres.ReplicationMode], nil - } - - synchronousCommit, err := mgr.GetPgCurrentSetting(ctx, "synchronous_commit") - if err != nil { - return "", err - } - - switch synchronousCommit { - case "off": - return postgres.Asynchronous, nil - case "local": - return postgres.Asynchronous, nil - case "remote_write": - return postgres.Asynchronous, nil - case "on": - return postgres.Synchronous, nil - case "remote_apply": - return postgres.Synchronous, nil - default: // default "on" - return postgres.Synchronous, nil - } -} - -func (mgr *Manager) getWalPositionWithHost(ctx context.Context, host string) (int64, error) { - if mgr.DBState != nil && mgr.DBState.OpTimestamp != 0 && host == "" { - return mgr.DBState.OpTimestamp, nil - } - - var ( - lsn int64 - isLeader bool - err error - ) - - if host == "" { - isLeader, err = mgr.IsLeader(ctx, nil) - } else { - isLeader, err = mgr.IsLeaderWithHost(ctx, host) - } - if err != nil { - return 0, err - } - - if isLeader { - lsn, err = mgr.getLsnWithHost(ctx, "current", host) - if err != nil { - return 0, err - } - } else { - replayLsn, errReplay := mgr.getLsnWithHost(ctx, "replay", host) - receiveLsn, errReceive := mgr.getLsnWithHost(ctx, "receive", host) - if errReplay != nil && errReceive != nil { - return 0, errors.Errorf("get replayLsn or receiveLsn failed, replayLsn err:%v, receiveLsn err:%v", errReplay, errReceive) - } - lsn = engines.MaxInt64(replayLsn, receiveLsn) - } - - return lsn, nil -} - -func (mgr *Manager) getLsnWithHost(ctx context.Context, types string, host string) (int64, error) { - var sql string - switch types { - case "current": - sql = "select pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), '0/0')::bigint;" - case "replay": - sql = "select pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_last_wal_replay_lsn(), '0/0')::bigint;" - case "receive": - sql = "select pg_catalog.pg_wal_lsn_diff(COALESCE(pg_catalog.pg_last_wal_receive_lsn(), '0/0'), '0/0')::bigint;" - } - - resp, err := mgr.QueryWithHost(ctx, sql, host) - if err != nil { - mgr.Logger.Error(err, "get wal position failed") - return 0, err - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - return 0, errors.Errorf("parse query response:%s failed, err:%v", string(resp), err) - } - - return cast.ToInt64(resMap[0]["pg_wal_lsn_diff"]), nil -} - -// only the leader has this information. -func (mgr *Manager) getSyncStandbys(ctx context.Context) *postgres.PGStandby { - if mgr.syncStandbys != nil { - return mgr.syncStandbys - } - - synchronousStandbyNames, err := mgr.GetPgCurrentSetting(ctx, "synchronous_standby_names") - if err != nil { - mgr.Logger.Error(err, "get synchronous_standby_names failed") - return nil - } - - syncStandbys, err := postgres.ParsePGSyncStandby(synchronousStandbyNames) - if err != nil { - mgr.Logger.Error(err, "parse pg sync standby failed") - return nil - } - return syncStandbys -} - -func (mgr *Manager) checkStandbySynchronizedToLeader(checkLeader bool, cluster *dcs.Cluster) bool { - if cluster.Leader == nil || cluster.Leader.DBState == nil { - return false - } - syncStandBysStr := cluster.Leader.DBState.Extra[postgres.SyncStandBys] - syncStandBys := strings.Split(syncStandBysStr, ",") - - return (checkLeader && mgr.CurrentMemberName == cluster.Leader.Name) || slices.Contains(syncStandBys, mgr.CurrentMemberName) -} - -func (mgr *Manager) handleRewind(ctx context.Context, cluster *dcs.Cluster) error { - needRewind := mgr.checkTimelineAndLsn(ctx, cluster) - if !needRewind { - return nil - } - - if !mgr.canRewind() { - return nil - } - - return mgr.executeRewind(ctx) -} - -func (mgr *Manager) canRewind() bool { - _, err := postgres.PgRewind("--help") - if err != nil { - mgr.Logger.Error(err, "unable to execute pg_rewind") - return false - } - - pgControlData := mgr.getPgControlData() - if pgControlData["wal_log_hints setting"] != "on" && pgControlData["Data page checksum version"] == "0" { - mgr.Logger.Info("unable to execute pg_rewind due to configuration not allowed") - } - - return true -} - -func (mgr *Manager) executeRewind(ctx context.Context) error { - if mgr.IsRunning() { - return errors.New("can't run rewind when pg is running") - } - - err := mgr.checkArchiveReadyWal(ctx) - if err != nil { - return err - } - - // TODO: checkpoint - - return nil -} - -func (mgr *Manager) checkArchiveReadyWal(ctx context.Context) error { - archiveMode, _ := mgr.GetPgCurrentSetting(ctx, "archive_mode") - archiveCommand, _ := mgr.GetPgCurrentSetting(ctx, "archive_command") - - if (archiveMode != "on" && archiveMode != "always") || archiveCommand == "" { - mgr.Logger.Info("archive is not enabled") - return nil - } - - // starting from PostgreSQL 10, the "wal" directory has been renamed to "pg_wal" - archiveDir := mgr.DataDir + "pg_wal/archive_status" - var walFileList []string - err := filepath.Walk(archiveDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - fileName := strings.Split(info.Name(), ".") - if len(fileName) == 2 && fileName[1] == "ready" { - walFileList = append(walFileList, fileName[0]) - } - - return nil - }) - if err != nil { - return err - } - if len(walFileList) == 0 { - mgr.Logger.Info("no ready wal file exist") - return nil - } - - sort.Strings(walFileList) - for _, wal := range walFileList { - walFileName := archiveDir + wal + ".ready" - _, err = postgres.ExecCommand(buildArchiverCommand(archiveCommand, wal, archiveDir)) - if err != nil { - return err - } - - err = fs.Rename(walFileName, archiveDir+wal+".done") - if err != nil { - return err - } - } - - return nil -} - -func buildArchiverCommand(archiveCommand, walFileName, walDir string) string { - cmd := "" - - i := 0 - archiveCommandLength := len(archiveCommand) - for i < archiveCommandLength { - if archiveCommand[i] == '%' && i+1 < archiveCommandLength { - i += 1 - switch archiveCommand[i] { - case 'p': - cmd += walDir + walFileName - case 'f': - cmd += walFileName - case 'r': - cmd += "000000010000000000000001" - case '%': - cmd += "%" - default: - cmd += "%" - i -= 1 - } - } else { - cmd += string(archiveCommand[i]) - } - i += 1 - } - - return cmd -} - -func (mgr *Manager) checkTimelineAndLsn(ctx context.Context, cluster *dcs.Cluster) bool { - var needRewind bool - var history *postgres.HistoryFile - - isRecovery, localTimeLine, localLsn := mgr.getLocalTimeLineAndLsn(ctx) - if localTimeLine == 0 || localLsn == 0 { - return false - } - - isLeader, err := mgr.IsLeaderWithHost(ctx, cluster.GetMemberAddr(*cluster.GetLeaderMember())) - if err != nil || !isLeader { - mgr.Logger.Error(nil, "Leader is still in recovery and can't rewind") - return false - } - - primaryTimeLine, err := mgr.getPrimaryTimeLine(cluster.GetMemberAddr(*cluster.GetLeaderMember())) - if err != nil { - mgr.Logger.Error(err, "get primary timeLine failed") - return false - } - - switch { - case localTimeLine > primaryTimeLine: - needRewind = true - case localTimeLine == primaryTimeLine: - needRewind = false - case localTimeLine < primaryTimeLine: - history = mgr.getHistory(cluster.GetMemberAddr(*cluster.GetLeaderMember()), primaryTimeLine) - } - - if len(history.History) != 0 { - // use a boolean value to check if the loop should exit early - exitFlag := false - for _, h := range history.History { - // Don't need to rewind just when: - // for replica: replayed location is not ahead of switchpoint - // for the former primary: end of checkpoint record is the same as switchpoint - if h.ParentTimeline == localTimeLine { - switch { - case isRecovery: - needRewind = localLsn > h.SwitchPoint - case localLsn >= h.SwitchPoint: - needRewind = true - default: - checkPointEnd := mgr.getCheckPointEnd(localTimeLine, localLsn) - needRewind = h.SwitchPoint != checkPointEnd - } - exitFlag = true - break - } else if h.ParentTimeline > localTimeLine { - needRewind = true - exitFlag = true - break - } - } - if !exitFlag { - needRewind = true - } - } - - return needRewind -} - -func (mgr *Manager) getCheckPointEnd(timeLine, lsn int64) int64 { - lsnStr := postgres.FormatPgLsn(lsn) - - resp, err := postgres.PgWalDump("-t", strconv.FormatInt(timeLine, 10), "-s", lsnStr, "-n", "2") - if err == nil || resp == "" { - return 0 - } - - checkPointEndStr := postgres.ParsePgWalDumpError(err.Error(), lsnStr) - - return postgres.ParsePgLsn(checkPointEndStr) -} - -func (mgr *Manager) getPrimaryTimeLine(host string) (int64, error) { - resp, err := postgres.Psql("-h", host, "replication=database", "-c", "IDENTIFY_SYSTEM") - if err != nil { - mgr.Logger.Error(err, "get primary time line failed") - return 0, err - } - - stdoutList := strings.Split(resp, "\n") - value := stdoutList[2] - values := strings.Split(value, "|") - - primaryTimeLine := strings.TrimSpace(values[1]) - - return strconv.ParseInt(primaryTimeLine, 10, 64) -} - -func (mgr *Manager) getLocalTimeLineAndLsn(ctx context.Context) (bool, int64, int64) { - var inRecovery bool - - if !mgr.IsRunning() { - return mgr.getLocalTimeLineAndLsnFromControlData() - } - - inRecovery = true - timeLine := mgr.getReceivedTimeLine(ctx, "") - lsn, _ := mgr.getLsnWithHost(ctx, "replay", "") - - return inRecovery, timeLine, lsn -} - -func (mgr *Manager) getLocalTimeLineAndLsnFromControlData() (bool, int64, int64) { - var inRecovery bool - var timeLineStr, lsnStr string - var timeLine, lsn int64 - - pgControlData := mgr.getPgControlData() - if slices.Contains([]string{"shut down in recovery", "in archive recovery"}, (pgControlData)["Database cluster state"]) { - inRecovery = true - lsnStr = (pgControlData)["Minimum recovery ending location"] - timeLineStr = (pgControlData)["Min recovery ending loc's timeline"] - } else if (pgControlData)["Database cluster state"] == "shut down" { - inRecovery = false - lsnStr = (pgControlData)["Latest checkpoint location"] - timeLineStr = (pgControlData)["Latest checkpoint's TimeLineID"] - } - - if lsnStr != "" { - lsn = postgres.ParsePgLsn(lsnStr) - } - if timeLineStr != "" { - timeLine, _ = strconv.ParseInt(timeLineStr, 10, 64) - } - - return inRecovery, timeLine, lsn -} - -func (mgr *Manager) getTimeLineWithHost(ctx context.Context, host string) int64 { - if mgr.DBState != nil && mgr.DBState.Extra[postgres.TimeLine] != "" && host == "" { - return cast.ToInt64(mgr.DBState.Extra[postgres.TimeLine]) - } - - var isLeader bool - var err error - if host == "" { - isLeader, err = mgr.IsLeader(ctx, nil) - } else { - isLeader, err = mgr.IsLeaderWithHost(ctx, host) - } - if err != nil { - mgr.Logger.Error(err, "get timeLine check leader failed") - return 0 - } - - if isLeader { - return mgr.getCurrentTimeLine(ctx, host) - } else { - return mgr.getReceivedTimeLine(ctx, host) - } -} - -func (mgr *Manager) getCurrentTimeLine(ctx context.Context, host string) int64 { - sql := "SELECT timeline_id FROM pg_control_checkpoint();" - resp, err := mgr.QueryWithHost(ctx, sql, host) - if err != nil || resp == nil { - mgr.Logger.Error(err, "get current timeline failed") - return 0 - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, "parse query response failed", "response", string(resp)) - return 0 - } - - return cast.ToInt64(resMap[0]["timeline_id"]) -} - -func (mgr *Manager) getReceivedTimeLine(ctx context.Context, host string) int64 { - sql := "select case when latest_end_lsn is null then null " + - "else received_tli end as received_tli from pg_catalog.pg_stat_get_wal_receiver();" - resp, err := mgr.QueryWithHost(ctx, sql, host) - if err != nil || resp == nil { - mgr.Logger.Error(err, "get received timeline failed") - return 0 - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query response:%s failed", string(resp))) - return 0 - } - - return cast.ToInt64(resMap[0]["received_tli"]) -} - -func (mgr *Manager) getPgControlData() map[string]string { - if mgr.pgControlData != nil { - return mgr.pgControlData - } - - result := map[string]string{} - - resp, err := postgres.ExecCommand("pg_controldata") - if err != nil { - mgr.Logger.Error(err, "get pg control data failed") - return nil - } - - controlDataList := strings.Split(resp, "\n") - for _, s := range controlDataList { - out := strings.Split(s, ":") - if len(out) == 2 { - result[out[0]] = strings.TrimSpace(out[1]) - } - } - return result -} - -func (mgr *Manager) checkRecoveryConf(ctx context.Context, leaderName string) (bool, bool) { - if mgr.MajorVersion >= 12 { - _, err := fs.Stat(mgr.DataDir + "/standby.signal") - if errors.Is(err, afero.ErrFileNotFound) { - return true, true - } - } else { - mgr.Logger.Info("check recovery conf") - // TODO: support check recovery.conf - } - - recoveryParams, err := mgr.readRecoveryParams(ctx) - if err != nil { - return true, true - } - - if !strings.HasPrefix(recoveryParams[postgres.PrimaryConnInfo]["host"], leaderName) { - if recoveryParams[postgres.PrimaryConnInfo]["context"] == "postmaster" { - mgr.Logger.Info("host not match, need to restart") - return true, true - } else { - mgr.Logger.Info("host not match, need to reload") - return true, false - } - } - - return false, false -} - -func (mgr *Manager) readRecoveryParams(ctx context.Context) (map[string]map[string]string, error) { - if mgr.recoveryParams != nil { - return mgr.recoveryParams, nil - } - - sql := fmt.Sprintf(`SELECT name, setting, context FROM pg_catalog.pg_settings WHERE pg_catalog.lower(name) = '%s';`, postgres.PrimaryConnInfo) - resp, err := mgr.Query(ctx, sql) - if err != nil { - return nil, err - } - - resMap, err := postgres.ParseQuery(string(resp)) - if err != nil { - return nil, err - } - - primaryConnInfo := postgres.ParsePrimaryConnInfo(cast.ToString(resMap[0]["setting"])) - primaryConnInfo["context"] = cast.ToString(resMap[0]["context"]) - - return map[string]map[string]string{ - postgres.PrimaryConnInfo: primaryConnInfo, - }, nil -} - -func (mgr *Manager) getHistory(host string, timeline int64) *postgres.HistoryFile { - resp, err := postgres.Psql("-h", host, "replication=database", "-c", fmt.Sprintf("TIMELINE_HISTORY %d", timeline)) - if err != nil { - mgr.Logger.Error(err, "get history failed") - return nil - } - - return postgres.ParseHistory(resp) -} - -func (mgr *Manager) Promote(ctx context.Context, _ *dcs.Cluster) error { - if isLeader, err := mgr.IsLeader(ctx, nil); isLeader && err == nil { - mgr.Logger.Info("i am already the leader, don't need to promote") - return nil - } - - resp, err := postgres.PgCtl("promote") - if err != nil { - mgr.Logger.Error(err, "promote failed") - return err - } - - mgr.Logger.Info("promote success", "response", resp) - return nil -} - -func (mgr *Manager) Demote(ctx context.Context) error { - mgr.Logger.Info(fmt.Sprintf("current member demoting: %s", mgr.CurrentMemberName)) - if isLeader, err := mgr.IsLeader(ctx, nil); !isLeader && err == nil { - mgr.Logger.Info("i am not the leader, don't need to demote") - return nil - } - - return mgr.Stop() -} - -func (mgr *Manager) Stop() error { - err := mgr.DBManagerBase.Stop() - if err != nil { - return err - } - - _, err = postgres.PgCtl("stop -m fast") - if err != nil { - mgr.Logger.Error(err, "pg_ctl stop failed") - return err - } - - return nil -} - -func (mgr *Manager) Follow(ctx context.Context, cluster *dcs.Cluster) error { - // only when db is not running, leader probably be nil - if cluster.Leader == nil { - mgr.Logger.Info("cluster has no leader now, starts db firstly without following") - return nil - } - - err := mgr.handleRewind(ctx, cluster) - if err != nil { - mgr.Logger.Error(err, "handle rewind failed") - return err - } - - needChange, needRestart := mgr.checkRecoveryConf(ctx, cluster.Leader.Name) - if needChange { - return mgr.follow(ctx, needRestart, cluster) - } - - mgr.Logger.Info(fmt.Sprintf("no action coz i still follow the leader:%s", cluster.Leader.Name)) - return nil -} - -func (mgr *Manager) follow(ctx context.Context, needRestart bool, cluster *dcs.Cluster) error { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - mgr.Logger.Info("cluster has no leader now, just start if need") - if needRestart { - return mgr.DBManagerBase.Start(ctx, cluster) - } - return nil - } - - if mgr.CurrentMemberName == leaderMember.Name { - mgr.Logger.Info("i get the leader key, don't need to follow") - return nil - } - - primaryInfo := fmt.Sprintf("\nprimary_conninfo = 'host=%s port=%s user=%s password=%s application_name=%s'", - cluster.GetMemberAddr(*leaderMember), leaderMember.DBPort, mgr.Config.Username, mgr.Config.Password, mgr.CurrentMemberName) - - pgConf, err := fs.OpenFile("/kubeblocks/conf/postgresql.conf", os.O_APPEND|os.O_RDWR, 0644) - if err != nil { - mgr.Logger.Error(err, "open postgresql.conf failed") - return err - } - defer func() { - _ = pgConf.Close() - }() - - writer := bufio.NewWriter(pgConf) - _, err = writer.WriteString(primaryInfo) - if err != nil { - mgr.Logger.Error(err, "write into postgresql.conf failed") - return err - } - - err = writer.Flush() - if err != nil { - mgr.Logger.Error(err, "writer flush failed") - return err - } - - if !needRestart { - if err = mgr.PgReload(ctx); err != nil { - mgr.Logger.Error(err, "reload conf failed") - return err - } - return nil - } - - return mgr.DBManagerBase.Start(ctx, cluster) -} - -// Start for postgresql replication, not only means the start of a database instance -// but also signifies its launch as a follower in the cluster, following the leader. -func (mgr *Manager) Start(ctx context.Context, cluster *dcs.Cluster) error { - err := mgr.follow(ctx, true, cluster) - if err != nil { - mgr.Logger.Error(err, "start failed") - return err - } - return nil -} - -func (mgr *Manager) HasOtherHealthyLeader(context.Context, *dcs.Cluster) *dcs.Member { - return nil -} - -func (mgr *Manager) HasOtherHealthyMembers(ctx context.Context, cluster *dcs.Cluster, leader string) []*dcs.Member { - members := make([]*dcs.Member, 0) - - for i, m := range cluster.Members { - if m.Name != leader && mgr.IsMemberHealthy(ctx, cluster, &m) { - members = append(members, &cluster.Members[i]) - } - } - - return members -} diff --git a/pkg/lorry/engines/postgres/officalpostgres/manager_test.go b/pkg/lorry/engines/postgres/officalpostgres/manager_test.go deleted file mode 100644 index 8ac10f0baeb..00000000000 --- a/pkg/lorry/engines/postgres/officalpostgres/manager_test.go +++ /dev/null @@ -1,1049 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package officalpostgres - -import ( - "bytes" - "context" - "fmt" - "strings" - "testing" - - "github.com/pashagolub/pgxmock/v2" - "github.com/shirou/gopsutil/v3/process" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -func MockDatabase(t *testing.T) (*Manager, pgxmock.PgxPoolIface, error) { - properties := map[string]string{ - postgres.ConnectionURLKey: "user=test password=test host=localhost port=5432 dbname=postgres", - } - testConfig, err := postgres.NewConfig(properties) - assert.NotNil(t, testConfig) - assert.Nil(t, err) - - viper.Set(constant.KBEnvPodName, "test-pod-0") - viper.Set(constant.KBEnvClusterCompName, "test") - viper.Set(constant.KBEnvNamespace, "default") - viper.Set(postgres.PGDATA, "test") - viper.Set(postgres.PGMAJOR, 14) - mock, err := pgxmock.NewPool(pgxmock.MonitorPingsOption(true)) - if err != nil { - t.Fatal(err) - } - - dbManager, err := NewManager(engines.Properties(properties)) - if err != nil { - t.Fatal(err) - } - manager := dbManager.(*Manager) - manager.Pool = mock - - return manager, mock, err -} - -func TestIsLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("get member role primary", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(false)) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.Nil(t, err) - assert.Equal(t, true, isLeader) - }) - - t.Run("get member role secondary", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(true)) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.Nil(t, err) - assert.Equal(t, false, isLeader) - }) - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.NotNil(t, err) - assert.Equal(t, false, isLeader) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"})) - isLeader, err := manager.IsLeader(ctx, nil) - assert.NotNil(t, err) - assert.Equal(t, false, isLeader) - }) - - t.Run("has set isLeader", func(t *testing.T) { - manager.SetIsLeader(true) - isLeader, err := manager.IsLeader(ctx, nil) - assert.Nil(t, err) - assert.Equal(t, true, isLeader) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsClusterInitialized(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - - t.Run("DBStartup is set Ready", func(t *testing.T) { - manager.DBStartupReady = true - - isInitialized, err := manager.IsClusterInitialized(ctx, cluster) - if err != nil { - t.Errorf("expect check is cluster initialized success but failed") - } - - assert.True(t, isInitialized) - manager.DBStartupReady = false - }) - - t.Run("DBStartup is not set ready and ping success", func(t *testing.T) { - mock.ExpectPing() - isInitialized, err := manager.IsClusterInitialized(ctx, cluster) - if err != nil { - t.Errorf("expect check is cluster initialized success but failed") - } - - if err = mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } - - assert.True(t, isInitialized) - manager.DBStartupReady = false - }) - - t.Run("DBStartup is not set ready but ping failed", func(t *testing.T) { - isInitialized, err := manager.IsClusterInitialized(ctx, cluster) - if err != nil { - t.Errorf("expect check is cluster initialized success but failed") - } - - assert.False(t, isInitialized) - manager.DBStartupReady = false - }) -} - -func TestGetMemberAddrs(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{Namespace: "default"} - - t.Run("get empty addrs", func(t *testing.T) { - addrs := manager.GetMemberAddrs(ctx, cluster) - - assert.Equal(t, []string{}, addrs) - }) - - t.Run("get addrs", func(t *testing.T) { - cluster.ClusterCompName = "pg" - cluster.Members = append(cluster.Members, dcs.Member{ - Name: "test-0", - DBPort: "5432", - }) - addrs := manager.GetMemberAddrs(ctx, cluster) - - assert.Equal(t, 1, len(addrs)) - assert.Equal(t, "test-0.test-headless.default.svc.cluster.local:5432", addrs[0]) - }) -} - -func TestIsCurrentMemberHealthy(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - Name: manager.CurrentMemberName, - }, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("current member is healthy", func(t *testing.T) { - mock.ExpectExec(`create table if not exists`). - WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"check_ts"}).AddRow(1)) - - isCurrentMemberHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.True(t, isCurrentMemberHealthy) - }) - - t.Run("write check failed", func(t *testing.T) { - mock.ExpectExec(`create table if not exists`). - WillReturnError(fmt.Errorf("some error")) - - isCurrentMemberHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.False(t, isCurrentMemberHealthy) - }) - - t.Run("read check failed", func(t *testing.T) { - cluster.Leader.Name = "test" - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - isCurrentMemberHealthy := manager.IsCurrentMemberHealthy(ctx, cluster) - assert.False(t, isCurrentMemberHealthy) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetReplicationMode(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - values := []string{"off", "local", "remote_write", "remote_apply", "on", ""} - expects := []string{postgres.Asynchronous, postgres.Asynchronous, postgres.Asynchronous, postgres.Synchronous, postgres.Synchronous, postgres.Synchronous} - manager.DBState = &dcs.DBState{ - Extra: map[string]string{}, - } - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"})) - - res, err := manager.getReplicationMode(ctx) - assert.NotNil(t, err) - assert.Equal(t, "", res) - }) - - t.Run("synchronous_commit has not been set", func(t *testing.T) { - for i, v := range values { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow(v)) - - res, err := manager.getReplicationMode(ctx) - assert.Nil(t, err) - assert.Equal(t, expects[i], res) - } - }) - - t.Run("synchronous_commit has been set", func(t *testing.T) { - for i, v := range expects { - manager.DBState.Extra[postgres.ReplicationMode] = v - res, err := manager.getReplicationMode(ctx) - assert.Nil(t, err) - assert.Equal(t, expects[i], res) - } - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetWalPositionWithHost(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("check is leader failed", func(t *testing.T) { - res, err := manager.getWalPositionWithHost(ctx, "test") - assert.NotNil(t, err) - assert.Zero(t, res) - }) - - t.Run("get primary wal position success", func(t *testing.T) { - manager.SetIsLeader(true) - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - - res, err := manager.getWalPositionWithHost(ctx, "") - assert.Nil(t, err) - assert.Equal(t, int64(23454272), res) - }) - - t.Run("get secondary wal position success", func(t *testing.T) { - manager.SetIsLeader(false) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - mock.ExpectQuery("pg_catalog.pg_last_wal_receive_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454273)) - - res, err := manager.getWalPositionWithHost(ctx, "") - assert.Nil(t, err) - assert.Equal(t, int64(23454273), res) - }) - - t.Run("get primary wal position failed", func(t *testing.T) { - manager.SetIsLeader(true) - manager.DBState = &dcs.DBState{} - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnError(fmt.Errorf("some error")) - - res, err := manager.getWalPositionWithHost(ctx, "") - assert.NotNil(t, err) - assert.Zero(t, res) - }) - - t.Run("get secondary wal position failed", func(t *testing.T) { - manager.SetIsLeader(false) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnError(fmt.Errorf("some error")) - mock.ExpectQuery("pg_catalog.pg_last_wal_receive_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"})) - - res, err := manager.getWalPositionWithHost(ctx, "") - assert.NotNil(t, err) - assert.Zero(t, res) - }) - - t.Run("op time has been set", func(t *testing.T) { - manager.DBState = &dcs.DBState{ - OpTimestamp: 100, - } - - res, err := manager.getWalPositionWithHost(ctx, "") - assert.Nil(t, err) - assert.Equal(t, int64(100), res) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetSyncStandbys(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - standbys := manager.getSyncStandbys(ctx) - assert.Nil(t, standbys) - }) - - t.Run("parse pg sync standby failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow(`ANY 4("a" b,"c c")`)) - - standbys := manager.getSyncStandbys(ctx) - assert.Nil(t, standbys) - }) - - t.Run("get sync standbys success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow(`ANY 4("a",*,b)`)) - - standbys := manager.getSyncStandbys(ctx) - assert.NotNil(t, standbys) - assert.True(t, standbys.HasStar) - assert.True(t, standbys.Members.Contains("a")) - assert.Equal(t, 4, standbys.Amount) - }) - - t.Run("pg sync standbys has been set", func(t *testing.T) { - manager.DBState = &dcs.DBState{} - manager.syncStandbys = &postgres.PGStandby{ - HasStar: true, - Amount: 3, - } - - standbys := manager.getSyncStandbys(ctx) - assert.True(t, standbys.HasStar) - assert.Equal(t, 3, standbys.Amount) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestCheckStandbySynchronizedToLeader(t *testing.T) { - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - DBState: &dcs.DBState{ - Extra: map[string]string{}, - }, - }, - } - - t.Run("synchronized to leader", func(t *testing.T) { - manager, _, _ := MockDatabase(t) - manager.CurrentMemberName = "a" - cluster.Leader.DBState.Extra[postgres.SyncStandBys] = "a,b,c" - - ok := manager.checkStandbySynchronizedToLeader(true, cluster) - assert.True(t, ok) - }) - - t.Run("is leader", func(t *testing.T) { - manager, _, _ := MockDatabase(t) - manager.CurrentMemberName = "a" - cluster.Leader.Name = "a" - cluster.Leader.DBState.Extra[postgres.SyncStandBys] = "b,c" - - ok := manager.checkStandbySynchronizedToLeader(true, cluster) - assert.True(t, ok) - }) - - t.Run("not synchronized to leader", func(t *testing.T) { - manager, _, _ := MockDatabase(t) - manager.CurrentMemberName = "d" - cluster.Leader.DBState.Extra[postgres.SyncStandBys] = "a,b,c" - - ok := manager.checkStandbySynchronizedToLeader(true, cluster) - assert.False(t, ok) - }) -} - -func TestGetReceivedTimeLine(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("get received timeline success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"}).AddRow(1)) - - timeLine := manager.getReceivedTimeLine(ctx, "") - assert.Equal(t, int64(1), timeLine) - }) - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - timeLine := manager.getReceivedTimeLine(ctx, "") - assert.Equal(t, int64(0), timeLine) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"})) - - timeLine := manager.getReceivedTimeLine(ctx, "") - assert.Equal(t, int64(0), timeLine) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestReadRecoveryParams(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("host match", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=maple72-postgresql-0.maple72-postgresql-headless port=5432 application_name=my-application", "signup")) - - leaderName := "maple72-postgresql-0" - recoveryParams, err := manager.readRecoveryParams(ctx) - assert.Nil(t, err) - assert.True(t, strings.HasPrefix(recoveryParams[postgres.PrimaryConnInfo]["host"], leaderName)) - }) - - t.Run("host not match", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=test port=5432 user=postgres application_name=my-application", "signup")) - - leaderName := "a" - recoveryParams, err := manager.readRecoveryParams(ctx) - assert.Nil(t, err) - assert.False(t, strings.HasPrefix(recoveryParams[postgres.PrimaryConnInfo]["host"], leaderName)) - }) - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnError(fmt.Errorf("some error")) - - recoveryParams, err := manager.readRecoveryParams(ctx) - assert.NotNil(t, err) - assert.Equal(t, "", recoveryParams[postgres.PrimaryConnInfo]["host"]) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"})) - - recoveryParams, err := manager.readRecoveryParams(ctx) - assert.NotNil(t, err) - assert.Equal(t, "", recoveryParams[postgres.PrimaryConnInfo]["host"]) - }) - - t.Run("primary info has been set", func(t *testing.T) { - manager.recoveryParams = map[string]map[string]string{ - postgres.PrimaryConnInfo: { - "host": "test", - }, - } - - recoveryParams, err := manager.readRecoveryParams(ctx) - assert.Nil(t, err) - assert.Equal(t, "test", recoveryParams[postgres.PrimaryConnInfo]["host"]) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestCheckRecoveryConf(t *testing.T) { - fs = afero.NewMemMapFs() - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("standby.signal not exist", func(t *testing.T) { - needChange, needRestart := manager.checkRecoveryConf(ctx, manager.CurrentMemberName) - assert.True(t, needChange) - assert.True(t, needRestart) - }) - - _, err := fs.Create(manager.DataDir + "/standby.signal") - assert.Nil(t, err) - - t.Run("query primaryInfo failed", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnError(fmt.Errorf("some error")) - - needChange, needRestart := manager.checkRecoveryConf(ctx, manager.CurrentMemberName) - assert.True(t, needChange) - assert.True(t, needRestart) - }) - - t.Run("host not match and restart", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=maple72-postgresql-0.maple72-postgresql-headless port=5432 application_name=my-application", "postmaster")) - - needChange, needRestart := manager.checkRecoveryConf(ctx, manager.CurrentMemberName) - assert.True(t, needChange) - assert.True(t, needRestart) - }) - - t.Run("host not match and reload", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=maple72-postgresql-0.maple72-postgresql-headless port=5432 application_name=my-application", "signup")) - - needChange, needRestart := manager.checkRecoveryConf(ctx, manager.CurrentMemberName) - assert.True(t, needChange) - assert.False(t, needRestart) - }) - - t.Run("host match", func(t *testing.T) { - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=test-pod-0.maple72-postgresql-headless port=5432 application_name=my-application", "signup")) - - needChange, needRestart := manager.checkRecoveryConf(ctx, manager.CurrentMemberName) - assert.False(t, needChange) - assert.False(t, needRestart) - }) - - if err = mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsMemberLagging(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - HaConfig: &dcs.HaConfig{}, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - currentMember := cluster.GetMemberWithName(manager.CurrentMemberName) - - t.Run("db state is nil", func(t *testing.T) { - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.False(t, isLagging) - assert.Equal(t, int64(0), lag) - }) - - cluster.Leader = &dcs.Leader{ - DBState: &dcs.DBState{ - OpTimestamp: 100, - Extra: map[string]string{ - postgres.TimeLine: "1", - }, - }, - } - - t.Run("get replication mode failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("not sync to leader", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("on")) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("get timeline failed", func(t *testing.T) { - manager.SetIsLeader(true) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("SELECT timeline_id"). - WillReturnError(fmt.Errorf("some error")) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("timeline not match", func(t *testing.T) { - manager.SetIsLeader(true) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("SELECT timeline_id"). - WillReturnRows(pgxmock.NewRows([]string{"timeline_id"}).AddRow(2)) - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("get wal position failed", func(t *testing.T) { - manager.SetIsLeader(true) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("SELECT timeline_id"). - WillReturnRows(pgxmock.NewRows([]string{"timeline_id"}).AddRow(1)) - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnError(fmt.Errorf("some error")) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.True(t, isLagging) - assert.Equal(t, int64(1), lag) - }) - - t.Run("current member is not lagging", func(t *testing.T) { - manager.SetIsLeader(true) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("SELECT timeline_id"). - WillReturnRows(pgxmock.NewRows([]string{"timeline_id"}).AddRow(1)) - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(100)) - - isLagging, lag := manager.IsMemberLagging(ctx, cluster, currentMember) - assert.False(t, isLagging) - assert.Equal(t, int64(0), lag) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetCurrentTimeLine(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("SELECT timeline_id"). - WillReturnError(fmt.Errorf("some error")) - - timeline := manager.getCurrentTimeLine(ctx, "") - assert.Equal(t, int64(0), timeline) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("SELECT timeline_id"). - WillReturnRows(pgxmock.NewRows([]string{"timeline_id"})) - - timeline := manager.getCurrentTimeLine(ctx, "") - assert.Equal(t, int64(0), timeline) - }) - - t.Run("get current timeline success", func(t *testing.T) { - mock.ExpectQuery("SELECT timeline_id"). - WillReturnRows(pgxmock.NewRows([]string{"timeline_id"}).AddRow(1)) - - timeline := manager.getCurrentTimeLine(ctx, "") - assert.Equal(t, int64(1), timeline) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetTimeLineWithHost(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("check is leader failed", func(t *testing.T) { - timeLine := manager.getTimeLineWithHost(ctx, "test") - assert.Zero(t, timeLine) - }) - - t.Run("timeLine has been set", func(t *testing.T) { - manager.DBState = &dcs.DBState{ - Extra: map[string]string{ - postgres.TimeLine: "1", - }, - } - - timeLine := manager.getTimeLineWithHost(ctx, "") - assert.Equal(t, int64(1), timeLine) - }) -} - -func TestGetLocalTimeLineAndLsn(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("db is not running", func(t *testing.T) { - isRecovery, localTimeLine, localLsn := manager.getLocalTimeLineAndLsn(ctx) - assert.False(t, isRecovery) - assert.Equal(t, int64(0), localTimeLine) - assert.Equal(t, int64(0), localLsn) - }) - - manager.Proc = &process.Process{ - // Process 1 is always in a running state. - Pid: 1, - } - - t.Run("get local timeline and lsn success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"}).AddRow(1)) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - - isRecovery, localTimeLine, localLsn := manager.getLocalTimeLineAndLsn(ctx) - assert.True(t, isRecovery) - assert.Equal(t, int64(1), localTimeLine) - assert.Equal(t, int64(23454272), localLsn) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestCleanDBState(t *testing.T) { - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("clean db state", func(t *testing.T) { - manager.cleanDBState() - isSet, isLeader := manager.GetIsLeader() - assert.False(t, isSet) - assert.False(t, isLeader) - assert.Nil(t, manager.recoveryParams) - assert.Nil(t, manager.syncStandbys) - assert.Equal(t, &dcs.DBState{ - Extra: map[string]string{}, - }, manager.DBState) - }) -} - -func TestGetDBState(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - defer func() { - postgres.LocalCommander = postgres.NewExecCommander - }() - cluster := &dcs.Cluster{} - - t.Run("check is leader failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get replication mode failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(false)) - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("synchronous mode but get wal position failed", func(t *testing.T) { - cluster.Leader = &dcs.Leader{ - Name: manager.CurrentMemberName, - } - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(false)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("on")) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow(`ANY 4("a",*,b)`)) - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get timeline failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(false)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("pg_catalog.pg_current_wal_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - mock.ExpectQuery("SELECT timeline_id"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("read recovery params failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(true)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - mock.ExpectQuery("pg_catalog.pg_last_wal_receive_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454273)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"}).AddRow(1)) - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnError(fmt.Errorf("some error")) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get pg control data failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(true)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - mock.ExpectQuery("pg_catalog.pg_last_wal_receive_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454273)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"}).AddRow(1)) - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=maple72-postgresql-0.maple72-postgresql-headless port=5432 application_name=my-application", "postmaster")) - postgres.LocalCommander = postgres.NewFakeCommander(func() error { - return fmt.Errorf("some error") - }, nil, nil) - - dbState := manager.GetDBState(ctx, cluster) - assert.Nil(t, dbState) - }) - - t.Run("get db state success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"pg_is_in_recovery"}).AddRow(true)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("off")) - mock.ExpectQuery("pg_last_wal_replay_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454272)) - mock.ExpectQuery("pg_catalog.pg_last_wal_receive_lsn()"). - WillReturnRows(pgxmock.NewRows([]string{"pg_wal_lsn_diff"}).AddRow(23454273)) - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"received_tli"}).AddRow(1)) - mock.ExpectQuery("pg_catalog.pg_settings"). - WillReturnRows(pgxmock.NewRows([]string{"name", "setting", "context"}). - AddRow("primary_conninfo", "host=maple72-postgresql-0.maple72-postgresql-headless port=5432 application_name=my-application", "postmaster")) - fakeControlData := "WAL block size: 8192\n" + - "Database cluster state: shut down" - - var stdout = bytes.NewBuffer([]byte(fakeControlData)) - postgres.LocalCommander = postgres.NewFakeCommander(func() error { - return nil - }, stdout, nil) - - dbState := manager.GetDBState(ctx, cluster) - isSet, isLeader := manager.GetIsLeader() - assert.NotNil(t, dbState) - assert.True(t, isSet) - assert.False(t, isLeader) - assert.Equal(t, postgres.Asynchronous, dbState.Extra[postgres.ReplicationMode]) - assert.Equal(t, int64(23454273), dbState.OpTimestamp) - assert.Equal(t, "1", dbState.Extra[postgres.TimeLine]) - assert.Equal(t, "maple72-postgresql-0.maple72-postgresql-headless", manager.recoveryParams[postgres.PrimaryConnInfo]["host"]) - assert.Equal(t, "postmaster", manager.recoveryParams[postgres.PrimaryConnInfo]["context"]) - assert.Equal(t, "shut down", manager.pgControlData["Database cluster state"]) - assert.Equal(t, "8192", manager.pgControlData["WAL block size"]) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestFollow(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - Name: manager.CurrentMemberName, - }, - } - fs = afero.NewMemMapFs() - - t.Run("cluster has no leader now", func(t *testing.T) { - err := manager.follow(ctx, false, cluster) - assert.Nil(t, err) - }) - - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("current member is leader", func(t *testing.T) { - err := manager.follow(ctx, false, cluster) - assert.Nil(t, err) - }) - - manager.CurrentMemberName = "test" - - t.Run("open postgresql conf failed", func(t *testing.T) { - err := manager.follow(ctx, true, cluster) - assert.NotNil(t, err) - }) - - t.Run("open postgresql conf failed", func(t *testing.T) { - err := manager.follow(ctx, true, cluster) - assert.NotNil(t, err) - }) - - t.Run("follow without restart", func(t *testing.T) { - _, _ = fs.Create("/kubeblocks/conf/postgresql.conf") - mock.ExpectExec("select pg_reload_conf()"). - WillReturnResult(pgxmock.NewResult("select", 1)) - - err := manager.follow(ctx, false, cluster) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestHasOtherHealthyMembers(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - cluster := &dcs.Cluster{} - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - t.Run("", func(t *testing.T) { - members := manager.HasOtherHealthyMembers(ctx, cluster, manager.CurrentMemberName) - assert.Equal(t, 0, len(members)) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetPgControlData(t *testing.T) { - manager, mock, _ := MockDatabase(t) - defer mock.Close() - defer func() { - postgres.LocalCommander = postgres.NewExecCommander - }() - - t.Run("get pg control data failed", func(t *testing.T) { - postgres.LocalCommander = postgres.NewFakeCommander(func() error { - return fmt.Errorf("some error") - }, nil, nil) - - data := manager.getPgControlData() - assert.Nil(t, data) - }) - - t.Run("get pg control data success", func(t *testing.T) { - fakeControlData := "pg_control version number: 1002\n" + - "Data page checksum version: 0" - - var stdout = bytes.NewBuffer([]byte(fakeControlData)) - postgres.LocalCommander = postgres.NewFakeCommander(func() error { - return nil - }, stdout, nil) - - data := manager.getPgControlData() - assert.NotNil(t, data) - assert.Equal(t, "1002", data["pg_control version number"]) - assert.Equal(t, "0", data["Data page checksum version"]) - }) - - t.Run("pg control data has been set", func(t *testing.T) { - manager.pgControlData = map[string]string{ - "Data page checksum version": "1", - } - - data := manager.getPgControlData() - assert.NotNil(t, data) - assert.Equal(t, "1", data["Data page checksum version"]) - }) -} diff --git a/pkg/lorry/engines/postgres/query.go b/pkg/lorry/engines/postgres/query.go deleted file mode 100644 index 0fba9fdf234..00000000000 --- a/pkg/lorry/engines/postgres/query.go +++ /dev/null @@ -1,216 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/pkg/errors" - "github.com/spf13/cast" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -// Query is equivalent to QueryWithHost(ctx, sql, ""), query itself. -func (mgr *Manager) Query(ctx context.Context, sql string) (result []byte, err error) { - return mgr.QueryWithHost(ctx, sql, "") -} - -func (mgr *Manager) QueryWithHost(ctx context.Context, sql string, host string) (result []byte, err error) { - var rows pgx.Rows - // when host is empty, use manager's connection pool - if host == "" { - rows, err = mgr.Pool.Query(ctx, sql) - } else { - rows, err = mgr.QueryOthers(ctx, sql, host) - } - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("query sql:%s failed", sql)) - return nil, err - } - defer func() { - rows.Close() - _ = rows.Err() - }() - - result, err = parseRows(rows) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("parse query:%s failed", sql)) - return nil, err - } - - return result, nil -} - -func (mgr *Manager) QueryOthers(ctx context.Context, sql string, host string) (rows pgx.Rows, err error) { - conn, err := pgx.Connect(ctx, config.GetConnectURLWithHost(host)) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("get host:%s connection failed", host)) - return nil, err - } - defer func() { - _ = conn.Close(ctx) - }() - - return conn.Query(ctx, sql) -} - -// GetLeaderAddr query leader addr from db kernel -func (mgr *Manager) GetLeaderAddr(ctx context.Context) (string, error) { - queryLeaderAddrSQL := `select ip_port from consensus_cluster_status where server_id = (select current_leader from consensus_member_status);` - resp, err := mgr.Query(ctx, queryLeaderAddrSQL) - if err != nil { - return "", err - } - - resMap, err := ParseQuery(string(resp)) - if err != nil { - return "", err - } - - return strings.Split(cast.ToString(resMap[0]["ip_port"]), ":")[0], nil -} - -func (mgr *Manager) QueryLeader(ctx context.Context, sql string, cluster *dcs.Cluster) (result []byte, err error) { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - leaderAddr, err := mgr.GetLeaderAddr(ctx) - if err != nil { - return nil, ClusterHasNoLeader - } - - return mgr.QueryWithHost(ctx, sql, leaderAddr) - } - - var host string - if leaderMember.Name != mgr.CurrentMemberName { - host = cluster.GetMemberAddr(*leaderMember) - } - return mgr.QueryWithHost(ctx, sql, host) -} - -// Exec is equivalent to ExecWithHost(ctx, sql, ""), exec itself. -func (mgr *Manager) Exec(ctx context.Context, sql string) (result int64, err error) { - return mgr.ExecWithHost(ctx, sql, "") -} - -func (mgr *Manager) ExecWithHost(ctx context.Context, sql string, host string) (result int64, err error) { - var res pgconn.CommandTag - - // when host is empty, use manager's connection pool - if host == "" { - res, err = mgr.Pool.Exec(ctx, sql) - } else { - res, err = mgr.ExecOthers(ctx, sql, host) - } - if err != nil { - return 0, errors.Wrapf(err, "sql:%s", sql) - } - - result = res.RowsAffected() - return result, nil -} - -func (mgr *Manager) ExecOthers(ctx context.Context, sql string, host string) (resp pgconn.CommandTag, err error) { - conn, err := pgx.Connect(ctx, config.GetConnectURLWithHost(host)) - if err != nil { - return resp, err - } - defer func() { - _ = conn.Close(ctx) - }() - - return conn.Exec(ctx, sql) -} - -func (mgr *Manager) ExecLeader(ctx context.Context, sql string, cluster *dcs.Cluster) (int64, error) { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - leaderAddr, err := mgr.GetLeaderAddr(ctx) - if err != nil { - return 0, ClusterHasNoLeader - } - - return mgr.ExecWithHost(ctx, sql, leaderAddr) - } - - var host string - if leaderMember.Name != mgr.CurrentMemberName { - host = cluster.GetMemberAddr(*leaderMember) - } - return mgr.ExecWithHost(ctx, sql, host) -} - -func (mgr *Manager) GetPgCurrentSetting(ctx context.Context, setting string) (string, error) { - sql := fmt.Sprintf(`select pg_catalog.current_setting('%s');`, setting) - - resp, err := mgr.Query(ctx, sql) - if err != nil { - return "", err - } - - resMap, err := ParseQuery(string(resp)) - if err != nil { - return "", err - } - - return cast.ToString(resMap[0]["current_setting"]), nil -} - -func parseRows(rows pgx.Rows) (result []byte, err error) { - rs := make([]interface{}, 0) - columnTypes := rows.FieldDescriptions() - for rows.Next() { - values := make([]interface{}, len(columnTypes)) - for i := range values { - values[i] = new(interface{}) - } - - if err = rows.Scan(values...); err != nil { - return nil, errors.Errorf("scanning row failed, err:%v", err) - } - - r := map[string]interface{}{} - for i, ct := range columnTypes { - r[ct.Name] = values[i] - } - rs = append(rs, r) - } - - if result, err = json.Marshal(rs); err != nil { - err = errors.Errorf("json marshal failed, err: %v", err) - } - return result, err -} - -func ParseQuery(str string) (result []map[string]interface{}, err error) { - // Notice: in golang, json unmarshal will map all numeric types to float64. - err = json.Unmarshal([]byte(str), &result) - if err != nil || len(result) == 0 { - return nil, errors.Errorf("json unmarshal failed, err:%v", err) - } - - return result, nil -} diff --git a/pkg/lorry/engines/postgres/query_test.go b/pkg/lorry/engines/postgres/query_test.go deleted file mode 100644 index 0e4e43937a6..00000000000 --- a/pkg/lorry/engines/postgres/query_test.go +++ /dev/null @@ -1,239 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "context" - "fmt" - "testing" - - "github.com/pashagolub/pgxmock/v2" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -const ( - execTest = "create database test" - queryTest = "select 1" -) - -func TestQuery(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("query success", func(t *testing.T) { - sql := queryTest - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"1"})) - - _, err := manager.Query(ctx, sql) - assert.Nil(t, err) - }) - - t.Run("query failed", func(t *testing.T) { - sql := queryTest - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - _, err := manager.Query(ctx, sql) - assert.NotNil(t, err) - }) - - t.Run("parse rows failed", func(t *testing.T) { - sql := queryTest - var val chan string - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"1"}).AddRow(val)) - _, err := manager.Query(ctx, sql) - assert.NotNil(t, err) - }) - - t.Run("can't connect db", func(t *testing.T) { - sql := queryTest - resp, err := manager.QueryWithHost(ctx, sql, "localhost") - assert.NotNil(t, err) - assert.Nil(t, resp) - }) - - t.Run("query leader success", func(t *testing.T) { - sql := queryTest - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"1"}).AddRow("1")) - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - Name: manager.CurrentMemberName, - }, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - resp, err := manager.QueryLeader(ctx, sql, cluster) - if err != nil { - t.Errorf("expect query leader success but failed") - } - - assert.Equal(t, []byte(`[{"1":"1"}]`), resp) - }) - - t.Run("query leader failed, cluster has no leader", func(t *testing.T) { - sql := queryTest - cluster := &dcs.Cluster{} - - _, err := manager.QueryLeader(ctx, sql, cluster) - if err == nil { - t.Errorf("expect query leader success but failed") - } - - assert.ErrorIs(t, ClusterHasNoLeader, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestParseQuery(t *testing.T) { - t.Run("parse query success", func(t *testing.T) { - data := []byte(`[{"current_setting":"off"}]`) - resMap, err := ParseQuery(string(data)) - assert.NotNil(t, resMap) - assert.Nil(t, err) - assert.Equal(t, 1, len(resMap)) - assert.Equal(t, "off", resMap[0]["current_setting"].(string)) - }) - - t.Run("parse query failed", func(t *testing.T) { - data := []byte(`{"current_setting":"off"}`) - resMap, err := ParseQuery(string(data)) - assert.NotNil(t, err) - assert.Nil(t, resMap) - }) -} - -func TestExec(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("exec success", func(t *testing.T) { - sql := execTest - - mock.ExpectExec("create database"). - WillReturnResult(pgxmock.NewResult("CREATE DATABASE", 1)) - - _, err := manager.Exec(ctx, sql) - assert.Nil(t, err) - }) - - t.Run("exec failed", func(t *testing.T) { - sql := execTest - - mock.ExpectExec("create database"). - WillReturnError(fmt.Errorf("some error")) - - _, err := manager.Exec(ctx, sql) - assert.NotNil(t, err) - }) - - t.Run("can't connect db", func(t *testing.T) { - sql := execTest - resp, err := manager.ExecWithHost(ctx, sql, "test") - if err == nil { - t.Errorf("expect query failed, but success") - } - assert.Equal(t, int64(0), resp) - }) - - t.Run("exec leader success", func(t *testing.T) { - sql := execTest - mock.ExpectExec("create"). - WillReturnResult(pgxmock.NewResult("CREATE", 1)) - cluster := &dcs.Cluster{ - Leader: &dcs.Leader{ - Name: manager.CurrentMemberName, - }, - } - cluster.Members = append(cluster.Members, dcs.Member{ - Name: manager.CurrentMemberName, - }) - - resp, err := manager.ExecLeader(ctx, sql, cluster) - if err != nil { - t.Errorf("expect exec leader success but failed") - } - assert.Equal(t, int64(1), resp) - }) - - t.Run("exec leader failed, cluster has no leader", func(t *testing.T) { - sql := execTest - cluster := &dcs.Cluster{} - - _, err := manager.ExecLeader(ctx, sql, cluster) - if err == nil { - t.Errorf("expect exec leader success but failed") - } - - assert.ErrorIs(t, ClusterHasNoLeader, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetPgCurrentSetting(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := MockDatabase(t) - defer mock.Close() - - t.Run("query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnError(fmt.Errorf("some error")) - - res, err := manager.GetPgCurrentSetting(ctx, "test") - assert.NotNil(t, err) - assert.Equal(t, "", res) - }) - - t.Run("parse query failed", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"})) - - res, err := manager.GetPgCurrentSetting(ctx, "test") - assert.NotNil(t, err) - assert.Equal(t, "", res) - }) - - t.Run("query success", func(t *testing.T) { - mock.ExpectQuery("select"). - WillReturnRows(pgxmock.NewRows([]string{"current_setting"}).AddRow("test")) - - res, err := manager.GetPgCurrentSetting(ctx, "test") - assert.Nil(t, err) - assert.Equal(t, "test", res) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/postgres/types.go b/pkg/lorry/engines/postgres/types.go deleted file mode 100644 index 7c634e0c09e..00000000000 --- a/pkg/lorry/engines/postgres/types.go +++ /dev/null @@ -1,364 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "bufio" - "bytes" - "context" - "fmt" - "strconv" - "strings" - - mapset "github.com/deckarep/golang-set/v2" - "github.com/dlclark/regexp2" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/cast" - "golang.org/x/exp/slices" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var ( - ClusterHasNoLeader = errors.New("cluster has no leader now") -) - -var fs = afero.NewOsFs() - -const ( - PGDATA = "PGDATA" - PGMAJOR = "PG_MAJOR" -) - -const ( - ReplicationMode = "replication_mode" - SyncStandBys = "sync_standbys" - PrimaryConnInfo = "primary_conninfo" - TimeLine = "timeline" -) - -const ( - Asynchronous = "asynchronous" - Synchronous = "synchronous" -) - -const ( - first = "first" - star = "star" - ident = "ident" - doubleQuote = "double_quote" - space = "space" - anyA = "any" - num = "num" - comma = "comma" - parenthesisStart = "parenthesis_start" - parenthesisEnd = "parenthesis_end" - quorum = "quorum" - priority = "priority" - off = "off" -) - -type PgBaseIFace interface { - GetMemberRoleWithHost(ctx context.Context, host string) (string, error) - IsMemberHealthy(ctx context.Context, cluster *dcs.Cluster, member *dcs.Member) bool - Query(ctx context.Context, sql string) (result []byte, err error) - Exec(ctx context.Context, sql string) (result int64, err error) -} - -type PgIFace interface { - engines.DBManager - PgBaseIFace -} - -type PgxIFace interface { - Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) - Query(ctx context.Context, query string, args ...interface{}) (pgx.Rows, error) - Ping(ctx context.Context) error -} - -// PgxPoolIFace is interface representing pgx pool -type PgxPoolIFace interface { - PgxIFace - Acquire(ctx context.Context) (*pgxpool.Conn, error) - Close() -} - -type LocalCommand interface { - Run() error - GetStdout() *bytes.Buffer - GetStderr() *bytes.Buffer -} - -type ConsensusMemberHealthStatus struct { - Connected bool - LogDelayNum int64 -} - -type PidFile struct { - pid int32 - dataDir string - startTS int64 - port int -} - -func readPidFile(dataDir string) (*PidFile, error) { - file := &PidFile{} - f, err := fs.Open(dataDir + "/postmaster.pid") - if err != nil { - return nil, err - } - defer func() { - _ = f.Close() - }() - - scanner := bufio.NewScanner(f) - var text []string - for scanner.Scan() { - text = append(text, scanner.Text()) - } - - pid, err := strconv.ParseInt(text[0], 10, 32) - if err != nil { - return nil, err - } - file.pid = int32(pid) - file.dataDir = text[1] - startTS, _ := strconv.ParseInt(text[2], 10, 64) - file.startTS = startTS - port, err := strconv.ParseInt(text[3], 10, 32) - if err != nil { - return nil, err - } - file.port = int(port) - - return file, nil -} - -type PGStandby struct { - Types string - Amount int - Members mapset.Set[string] - HasStar bool -} - -func ParsePGSyncStandby(standbyRow string) (*PGStandby, error) { - pattern := `(?P [fF][iI][rR][sS][tT] ) - |(?P [aA][nN][yY] ) - |(?P \s+ ) - |(?P [A-Za-z_][A-Za-z_0-9\$]* ) - |(?P " (?: [^"]+ | "" )* " ) - |(?P [*] ) - |(?P \d+ ) - |(?P , ) - |(?P \( ) - |(?P \) ) - |(?P . ) ` - patterns := []string{ - `(?P [fF][iI][rR][sS][tT]) `, - `(?P [aA][nN][yY]) `, - `(?P \s+ )`, - `(?P [A-Za-z_][A-Za-z_0-9\$]* )`, - `(?P "(?:[^"]+|"")*") `, - `(?P [*] )`, - `(?P \d+ )`, - `(?P , )`, - `(?P \( )`, - `(?P \) )`, - `(?P .) `, - } - result := &PGStandby{ - Types: off, - Members: mapset.NewSet[string](), - } - - rs := make([]*regexp2.Regexp, len(patterns)) - var patternPrefix string - for i, p := range patterns { - if i != 0 { - patternPrefix += `|` - } - patternPrefix += p - rs[i] = regexp2.MustCompile(patternPrefix, regexp2.IgnorePatternWhitespace+regexp2.RE2) - } - - r := regexp2.MustCompile(pattern, regexp2.RE2+regexp2.IgnorePatternWhitespace) - groupNames := r.GetGroupNames() - - match, err := r.FindStringMatch(standbyRow) - if err != nil { - return nil, err - } - - var matches [][]string - start := 0 - for match != nil { - nums := getMatchLastGroupNumber(rs, standbyRow, match.String(), start) - if groupNames[nums+2] != space { - matches = append(matches, []string{groupNames[nums+2], match.String(), strconv.FormatInt(int64(start), 10)}) - } - start = match.Index + match.Length - - match, err = r.FindNextMatch(match) - if err != nil { - return nil, err - } - } - - length := len(matches) - if length == 0 { - return result, nil - } - var syncList [][]string - switch { - case length >= 3 && matches[0][0] == anyA && matches[1][0] == num && matches[2][0] == parenthesisStart && matches[length-1][0] == parenthesisEnd: - result.Types = quorum - result.Amount = cast.ToInt(matches[1][1]) - syncList = matches[3 : length-1] - case length >= 3 && matches[0][0] == first && matches[1][0] == num && matches[2][0] == parenthesisStart && matches[length-1][0] == parenthesisEnd: - result.Types = priority - result.Amount = cast.ToInt(matches[1][1]) - syncList = matches[3 : length-1] - case length >= 2 && matches[0][0] == num && matches[1][0] == parenthesisStart && matches[length-1][0] == parenthesisEnd: - result.Types = priority - result.Amount = cast.ToInt(matches[0][1]) - syncList = matches[2 : length-1] - default: - result.Types = priority - result.Amount = 1 - syncList = matches - } - - for i, sync := range syncList { - switch { - case i%2 == 1: // odd elements are supposed to be commas - if len(syncList) == i+1 { - return nil, errors.Errorf("Unparseable synchronous_standby_names value: Unexpected token %s %s at %s", sync[0], sync[1], sync[2]) - } else if sync[0] != comma { - return nil, errors.Errorf("Unparseable synchronous_standby_names value: Got token %s %s while expecting comma at %s", sync[0], sync[1], sync[2]) - } - case slices.Contains([]string{ident, first, anyA}, sync[0]): - result.Members.Add(sync[1]) - case sync[0] == star: - result.Members.Add(sync[1]) - result.HasStar = true - case sync[0] == doubleQuote: - result.Members.Add(strings.ReplaceAll(sync[1][1:len(sync)-1], `""`, `"`)) - default: - return nil, errors.Errorf("Unparseable synchronous_standby_names value: Unexpected token %s %s at %s", sync[0], sync[1], sync[2]) - } - } - - return result, nil -} - -func getMatchLastGroupNumber(rs []*regexp2.Regexp, str string, substr string, start int) int { - for i := len(rs) - 1; i >= 0; i-- { - match, err := rs[i].FindStringMatchStartingAt(str, start) - if match == nil || err != nil { - return i - } - if match.String() != substr { - return i - } - } - - return -1 -} - -type HistoryFile struct { - History []History -} - -type History struct { - ParentTimeline int64 - SwitchPoint int64 -} - -func ParseHistory(str string) *HistoryFile { - result := &HistoryFile{ - History: []History{}, - } - - lines := strings.Split(str, "\n") - for _, line := range lines { - values := strings.Split(line, "|") - if len(values) <= 1 { - continue - } - content := strings.TrimSpace(values[1]) - history := strings.Split(content, " ") - if len(history) > 2 { - result.History = append(result.History, History{ - ParentTimeline: cast.ToInt64(history[0]), - SwitchPoint: ParsePgLsn(history[1]), - }) - } - } - - return result -} - -func ParsePgLsn(str string) int64 { - list := strings.Split(str, "/") - if len(list) < 2 { - return 0 - } - - prefix, _ := strconv.ParseInt(list[0], 16, 64) - suffix, _ := strconv.ParseInt(list[1], 16, 64) - return prefix*0x100000000 + suffix -} - -func FormatPgLsn(lsn int64) string { - return fmt.Sprintf("%X/%08X", lsn>>32, lsn&0xFFFFFFFF) -} - -func ParsePrimaryConnInfo(str string) map[string]string { - infos := strings.Split(str, " ") - result := make(map[string]string) - - for _, info := range infos { - v := strings.Split(info, "=") - if len(v) >= 2 { - result[v[0]] = v[1] - } - } - - return result -} - -func ParsePgWalDumpError(errorInfo string, lsnStr string) string { - prefixPattern := fmt.Sprintf("error in WAL record at %s: invalid record length at ", lsnStr) - suffixPattern := ": wanted " - - startIndex := strings.Index(errorInfo, prefixPattern) + len(prefixPattern) - endIndex := strings.Index(errorInfo, suffixPattern) - - if startIndex == -1 || endIndex == -1 { - return "" - } - - return errorInfo[startIndex:endIndex] -} diff --git a/pkg/lorry/engines/postgres/types_test.go b/pkg/lorry/engines/postgres/types_test.go deleted file mode 100644 index 0f85a30b35e..00000000000 --- a/pkg/lorry/engines/postgres/types_test.go +++ /dev/null @@ -1,239 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" -) - -func TestReadPidFile(t *testing.T) { - fs = afero.NewMemMapFs() - - t.Run("can't open file", func(t *testing.T) { - pidFile, err := readPidFile("") - assert.Nil(t, pidFile) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "file does not exist") - }) - - t.Run("read pid file success", func(t *testing.T) { - data := "97\n/postgresql/data\n1692770488\n5432\n/var/run/postgresql\n*\n 2388960 4\nready" - err := afero.WriteFile(fs, "/postmaster.pid", []byte(data), 0644) - if err != nil { - t.Fatal(err) - } - - pidFile, err := readPidFile("") - assert.Nil(t, err) - assert.Equal(t, pidFile.pid, int32(97)) - assert.Equal(t, pidFile.port, 5432) - assert.Equal(t, pidFile.dataDir, "/postgresql/data") - assert.Equal(t, pidFile.startTS, int64(1692770488)) - }) - - t.Run("pid invalid", func(t *testing.T) { - data := "test\n/postgresql/data\n1692770488\n5432\n/var/run/postgresql\n*\n 2388960 4\nready" - err := afero.WriteFile(fs, "/postmaster.pid", []byte(data), 0644) - if err != nil { - t.Fatal(err) - } - - pidFile, err := readPidFile("") - assert.Nil(t, pidFile) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "invalid syntax") - }) - - t.Run("pid invalid", func(t *testing.T) { - data := "97\n/postgresql/data\n1692770488\ntest\n/var/run/postgresql\n*\n 2388960 4\nready" - err := afero.WriteFile(fs, "/postmaster.pid", []byte(data), 0644) - if err != nil { - t.Fatal(err) - } - - pidFile, err := readPidFile("") - assert.Nil(t, pidFile) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "invalid syntax") - }) -} - -func TestParsePGSyncStandby(t *testing.T) { - t.Run("empty sync string", func(t *testing.T) { - syncStandBys := "" - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.Equal(t, off, resp.Types) - }) - - t.Run("only first", func(t *testing.T) { - syncStandBys := "FiRsT" - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.True(t, resp.Members.Contains("FiRsT")) - assert.Equal(t, priority, resp.Types) - assert.Equal(t, 1, resp.Amount) - }) - - t.Run("custom values", func(t *testing.T) { - syncStandBys := `ANY 4("a",*,b)` - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.Equal(t, quorum, resp.Types) - assert.True(t, resp.HasStar) - assert.True(t, resp.Members.Contains("a")) - assert.Equal(t, 4, resp.Amount) - }) - - t.Run("custom values", func(t *testing.T) { - syncStandBys := ` a , b ` - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.Equal(t, priority, resp.Types) - assert.False(t, resp.HasStar) - assert.True(t, resp.Members.Contains("a")) - assert.True(t, resp.Members.Contains("b")) - assert.Equal(t, 1, resp.Amount) - }) - - t.Run("custom values", func(t *testing.T) { - syncStandBys := `FIRST 2 (s1,s2,s3)` - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.Equal(t, priority, resp.Types) - assert.False(t, resp.HasStar) - assert.True(t, resp.Members.Contains("s1")) - assert.True(t, resp.Members.Contains("s2")) - assert.Equal(t, 2, resp.Amount) - }) - - t.Run("custom values", func(t *testing.T) { - syncStandBys := `2 (s1,s2,s3)` - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.Nil(t, err) - assert.Equal(t, priority, resp.Types) - assert.False(t, resp.HasStar) - assert.True(t, resp.Members.Contains("s1")) - assert.True(t, resp.Members.Contains("s2")) - assert.Equal(t, 2, resp.Amount) - }) - - t.Run("can't parse synchronous standby name", func(t *testing.T) { - syncStandBys := `ANY 4("a" b,"c c")` - resp, err := ParsePGSyncStandby(syncStandBys) - - assert.NotNil(t, err) - assert.Nil(t, resp) - assert.Contains(t, err.Error(), "Unparseable synchronous_standby_names value") - }) -} - -func TestParsePGLsn(t *testing.T) { - t.Run("legal lsn str", func(t *testing.T) { - lsnStr := "16/B374D848" - - lsn := ParsePgLsn(lsnStr) - assert.Equal(t, int64(97500059720), lsn) - }) - - t.Run("illegal lsn str", func(t *testing.T) { - lsnStr := "B374D848" - - lsn := ParsePgLsn(lsnStr) - assert.Equal(t, int64(0), lsn) - }) -} - -func TestFormatPgLsn(t *testing.T) { - t.Run("format lsn", func(t *testing.T) { - lsn := int64(16777376) - - lsnStr := FormatPgLsn(lsn) - assert.Equal(t, "0/010000A0", lsnStr) - }) -} - -func TestParsePrimaryConnInfo(t *testing.T) { - t.Run("legal primary conn info str", func(t *testing.T) { - primaryConnInfoStr := "host=pg-pg-replication-0.pg-pg-replication-headless port=5432 user=postgres application_name=my-application" - - result := ParsePrimaryConnInfo(primaryConnInfoStr) - assert.NotNil(t, result) - assert.Equal(t, "pg-pg-replication-0.pg-pg-replication-headless", result["host"]) - assert.Equal(t, "5432", result["port"]) - assert.Equal(t, "postgres", result["user"]) - assert.Equal(t, "my-application", result["application_name"]) - }) - - t.Run("illegal primary conn info str", func(t *testing.T) { - primaryConnInfoStr := "host pg-pg-replication-0.pg-pg-replication-headless port 5432 user postgres application_name my-application" - - result := ParsePrimaryConnInfo(primaryConnInfoStr) - assert.NotNil(t, result) - assert.Equal(t, map[string]string{}, result) - }) -} - -func TestParseHistory(t *testing.T) { - t.Run("parse history success", func(t *testing.T) { - historyStr := - ` filename | content -------------------+------------------------------------------------------ - 00000003.history | 1 0/50000A0 no recovery target specified+ - | + - | 2 0/60000A0 no recovery target specified+ - |` - - history := ParseHistory(historyStr) - assert.NotNil(t, history) - assert.Len(t, history.History, 2) - assert.Equal(t, int64(1), history.History[0].ParentTimeline) - assert.Equal(t, int64(2), history.History[1].ParentTimeline) - assert.Equal(t, ParsePgLsn("0/50000A0 "), history.History[0].SwitchPoint) - assert.Equal(t, ParsePgLsn("0/60000A0 "), history.History[1].SwitchPoint) - }) -} - -func TestParsePgWalDumpError(t *testing.T) { - t.Run("parse success", func(t *testing.T) { - errorInfo := "pg_waldump: fatal: error in WAL record at 0/182E220: invalid record length at 0/182E298: wanted 24, got 0" - - resp := ParsePgWalDumpError(errorInfo, "0/182E220") - - assert.Equal(t, "0/182E298", resp) - }) - - t.Run("parse failed", func(t *testing.T) { - errorInfo := "pg_waldump: fatal: error in WAL record at 0/182E220: invalid record length at 0/182E298" - - resp := ParsePgWalDumpError(errorInfo, "0/182E220") - - assert.Equal(t, "", resp) - }) -} diff --git a/pkg/lorry/engines/postgres/user.go b/pkg/lorry/engines/postgres/user.go deleted file mode 100644 index f7fe9fce56f..00000000000 --- a/pkg/lorry/engines/postgres/user.go +++ /dev/null @@ -1,238 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package postgres - -import ( - "context" - "encoding/json" - "fmt" - - "golang.org/x/exp/slices" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - listUserTpl = ` - SELECT usename AS userName, valuntil 'postgres' and usename not like 'kb%' - ORDER BY usename; - ` - descUserTpl = ` - SELECT usename AS userName, valuntil 0 { - return &users[0], nil - } - return nil, nil -} - -func (mgr *Manager) CreateUser(ctx context.Context, userName, password, _ string) error { - sql := fmt.Sprintf(createUserTpl, userName, password) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) DeleteUser(ctx context.Context, userName string) error { - sql := fmt.Sprintf(dropUserTpl, userName) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) GrantUserRole(ctx context.Context, userName, roleName string) error { - var sql string - if models.SuperUserRole.EqualTo(roleName) { - sql = "ALTER USER " + userName + " WITH SUPERUSER;" - } else { - roleDesc, _ := role2PGRole(roleName) - sql = fmt.Sprintf(grantTpl, roleDesc, userName) - } - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) RevokeUserRole(ctx context.Context, userName, roleName string) error { - var sql string - if models.SuperUserRole.EqualTo(roleName) { - sql = "ALTER USER " + userName + " WITH NOSUPERUSER;" - } else { - roleDesc, _ := role2PGRole(roleName) - sql = fmt.Sprintf(revokeTpl, roleDesc, userName) - } - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -// post-processing -func pgUserRolesProcessor(data interface{}) ([]models.UserInfo, error) { - type pgUserInfo struct { - UserName string `json:"username"` - Expired bool `json:"expired"` - Super bool `json:"usesuper"` - Roles []string `json:"roles"` - } - // parse data to struct - var pgUsers []pgUserInfo - err := json.Unmarshal(data.([]byte), &pgUsers) - if err != nil { - return nil, err - } - // parse roles - users := make([]models.UserInfo, len(pgUsers)) - for i := range pgUsers { - users[i] = models.UserInfo{ - UserName: pgUsers[i].UserName, - } - - if pgUsers[i].Expired { - users[i].Expired = "T" - } else { - users[i].Expired = "F" - } - - // parse Super attribute - if pgUsers[i].Super { - pgUsers[i].Roles = append(pgUsers[i].Roles, string(models.SuperUserRole)) - } - - // convert to RoleType and sort by weight - roleTypes := make([]models.RoleType, 0) - for _, role := range pgUsers[i].Roles { - roleTypes = append(roleTypes, models.String2RoleType(role)) - } - slices.SortFunc(roleTypes, models.SortRoleByWeight) - if len(roleTypes) > 0 { - users[i].RoleName = string(roleTypes[0]) - } - } - return users, nil -} - -func role2PGRole(roleName string) (string, error) { - roleType := models.String2RoleType(roleName) - switch roleType { - case models.ReadWriteRole: - return "pg_write_all_data", nil - case models.ReadOnlyRole: - return "pg_read_all_data", nil - } - return "", fmt.Errorf("role name: %s is not supported", roleName) -} diff --git a/pkg/lorry/engines/pulsar/commands.go b/pkg/lorry/engines/pulsar/commands.go deleted file mode 100644 index a5dc9a2c837..00000000000 --- a/pkg/lorry/engines/pulsar/commands.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package pulsar - -import ( - "fmt" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -var _ engines.ClusterCommands = &Commands{} - -func NewBrokerCommands() engines.ClusterCommands { - return NewCommands("broker") -} - -func NewProxyCommands() engines.ClusterCommands { - return NewCommands("proxy") -} - -func NewCommands(containName string) engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "pulsar-shell", - Container: containName, - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return "# pulsar client connection example\n bin/pulsar-shell" - }, - }, - } -} - -func (r *Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - return []string{"sh", "-c", "bin/pulsar-shell"} -} - -func (r *Commands) Container() string { - return r.info.Container -} - -func (r *Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, r.examples) -} - -func (r *Commands) ExecuteCommand([]string) ([]string, []corev1.EnvVar, error) { - return nil, nil, fmt.Errorf("%s not implemented", r.info.Client) -} diff --git a/pkg/lorry/engines/pulsar/commands_test.go b/pkg/lorry/engines/pulsar/commands_test.go deleted file mode 100644 index 705c4474354..00000000000 --- a/pkg/lorry/engines/pulsar/commands_test.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package pulsar - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Pulsar Engine", func() { - It("connection command", func() { - pulsar := NewProxyCommands() - - Expect(pulsar.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(pulsar.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("proxy connection example", func() { - pulsar := NewProxyCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range pulsar.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(pulsar.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(pulsar.ConnectExample(info, "")).ShouldNot(BeZero()) - }) - - It("broker connection example", func() { - pulsar := NewBrokerCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range pulsar.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(pulsar.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(pulsar.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/pulsar/suite_test.go b/pkg/lorry/engines/pulsar/suite_test.go deleted file mode 100644 index ae5fe97d087..00000000000 --- a/pkg/lorry/engines/pulsar/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package pulsar - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "pulsar Suite") -} diff --git a/pkg/lorry/engines/redis/commands.go b/pkg/lorry/engines/redis/commands.go deleted file mode 100644 index c4f7aa8b270..00000000000 --- a/pkg/lorry/engines/redis/commands.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "fmt" - "strings" - - corev1 "k8s.io/api/core/v1" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -var _ engines.ClusterCommands = &Commands{} - -type Commands struct { - info engines.EngineInfo - examples map[models.ClientType]engines.BuildConnectExample -} - -func NewCommands() engines.ClusterCommands { - return &Commands{ - info: engines.EngineInfo{ - Client: "redis-cli", - Container: "redis", - }, - examples: map[models.ClientType]engines.BuildConnectExample{ - models.CLI: func(info *engines.ConnectionInfo) string { - return fmt.Sprintf(`# redis client connection example -redis-cli -h %s -p %s --user %s --pass %s -`, info.Host, info.Port, info.User, info.Password) - }, - }, - } -} - -func (r Commands) ConnectCommand(connectInfo *engines.AuthInfo) []string { - redisCmd := []string{ - "redis-cli", - } - - if connectInfo != nil { - redisCmd = append(redisCmd, "--user", engines.AddSingleQuote(connectInfo.UserName)) - redisCmd = append(redisCmd, "--pass", engines.AddSingleQuote(connectInfo.UserPasswd)) - } - return []string{"sh", "-c", strings.Join(redisCmd, " ")} -} - -func (r Commands) Container() string { - return r.info.Container -} - -func (r Commands) ConnectExample(info *engines.ConnectionInfo, client string) string { - return engines.BuildExample(info, client, r.examples) -} - -func (r Commands) ExecuteCommand(scripts []string) ([]string, []corev1.EnvVar, error) { - cmd := []string{} - args := []string{} - cmd = append(cmd, "/bin/sh", "-c") - for _, script := range scripts { - args = append(args, fmt.Sprintf("%s -h %s -p 6379 --user %s --pass %s %s", r.info.Client, - fmt.Sprintf("$%s", engines.EnvVarMap[engines.HOST]), - fmt.Sprintf("$%s", engines.EnvVarMap[engines.USER]), - fmt.Sprintf("$%s", engines.EnvVarMap[engines.PASSWORD]), script)) - } - cmd = append(cmd, strings.Join(args, " && ")) - return cmd, nil, nil -} diff --git a/pkg/lorry/engines/redis/commands_test.go b/pkg/lorry/engines/redis/commands_test.go deleted file mode 100644 index f8538a3ad41..00000000000 --- a/pkg/lorry/engines/redis/commands_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var _ = Describe("Redis Engine", func() { - It("connection command", func() { - redis := NewCommands() - - Expect(redis.ConnectCommand(nil)).ShouldNot(BeNil()) - authInfo := &engines.AuthInfo{ - UserName: "user-test", - UserPasswd: "pwd-test", - } - Expect(redis.ConnectCommand(authInfo)).ShouldNot(BeNil()) - }) - - It("connection example", func() { - redis := NewCommands().(*Commands) - - info := &engines.ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Port: "1234", - } - for k := range redis.examples { - fmt.Printf("%s Connection Example\n", k.String()) - Expect(redis.ConnectExample(info, k.String())).ShouldNot(BeZero()) - } - - Expect(redis.ConnectExample(info, "")).ShouldNot(BeZero()) - }) -}) diff --git a/pkg/lorry/engines/redis/get_replica_role.go b/pkg/lorry/engines/redis/get_replica_role.go deleted file mode 100644 index 2f734a55ef9..00000000000 --- a/pkg/lorry/engines/redis/get_replica_role.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "context" - "strings" - "time" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, _ *dcs.Cluster) (string, error) { - // To ensure that the role information obtained through subscription is always delivered. - if mgr.role != "" && mgr.roleSubscribeUpdateTime+mgr.roleProbePeriod*2 < time.Now().Unix() { - return mgr.role, nil - } - - // We use the role obtained from Sentinel as the sole source of truth. - masterAddr, err := mgr.sentinelClient.GetMasterAddrByName(ctx, mgr.ClusterCompName).Result() - if err != nil { - // when we can't get role from sentinel, we query redis instead - var role string - result, err := mgr.client.Info(ctx, "Replication").Result() - if err != nil { - mgr.Logger.Info("Role query failed", "error", err.Error()) - return role, err - } else { - // split the result into lines - lines := strings.Split(result, "\r\n") - // find the line with role - for _, line := range lines { - if strings.HasPrefix(line, "role:") { - role = strings.Split(line, ":")[1] - break - } - } - } - if role == models.MASTER { - return models.PRIMARY, nil - } else { - return models.SECONDARY, nil - } - } - - masterName := strings.Split(masterAddr[0], ".")[0] - // if current member is not master from sentinel, just return secondary to avoid double master - if masterName != mgr.CurrentMemberName { - return models.SECONDARY, nil - } - return models.PRIMARY, nil -} - -func (mgr *Manager) SubscribeRoleChange(ctx context.Context) { - pubSub := mgr.sentinelClient.Subscribe(ctx, "+switch-master") - - // go-redis periodically sends ping messages to test connection health - // and re-subscribes if ping can not receive for 30 seconds. - // so we don't need to retry - ch := pubSub.Channel() - for msg := range ch { - // +switch-master - masterAddr := strings.Split(msg.Payload, " ") - masterName := strings.Split(masterAddr[3], ".")[0] - - if masterName == mgr.CurrentMemberName { - mgr.role = models.PRIMARY - } else { - mgr.role = models.SECONDARY - } - mgr.roleSubscribeUpdateTime = time.Now().Unix() - } -} diff --git a/pkg/lorry/engines/redis/manager.go b/pkg/lorry/engines/redis/manager.go deleted file mode 100644 index 70a3610245d..00000000000 --- a/pkg/lorry/engines/redis/manager.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "context" - "strings" - "time" - - "github.com/redis/go-redis/v9" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var ( - redisUser = "default" - redisPasswd = "" -) - -type Manager struct { - engines.DBManagerBase - client redis.UniversalClient - clientSettings *Settings - sentinelClient *redis.SentinelClient - - ctx context.Context - cancel context.CancelFunc - startAt time.Time - role string - roleSubscribeUpdateTime int64 - roleProbePeriod int64 -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("Redis") - - if viper.IsSet(constant.KBEnvServiceUser) { - redisUser = viper.GetString(constant.KBEnvServiceUser) - } - - if viper.IsSet(constant.KBEnvServicePassword) { - redisPasswd = viper.GetString(constant.KBEnvServicePassword) - } - - managerBase, err := engines.NewDBManagerBase(logger) - if err != nil { - return nil, err - } - mgr := &Manager{ - DBManagerBase: *managerBase, - roleProbePeriod: int64(viper.GetInt(constant.KBEnvRoleProbePeriod)), - } - - mgr.startAt = time.Now() - - defaultSettings := &Settings{ - Password: redisPasswd, - Username: redisUser, - } - mgr.client, mgr.clientSettings, err = ParseClientFromProperties(properties, defaultSettings) - if err != nil { - return nil, err - } - - mgr.sentinelClient = newSentinelClient(mgr.clientSettings, mgr.ClusterCompName) - - mgr.ctx, mgr.cancel = context.WithCancel(context.Background()) - - go mgr.SubscribeRoleChange(mgr.ctx) - return mgr, nil -} - -func (mgr *Manager) IsDBStartupReady() bool { - if mgr.DBStartupReady { - return true - } - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - if _, err := mgr.client.Ping(ctx).Result(); err != nil { - mgr.Logger.Info("connecting to redis failed", "host", mgr.clientSettings.Host, "error", err) - return false - } - - mgr.DBStartupReady = true - mgr.Logger.Info("DB startup ready") - return true -} - -func tokenizeCmd2Args(cmd string) []interface{} { - args := strings.Split(cmd, " ") - redisArgs := make([]interface{}, 0, len(args)) - for _, arg := range args { - redisArgs = append(redisArgs, arg) - } - return redisArgs -} diff --git a/pkg/lorry/engines/redis/manager_test.go b/pkg/lorry/engines/redis/manager_test.go deleted file mode 100644 index 62556b92ac9..00000000000 --- a/pkg/lorry/engines/redis/manager_test.go +++ /dev/null @@ -1,586 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -const ( -// testData = `{"data":"data"}` -// testKey = "test" -// redisHost = "127.0.0.1:6379" - -// userName = "kiminonawa" -// password = "moss" -// roleName = util.ReadWriteRole -) - -var _ = Describe("Redis DBManager", func() { - // Set up relevant viper config variables - viper.Set(constant.KBEnvServiceUser, "testuser") - viper.Set(constant.KBEnvServicePassword, "testpassword") - Context("new db manager", func() { - It("with right configurations", func() { - properties := engines.Properties{ - "url": "127.0.0.1", - } - dbManger, err := NewManager(properties) - Expect(err).Should(Succeed()) - Expect(dbManger).ShouldNot(BeNil()) - }) - - It("with wrong configurations", func() { - properties := engines.Properties{ - "poolSize": "wrong-number", - } - dbManger, err := NewManager(properties) - Expect(err).Should(HaveOccurred()) - Expect(dbManger).Should(BeNil()) - }) - }) -}) - -// func TestRedisInit(t *testing.T) { -// r, _ := mockRedisOps(t) -// defer r.Close() -// // make sure operations are inited -// assert.NotNil(t, r.client) -// assert.NotNil(t, r.OperationsMap[util.ListUsersOp]) -// assert.NotNil(t, r.OperationsMap[util.CreateUserOp]) -// assert.NotNil(t, r.OperationsMap[util.DeleteUserOp]) -// assert.NotNil(t, r.OperationsMap[util.DescribeUserOp]) -// assert.NotNil(t, r.OperationsMap[util.GrantUserRoleOp]) -// assert.NotNil(t, r.OperationsMap[util.RevokeUserRoleOp]) -// } -// func TestRedisInvokeCreate(t *testing.T) { -// r, mock := mockRedisOps(t) -// defer r.Close() -// -// result := OpsResult{} -// request := &ProbeRequest{ -// Data: []byte(testData), -// Metadata: map[string]string{"key": testKey}, -// Operation: util.CreateOperation, -// } -// // mock expectation -// mock.ExpectDo("SET", testKey, testData).SetVal("ok") -// -// // invoke -// bindingRes, err := r.Invoke(context.TODO(), request) -// assert.Equal(t, nil, err) -// assert.NotNil(t, bindingRes) -// assert.NotNil(t, bindingRes.Data) -// -// err = json.Unmarshal(bindingRes.Data, &result) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveSucc, result[util.RespFieldEvent], result[util.RespFieldMessage]) -// } -// -// func TestRedisInvokeGet(t *testing.T) { -// r, mock := mockRedisOps(t) -// defer r.Close() -// -// opsResult := OpsResult{} -// request := &ProbeRequest{ -// Metadata: map[string]string{"key": testKey}, -// Operation: util.GetOperation, -// } -// // mock expectation, set to nil -// mock.ExpectDo("GET", testKey).RedisNil() -// mock.ExpectDo("GET", testKey).SetVal(testData) -// -// // invoke create -// bindingRes, err := r.Invoke(context.TODO(), request) -// assert.Nil(t, err) -// assert.NotNil(t, bindingRes) -// assert.NotNil(t, bindingRes.Data) -// err = json.Unmarshal(bindingRes.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveFail, opsResult[util.RespFieldEvent]) -// -// // invoke one more time -// bindingRes, err = r.Invoke(context.TODO(), request) -// assert.Nil(t, err) -// assert.NotNil(t, bindingRes.Data) -// err = json.Unmarshal(bindingRes.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent]) -// var o1 interface{} -// _ = json.Unmarshal([]byte(opsResult[util.RespFieldMessage].(string)), &o1) -// assert.Equal(t, testData, o1) -// } -// -// func TestRedisInvokeDelete(t *testing.T) { -// r, mock := mockRedisOps(t) -// defer r.Close() -// -// opsResult := OpsResult{} -// request := &ProbeRequest{ -// Metadata: map[string]string{"key": testKey}, -// Operation: util.DeleteOperation, -// } -// // mock expectation, set to err -// mock.ExpectDo("DEL", testKey).SetVal("ok") -// -// // invoke delete -// bindingRes, err := r.Invoke(context.TODO(), request) -// assert.Nil(t, err) -// assert.NotNil(t, bindingRes) -// assert.NotNil(t, bindingRes.Data) -// err = json.Unmarshal(bindingRes.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent]) -// } -// -// func TestRedisGetRoles(t *testing.T) { -// r, mock := mockRedisOps(t) -// defer r.Close() -// -// opsResult := OpsResult{} -// request := &ProbeRequest{ -// Operation: util.GetRoleOperation, -// } -// -// // mock expectation, set to err -// mock.ExpectInfo("Replication").SetVal("role:master\r\nconnected_slaves:1") -// mock.ExpectInfo("Replication").SetVal("role:slave\r\nmaster_port:6379") -// // invoke request -// bindingRes, err := r.Invoke(context.TODO(), request) -// assert.Nil(t, err) -// assert.NotNil(t, bindingRes) -// assert.NotNil(t, bindingRes.Data) -// err = json.Unmarshal(bindingRes.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent]) -// assert.Equal(t, PRIMARY, opsResult["role"]) -// -// // invoke one more time -// bindingRes, err = r.Invoke(context.TODO(), request) -// assert.Nil(t, err) -// err = json.Unmarshal(bindingRes.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent]) -// assert.Equal(t, SECONDARY, opsResult["role"]) -// } -// -// func TestRedisAccounts(t *testing.T) { -// // prepare -// r, mock := mockRedisOps(t) -// defer r.Close() -// -// ctx := context.TODO() -// // list accounts -// t.Run("List Accounts", func(t *testing.T) { -// mock.ExpectDo("ACL", "USERS").SetVal([]string{"ape", "default", "kbadmin"}) -// -// response, err := r.Invoke(ctx, &ProbeRequest{ -// Operation: util.ListUsersOp, -// }) -// -// assert.Nil(t, err) -// assert.NotNil(t, response) -// assert.NotNil(t, response.Data) -// // parse result -// opsResult := OpsResult{} -// _ = json.Unmarshal(response.Data, &opsResult) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// -// users := make([]util.UserInfo, 0) -// err = json.Unmarshal([]byte(opsResult[util.RespFieldMessage].(string)), &users) -// assert.Nil(t, err) -// assert.NotEmpty(t, users) -// user := users[0] -// assert.Equal(t, "ape", user.UserName) -// mock.ClearExpect() -// }) -// -// // create accounts -// t.Run("Create Accounts", func(t *testing.T) { -// -// var ( -// err error -// opsResult = OpsResult{} -// response *ProbeResponse -// request = &ProbeRequest{ -// Operation: util.CreateUserOp, -// } -// ) -// -// testCases := []redisTestCase{ -// { -// testName: "emptymeta", -// testMetaData: map[string]string{}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "nousername", -// testMetaData: map[string]string{"password": "moli"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "nopasswd", -// testMetaData: map[string]string{"userName": "namae"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoPassword.Error(), -// }, -// { -// testName: "validInput", -// testMetaData: map[string]string{ -// "userName": userName, -// "password": password, -// }, -// expectEveType: util.RespEveSucc, -// expectEveMsg: fmt.Sprintf("created user: %s", userName), -// }, -// } -// // mock a user -// mock.ExpectDo("ACL", "SETUSER", userName, ">"+password).SetVal("ok") -// -// for _, accTest := range testCases { -// request.Metadata = accTest.testMetaData -// response, err = r.Invoke(ctx, request) -// assert.Nil(t, err) -// assert.NotNil(t, response.Data) -// err = json.Unmarshal(response.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, accTest.expectEveType, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// assert.Contains(t, opsResult[util.RespFieldMessage], accTest.expectEveMsg) -// } -// mock.ClearExpect() -// }) -// // grant and revoke role -// t.Run("Grant Accounts", func(t *testing.T) { -// -// var ( -// err error -// opsResult = OpsResult{} -// response *ProbeResponse -// ) -// -// testCases := []redisTestCase{ -// { -// testName: "emptymeta", -// testMetaData: map[string]string{}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "nousername", -// testMetaData: map[string]string{"password": "moli"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "norolename", -// testMetaData: map[string]string{"userName": "namae"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoRoleName.Error(), -// }, -// { -// testName: "invalidRoleName", -// testMetaData: map[string]string{"userName": "namae", "roleName": "superman"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrInvalidRoleName.Error(), -// }, -// { -// testName: "validInput", -// testMetaData: map[string]string{ -// "userName": userName, -// "roleName": (string)(roleName), -// }, -// expectEveType: util.RespEveSucc, -// }, -// } -// -// for _, ops := range []util.OperationKind{util.GrantUserRoleOp, util.RevokeUserRoleOp} { -// // mock exepctation -// args := tokenizeCmd2Args(fmt.Sprintf("ACL SETUSER %s %s", userName, r.role2Priv(ops, (string)(roleName)))) -// mock.ExpectDo(args...).SetVal("ok") -// -// request := &ProbeRequest{ -// Operation: ops, -// } -// for _, accTest := range testCases { -// request.Metadata = accTest.testMetaData -// response, err = r.Invoke(ctx, request) -// assert.Nil(t, err) -// assert.NotNil(t, response.Data) -// err = json.Unmarshal(response.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, accTest.expectEveType, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// if len(accTest.expectEveMsg) > 0 { -// assert.Contains(t, accTest.expectEveMsg, opsResult[util.RespFieldMessage]) -// } -// } -// } -// mock.ClearExpect() -// }) -// -// // desc accounts -// t.Run("Desc Accounts", func(t *testing.T) { -// var ( -// err error -// opsResult = OpsResult{} -// response *ProbeResponse -// request = &ProbeRequest{ -// Operation: util.DescribeUserOp, -// } -// // mock a user, describing it as an array of interface{} -// userInfo = []interface{}{ -// "flags", -// []interface{}{"on"}, -// "passwords", -// []interface{}{"mock-password"}, -// "commands", -// "+@all", -// "keys", -// "~*", -// "channels", -// "", -// "selectors", -// []interface{}{}, -// } -// -// userInfoMap = map[string]interface{}{ -// "flags": []interface{}{"on"}, -// "passwords": []interface{}{"mock-password"}, -// "commands": "+@all", -// "keys": "~*", -// "channels": "", -// "selectors": []interface{}{}, -// } -// ) -// -// testCases := []redisTestCase{ -// { -// testName: "emptymeta", -// testMetaData: map[string]string{}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "nousername", -// testMetaData: map[string]string{"password": "moli"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "validInputButNil", -// testMetaData: map[string]string{ -// "userName": userName, -// }, -// expectEveType: util.RespEveFail, -// expectEveMsg: "redis: nil", -// }, -// { -// testName: "validInput", -// testMetaData: map[string]string{ -// "userName": userName, -// }, -// expectEveType: util.RespEveSucc, -// }, -// { -// testName: "validInputAsMap", -// testMetaData: map[string]string{ -// "userName": userName, -// }, -// expectEveType: util.RespEveSucc, -// }, -// } -// -// mock.ExpectDo("ACL", "GETUSER", userName).RedisNil() -// mock.ExpectDo("ACL", "GETUSER", userName).SetVal(userInfo) -// mock.ExpectDo("ACL", "GETUSER", userName).SetVal(userInfoMap) -// -// for _, accTest := range testCases { -// request.Metadata = accTest.testMetaData -// response, err = r.Invoke(ctx, request) -// assert.Nil(t, err) -// assert.NotNil(t, response.Data) -// err = json.Unmarshal(response.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, accTest.expectEveType, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// if len(accTest.expectEveMsg) > 0 { -// assert.Contains(t, opsResult[util.RespFieldMessage], accTest.expectEveMsg) -// } -// if util.RespEveSucc == opsResult[util.RespFieldEvent] { -// // parse user info -// users := make([]util.UserInfo, 0) -// err = json.Unmarshal([]byte(opsResult[util.RespFieldMessage].(string)), &users) -// assert.Nil(t, err) -// assert.Len(t, users, 1) -// user := users[0] -// assert.Equal(t, userName, user.UserName) -// assert.True(t, util.SuperUserRole.EqualTo(user.RoleName)) -// } -// } -// mock.ClearExpect() -// }) -// // delete accounts -// t.Run("Delete Accounts", func(t *testing.T) { -// -// var ( -// err error -// opsResult = OpsResult{} -// response *ProbeResponse -// request = &ProbeRequest{ -// Operation: util.DeleteUserOp, -// } -// ) -// -// testCases := []redisTestCase{ -// { -// testName: "emptymeta", -// testMetaData: map[string]string{}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "nousername", -// testMetaData: map[string]string{"password": "moli"}, -// expectEveType: util.RespEveFail, -// expectEveMsg: ErrNoUserName.Error(), -// }, -// { -// testName: "validInput", -// testMetaData: map[string]string{ -// "userName": userName, -// }, -// expectEveType: util.RespEveSucc, -// expectEveMsg: fmt.Sprintf("deleted user: %s", userName), -// }, -// } -// // mock a user -// mock.ExpectDo("ACL", "DELUSER", userName).SetVal("ok") -// -// for _, accTest := range testCases { -// request.Metadata = accTest.testMetaData -// response, err = r.Invoke(ctx, request) -// assert.Nil(t, err) -// assert.NotNil(t, response.Data) -// err = json.Unmarshal(response.Data, &opsResult) -// assert.Nil(t, err) -// assert.Equal(t, accTest.expectEveType, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// assert.Contains(t, opsResult[util.RespFieldMessage], accTest.expectEveMsg) -// } -// mock.ClearExpect() -// }) -// -// t.Run("RoleName Conversion", func(t *testing.T) { -// type roleTestCase struct { -// roleName util.RoleType -// redisPrivs string -// } -// grantTestCases := []roleTestCase{ -// { -// util.SuperUserRole, -// "+@all allkeys", -// }, -// { -// util.ReadWriteRole, -// "-@all +@write +@read allkeys", -// }, -// { -// util.ReadOnlyRole, -// "-@all +@read allkeys", -// }, -// } -// for _, test := range grantTestCases { -// cmd := r.role2Priv(util.GrantUserRoleOp, (string)(test.roleName)) -// assert.Equal(t, test.redisPrivs, cmd) -// -// // allkeys -> ~* -// cmd = strings.Replace(cmd, "allkeys", "~*", 1) -// inferredRole := r.priv2Role(cmd) -// assert.Equal(t, test.roleName, inferredRole) -// } -// -// revokeTestCases := []roleTestCase{ -// { -// util.SuperUserRole, -// "-@all allkeys", -// }, -// { -// util.ReadWriteRole, -// "-@all -@write -@read allkeys", -// }, -// { -// util.ReadOnlyRole, -// "-@all -@read allkeys", -// }, -// } -// for _, test := range revokeTestCases { -// cmd := r.role2Priv(util.RevokeUserRoleOp, (string)(test.roleName)) -// assert.Equal(t, test.redisPrivs, cmd) -// } -// }) -// // list accounts -// t.Run("List System Accounts", func(t *testing.T) { -// mock.ExpectDo("ACL", "USERS").SetVal([]string{"ape", "default", "kbadmin"}) -// -// response, err := r.Invoke(ctx, &ProbeRequest{ -// Operation: util.ListSystemAccountsOp, -// }) -// -// assert.Nil(t, err) -// assert.NotNil(t, response) -// assert.NotNil(t, response.Data) -// // parse result -// opsResult := OpsResult{} -// _ = json.Unmarshal(response.Data, &opsResult) -// assert.Equal(t, util.RespEveSucc, opsResult[util.RespFieldEvent], opsResult[util.RespFieldMessage]) -// -// users := []string{} -// err = json.Unmarshal([]byte(opsResult[util.RespFieldMessage].(string)), &users) -// assert.Nil(t, err) -// assert.NotEmpty(t, users) -// assert.Len(t, users, 2) -// assert.Contains(t, users, "kbadmin") -// assert.Contains(t, users, "default") -// mock.ClearExpect() -// }) -// } -// -// func mockRedisOps(t *testing.T) (*Redis, redismock.ClientMock) { -// client, mock := redismock.NewClientMock() -// viper.SetDefault("KB_ROLECHECK_DELAY", "0") -// -// if client == nil || mock == nil { -// t.Fatalf("failed to mock a redis client") -// return nil, nil -// } -// r := &Redis{} -// development, _ := zap.NewDevelopment() -// r.Logger = zapr.NewLogger(development) -// r.client = client -// r.ctx, r.cancel = context.WithCancel(context.Background()) -// _ = r.Init(nil) -// r.DBPort = 6379 -// return r, mock -// } -// diff --git a/pkg/lorry/engines/redis/metadata.go b/pkg/lorry/engines/redis/metadata.go deleted file mode 100644 index 73cc67a3ad1..00000000000 --- a/pkg/lorry/engines/redis/metadata.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "fmt" - "strconv" - "time" -) - -const ( - maxRetries = "maxRetries" - maxRetryBackoff = "maxRetryBackoff" - ttlInSeconds = "ttlInSeconds" - queryIndexes = "queryIndexes" - defaultBase = 10 - defaultBitSize = 0 - defaultMaxRetries = 3 - defaultMaxRetryBackoff = time.Second * 2 -) - -type Metadata struct { - MaxRetries int - MaxRetryBackoff time.Duration - TTLInSeconds *int - QueryIndexes string -} - -func ParseRedisMetadata(properties map[string]string) (Metadata, error) { - m := Metadata{} - - m.MaxRetries = defaultMaxRetries - if val, ok := properties[maxRetries]; ok && val != "" { - parsedVal, err := strconv.ParseInt(val, defaultBase, defaultBitSize) - if err != nil { - return m, fmt.Errorf("redis store error: can't parse maxRetries field: %s", err) - } - m.MaxRetries = int(parsedVal) - } - - m.MaxRetryBackoff = defaultMaxRetryBackoff - if val, ok := properties[maxRetryBackoff]; ok && val != "" { - parsedVal, err := strconv.ParseInt(val, defaultBase, defaultBitSize) - if err != nil { - return m, fmt.Errorf("redis store error: can't parse maxRetryBackoff field: %s", err) - } - m.MaxRetryBackoff = time.Duration(parsedVal) - } - - if val, ok := properties[ttlInSeconds]; ok && val != "" { - parsedVal, err := strconv.ParseInt(val, defaultBase, defaultBitSize) - if err != nil { - return m, fmt.Errorf("redis store error: can't parse ttlInSeconds field: %s", err) - } - intVal := int(parsedVal) - m.TTLInSeconds = &intVal - } else { - m.TTLInSeconds = nil - } - - if val, ok := properties[queryIndexes]; ok && val != "" { - m.QueryIndexes = val - } - return m, nil -} diff --git a/pkg/lorry/engines/redis/query.go b/pkg/lorry/engines/redis/query.go deleted file mode 100644 index 453128bbe32..00000000000 --- a/pkg/lorry/engines/redis/query.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "context" - "encoding/json" -) - -func (mgr *Manager) Exec(ctx context.Context, cmd string) (int64, error) { - args := tokenizeCmd2Args(cmd) - return 0, mgr.client.Do(ctx, args...).Err() -} - -func (mgr *Manager) Query(ctx context.Context, cmd string) ([]byte, error) { - args := tokenizeCmd2Args(cmd) - // parse result into a slice of string - data, err := mgr.client.Do(ctx, args...).Result() - if err != nil { - return nil, err - } - // convert interface{} to []byte - switch v := data.(type) { - case map[interface{}]interface{}: - strMap := make(map[string]interface{}) - for key, value := range v { - strMap[key.(string)] = value - } - return json.Marshal(strMap) - default: - return json.Marshal(v) - } -} diff --git a/pkg/lorry/engines/redis/redis.go b/pkg/lorry/engines/redis/redis.go deleted file mode 100644 index 39e8b8a8c91..00000000000 --- a/pkg/lorry/engines/redis/redis.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -# This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "crypto/tls" - "fmt" - "strings" - "time" - - "github.com/redis/go-redis/v9" - - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -const ( - ClusterType = "cluster" - NodeType = "node" -) - -func ParseClientFromProperties(properties map[string]string, defaultSettings *Settings) (client redis.UniversalClient, settings *Settings, err error) { - if defaultSettings == nil { - settings = &Settings{} - } else { - settings = defaultSettings - } - err = settings.Decode(properties) - if err != nil { - return nil, nil, fmt.Errorf("redis client configuration error: %w", err) - } - if settings.Failover { - return newFailoverClient(settings), settings, nil - } - - return newClient(settings), settings, nil -} - -func newFailoverClient(s *Settings) redis.UniversalClient { - if s == nil { - return nil - } - opts := &redis.FailoverOptions{ - DB: s.DB, - MasterName: s.SentinelMasterName, - SentinelAddrs: []string{s.Host}, - Password: s.Password, - Username: s.Username, - MaxRetries: s.RedisMaxRetries, - MaxRetryBackoff: time.Duration(s.RedisMaxRetryInterval), - MinRetryBackoff: time.Duration(s.RedisMinRetryInterval), - DialTimeout: time.Duration(s.DialTimeout), - ReadTimeout: time.Duration(s.ReadTimeout), - WriteTimeout: time.Duration(s.WriteTimeout), - PoolSize: s.PoolSize, - MinIdleConns: s.MinIdleConns, - PoolTimeout: time.Duration(s.PoolTimeout), - } - - /* #nosec */ - if s.EnableTLS { - opts.TLSConfig = &tls.Config{ - InsecureSkipVerify: s.EnableTLS, - } - } - - if s.RedisType == ClusterType { - opts.SentinelAddrs = strings.Split(s.Host, ",") - - return redis.NewFailoverClusterClient(opts) - } - - return redis.NewFailoverClient(opts) -} - -func newClient(s *Settings) redis.UniversalClient { - if s == nil { - return nil - } - if s.RedisType == ClusterType { - options := &redis.ClusterOptions{ - Addrs: strings.Split(s.Host, ","), - Password: s.Password, - Username: s.Username, - MaxRetries: s.RedisMaxRetries, - MaxRetryBackoff: time.Duration(s.RedisMaxRetryInterval), - MinRetryBackoff: time.Duration(s.RedisMinRetryInterval), - DialTimeout: time.Duration(s.DialTimeout), - ReadTimeout: time.Duration(s.ReadTimeout), - WriteTimeout: time.Duration(s.WriteTimeout), - PoolSize: s.PoolSize, - MinIdleConns: s.MinIdleConns, - PoolTimeout: time.Duration(s.PoolTimeout), - } - /* #nosec */ - if s.EnableTLS { - options.TLSConfig = &tls.Config{ - InsecureSkipVerify: s.EnableTLS, - } - } - - return redis.NewClusterClient(options) - } - - options := &redis.Options{ - Addr: s.Host, - Password: s.Password, - Username: s.Username, - DB: s.DB, - MaxRetries: s.RedisMaxRetries, - MaxRetryBackoff: time.Duration(s.RedisMaxRetryInterval), - MinRetryBackoff: time.Duration(s.RedisMinRetryInterval), - DialTimeout: time.Duration(s.DialTimeout), - ReadTimeout: time.Duration(s.ReadTimeout), - WriteTimeout: time.Duration(s.WriteTimeout), - PoolSize: s.PoolSize, - MinIdleConns: s.MinIdleConns, - PoolTimeout: time.Duration(s.PoolTimeout), - } - - /* #nosec */ - if s.EnableTLS { - options.TLSConfig = &tls.Config{ - InsecureSkipVerify: s.EnableTLS, - } - } - - return redis.NewClient(options) -} - -func newSentinelClient(s *Settings, clusterCompName string) *redis.SentinelClient { - // TODO: use headless service directly - sentinelEnv := fmt.Sprintf("%s_SENTINEL_SERVICE", strings.ToUpper(strings.Join(strings.Split(clusterCompName, "-"), "_"))) - sentinelHost := viper.GetString(fmt.Sprintf("%s_HOST", sentinelEnv)) - sentinelPort := viper.GetString(fmt.Sprintf("%s_PORT", sentinelEnv)) - - opt := &redis.Options{ - DB: s.DB, - Addr: fmt.Sprintf("%s:%s", sentinelHost, sentinelPort), - Password: s.Password, - Username: s.Username, - MaxRetries: s.RedisMaxRetries, - MaxRetryBackoff: time.Duration(s.RedisMaxRetryInterval), - MinRetryBackoff: time.Duration(s.RedisMinRetryInterval), - DialTimeout: time.Duration(s.DialTimeout), - ReadTimeout: time.Duration(s.ReadTimeout), - WriteTimeout: time.Duration(s.WriteTimeout), - PoolSize: s.PoolSize, - MinIdleConns: s.MinIdleConns, - PoolTimeout: time.Duration(s.PoolTimeout), - } - - sentinelClient := redis.NewSentinelClient(opt) - - return sentinelClient -} diff --git a/pkg/lorry/engines/redis/settings.go b/pkg/lorry/engines/redis/settings.go deleted file mode 100644 index 96ab762788b..00000000000 --- a/pkg/lorry/engines/redis/settings.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "fmt" - "strconv" - "time" - - "github.com/apecloud/kubeblocks/pkg/lorry/util/config" -) - -type Settings struct { - // The Redis host - Host string `mapstructure:"redisHost"` - // The Redis password - Password string `mapstructure:"redisPassword"` - // The Redis username - Username string `mapstructure:"redisUsername"` - // Database to be selected after connecting to the server. - DB int `mapstructure:"redisDB"` - // The redis type node or cluster - RedisType string `mapstructure:"redisType"` - // Maximum number of retries before giving up. - // A value of -1 (not 0) disables retries - // Default is 3 retries - RedisMaxRetries int `mapstructure:"redisMaxRetries"` - // Minimum backoff between each retry. - // Default is 8 milliseconds; -1 disables backoff. - RedisMinRetryInterval Duration `mapstructure:"redisMinRetryInterval"` - // Maximum backoff between each retry. - // Default is 512 milliseconds; -1 disables backoff. - RedisMaxRetryInterval Duration `mapstructure:"redisMaxRetryInterval"` - // Dial timeout for establishing new connections. - DialTimeout Duration `mapstructure:"dialTimeout"` - // Timeout for socket reads. If reached, commands will fail - // with a timeout instead of blocking. Use value -1 for no timeout and 0 for default. - ReadTimeout Duration `mapstructure:"readTimeout"` - // Timeout for socket writes. If reached, commands will fail - WriteTimeout Duration `mapstructure:"writeTimeout"` - // Maximum number of socket connections. - PoolSize int `mapstructure:"poolSize"` - // Minimum number of idle connections which is useful when establishing - // new connection is slow. - MinIdleConns int `mapstructure:"minIdleConns"` - // Connection age at which client retires (closes) the connection. - // Default is to not close aged connections. - MaxConnAge Duration `mapstructure:"maxConnAge"` - // Amount of time client waits for connection if all connections - // are busy before returning an error. - // Default is ReadTimeout + 1 second. - PoolTimeout Duration `mapstructure:"poolTimeout"` - // Amount of time after which client closes idle connections. - // Should be less than server's timeout. - // Default is 5 minutes. -1 disables idle timeout check. - IdleTimeout Duration `mapstructure:"idleTimeout"` - // Frequency of idle checks made by idle connections reaper. - // Default is 1 minute. -1 disables idle connections reaper, - // but idle connections are still discarded by the client - // if IdleTimeout is set. - IdleCheckFrequency Duration `mapstructure:"idleCheckFrequency"` - // The master name - SentinelMasterName string `mapstructure:"sentinelMasterName"` - // Use Redis Sentinel for automatic failover. - Failover bool `mapstructure:"failover"` - - // A flag to enables TLS by setting InsecureSkipVerify to true - EnableTLS bool `mapstructure:"enableTLS"` -} - -func (s *Settings) Decode(in interface{}) error { - if err := config.Decode(in, s); err != nil { - return fmt.Errorf("decode failed. %w", err) - } - - return nil -} - -type Duration time.Duration - -func (r *Duration) DecodeString(value string) error { - if val, err := strconv.Atoi(value); err == nil { - if val < 0 { - *r = Duration(val) - - return nil - } - *r = Duration(time.Duration(val) * time.Millisecond) - - return nil - } - - // Convert it by parsing - d, err := time.ParseDuration(value) - if err != nil { - return err - } - - *r = Duration(d) - - return nil -} diff --git a/pkg/lorry/engines/redis/settings_test.go b/pkg/lorry/engines/redis/settings_test.go deleted file mode 100644 index aac5018c363..00000000000 --- a/pkg/lorry/engines/redis/settings_test.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const ( - host = "redisHost" - password = "redisPassword" - username = "redisUsername" - db = "redisDB" - redisType = "redisType" - redisMaxRetries = "redisMaxRetries" - redisMinRetryInterval = "redisMinRetryInterval" - redisMaxRetryInterval = "redisMaxRetryInterval" - dialTimeout = "dialTimeout" - readTimeout = "readTimeout" - writeTimeout = "writeTimeout" - poolSize = "poolSize" - minIdleConns = "minIdleConns" - poolTimeout = "poolTimeout" - idleTimeout = "idleTimeout" - idleCheckFrequency = "idleCheckFrequency" - maxConnAge = "maxConnAge" - enableTLS = "enableTLS" - failover = "failover" - sentinelMasterName = "sentinelMasterName" -) - -func getFakeProperties() map[string]string { - return map[string]string{ - host: "fake.redis.com", - password: "fakePassword", - username: "fakeUsername", - redisType: "node", - enableTLS: "true", - dialTimeout: "5s", - readTimeout: "5s", - writeTimeout: "50000", - poolSize: "20", - maxConnAge: "200s", - db: "1", - redisMaxRetries: "1", - redisMinRetryInterval: "8ms", - redisMaxRetryInterval: "1s", - minIdleConns: "1", - poolTimeout: "1s", - idleTimeout: "1s", - idleCheckFrequency: "1s", - failover: "true", - sentinelMasterName: "master", - } -} - -func TestParseRedisMetadata(t *testing.T) { - t.Run("ClientMetadata is correct", func(t *testing.T) { - fakeProperties := getFakeProperties() - - // act - m := &Settings{} - err := m.Decode(fakeProperties) - - // assert - assert.NoError(t, err) - assert.Equal(t, fakeProperties[host], m.Host) - assert.Equal(t, fakeProperties[password], m.Password) - assert.Equal(t, fakeProperties[username], m.Username) - assert.Equal(t, fakeProperties[redisType], m.RedisType) - assert.Equal(t, true, m.EnableTLS) - assert.Equal(t, 5*time.Second, time.Duration(m.DialTimeout)) - assert.Equal(t, 5*time.Second, time.Duration(m.ReadTimeout)) - assert.Equal(t, 50000*time.Millisecond, time.Duration(m.WriteTimeout)) - assert.Equal(t, 20, m.PoolSize) - assert.Equal(t, 200*time.Second, time.Duration(m.MaxConnAge)) - assert.Equal(t, 1, m.DB) - assert.Equal(t, 1, m.RedisMaxRetries) - assert.Equal(t, 8*time.Millisecond, time.Duration(m.RedisMinRetryInterval)) - assert.Equal(t, 1*time.Second, time.Duration(m.RedisMaxRetryInterval)) - assert.Equal(t, 1, m.MinIdleConns) - assert.Equal(t, 1*time.Second, time.Duration(m.PoolTimeout)) - assert.Equal(t, 1*time.Second, time.Duration(m.IdleTimeout)) - assert.Equal(t, 1*time.Second, time.Duration(m.IdleCheckFrequency)) - assert.Equal(t, true, m.Failover) - assert.Equal(t, "master", m.SentinelMasterName) - }) - - t.Run("host is not given", func(t *testing.T) { - fakeProperties := getFakeProperties() - - fakeProperties[host] = "" - - // act - m := &Settings{} - err := m.Decode(fakeProperties) - - // assert - // m.Decode dose not return error when host is "" - assert.Error(t, errors.New("redis streams error: missing host address"), err) - assert.Empty(t, m.Host) - }) - - t.Run("check values can be set as -1", func(t *testing.T) { - fakeProperties := getFakeProperties() - - fakeProperties[readTimeout] = "-1" - fakeProperties[idleTimeout] = "-1" - fakeProperties[idleCheckFrequency] = "-1" - fakeProperties[redisMaxRetryInterval] = "-1" - fakeProperties[redisMinRetryInterval] = "-1" - - // act - m := &Settings{} - err := m.Decode(fakeProperties) - // assert - assert.NoError(t, err) - assert.True(t, m.ReadTimeout == -1) - assert.True(t, m.IdleTimeout == -1) - assert.True(t, m.IdleCheckFrequency == -1) - assert.True(t, m.RedisMaxRetryInterval == -1) - assert.True(t, m.RedisMinRetryInterval == -1) - }) -} diff --git a/pkg/lorry/engines/redis/suite_test.go b/pkg/lorry/engines/redis/suite_test.go deleted file mode 100644 index bce30cea869..00000000000 --- a/pkg/lorry/engines/redis/suite_test.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -var ( - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test-0") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestRedisDBManager(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Redis DBManager. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock dcs store - InitMockDCSStore() -}) - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} diff --git a/pkg/lorry/engines/redis/user.go b/pkg/lorry/engines/redis/user.go deleted file mode 100644 index 409aa7ca780..00000000000 --- a/pkg/lorry/engines/redis/user.go +++ /dev/null @@ -1,285 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package redis - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "golang.org/x/exp/slices" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - listUserTpl = "ACL USERS" - descUserTpl = "ACL GETUSER %s" - createUserTpl = "ACL SETUSER %s >%s" - dropUserTpl = "ACL DELUSER %s" - grantTpl = "ACL SETUSER %s %s" - revokeTpl = "ACL SETUSER %s %s" -) - -var ( - redisPreDefinedUsers = []string{ - "default", - "kbadmin", - "kbdataprotection", - "kbmonitoring", - "kbprobe", - "kbreplicator", - } -) - -func (mgr *Manager) ListUsers(ctx context.Context) ([]models.UserInfo, error) { - data, err := mgr.Query(ctx, listUserTpl) - if err != nil { - mgr.Logger.Error(err, "error executing %s") - return nil, err - } - - results := make([]string, 0) - err = json.Unmarshal(data, &results) - if err != nil { - return nil, err - } - users := make([]models.UserInfo, 0) - for _, userInfo := range results { - userName := strings.TrimSpace(userInfo) - if slices.Contains(redisPreDefinedUsers, userName) { - continue - } - user := models.UserInfo{UserName: userName} - users = append(users, user) - } - return users, nil -} - -func (mgr *Manager) ListSystemAccounts(ctx context.Context) ([]models.UserInfo, error) { - data, err := mgr.Query(ctx, listUserTpl) - if err != nil { - mgr.Logger.Error(err, "error executing %s") - return nil, err - } - - results := make([]string, 0) - err = json.Unmarshal(data, &results) - if err != nil { - return nil, err - } - users := make([]models.UserInfo, 0) - for _, userInfo := range results { - userName := strings.TrimSpace(userInfo) - if !slices.Contains(redisPreDefinedUsers, userName) { - continue - } - user := models.UserInfo{UserName: userName} - users = append(users, user) - } - return users, nil -} - -func (mgr *Manager) DescribeUser(ctx context.Context, userName string) (*models.UserInfo, error) { - sql := fmt.Sprintf(descUserTpl, userName) - - data, err := mgr.Query(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return nil, err - } - - // parse it to a map or an []interface - // try map first - var profile map[string]string - profile, err = parseCommandAndKeyFromMap(data) - if err != nil { - // try list - profile, err = parseCommandAndKeyFromList(data) - if err != nil { - return nil, err - } - } - - user := &models.UserInfo{ - UserName: userName, - RoleName: (string)(priv2Role(profile["commands"] + " " + profile["keys"])), - } - return user, nil -} - -func (mgr *Manager) CreateUser(ctx context.Context, userName, password, _ string) error { - sql := fmt.Sprintf(createUserTpl, userName, password) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Info("create user failed", "error", err.Error()) - return err - } - - return nil -} - -func (mgr *Manager) DeleteUser(ctx context.Context, userName string) error { - sql := fmt.Sprintf(dropUserTpl, userName) - - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) GrantUserRole(ctx context.Context, userName, roleName string) error { - var sql string - command := role2Priv("+", roleName) - sql = fmt.Sprintf(grantTpl, userName, command) - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func (mgr *Manager) RevokeUserRole(ctx context.Context, userName, roleName string) error { - var sql string - command := role2Priv("-", roleName) - sql = fmt.Sprintf(revokeTpl, userName, command) - _, err := mgr.Exec(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "execute sql failed", "sql", sql) - return err - } - - return nil -} - -func role2Priv(prefix, roleName string) string { - var command string - - roleType := models.String2RoleType(roleName) - switch roleType { - case models.SuperUserRole: - command = fmt.Sprintf("%s@all allkeys", prefix) - case models.ReadWriteRole: - command = fmt.Sprintf("-@all %s@write %s@read allkeys", prefix, prefix) - case models.ReadOnlyRole: - command = fmt.Sprintf("-@all %s@read allkeys", prefix) - } - return command -} - -func priv2Role(commands string) models.RoleType { - if commands == "-@all" { - return models.NoPrivileges - } - switch commands { - case "-@all +@read ~*": - return models.ReadOnlyRole - case "-@all +@write +@read ~*": - return models.ReadWriteRole - case "+@all ~*": - return models.SuperUserRole - default: - return models.CustomizedRole - } -} - -func parseCommandAndKeyFromMap(data interface{}) (map[string]string, error) { - var ( - redisUserPrivContxt = []string{"commands", "keys", "channels", "selectors"} - ) - - profile := make(map[string]string, 0) - results := make(map[string]interface{}, 0) - - err := json.Unmarshal(data.([]byte), &results) - if err != nil { - return nil, err - } - for k, v := range results { - // each key is string, and each v is string or list of string - if !slices.Contains(redisUserPrivContxt, k) { - continue - } - - switch v := v.(type) { - case string: - profile[k] = v - case []interface{}: - selectors := make([]string, 0) - for _, sel := range v { - selectors = append(selectors, sel.(string)) - } - profile[k] = strings.Join(selectors, " ") - default: - return nil, fmt.Errorf("unknown data type: %v", v) - } - } - return profile, nil -} - -func parseCommandAndKeyFromList(data interface{}) (map[string]string, error) { - var ( - redisUserPrivContxt = []string{"commands", "keys", "channels", "selectors"} - redisUserInfoContext = []string{"flags", "passwords"} - ) - - profile := make(map[string]string, 0) - results := make([]interface{}, 0) - - err := json.Unmarshal(data.([]byte), &results) - if err != nil { - return nil, err - } - // parse line by line - var context string - for i := 0; i < len(results); i++ { - result := results[i] - switch result := result.(type) { - case string: - strVal := strings.TrimSpace(result) - if len(strVal) == 0 { - continue - } - if slices.Contains(redisUserInfoContext, strVal) { - i++ - continue - } - if slices.Contains(redisUserPrivContxt, strVal) { - context = strVal - } else { - profile[context] = strVal - } - case []interface{}: - selectors := make([]string, 0) - for _, sel := range result { - selectors = append(selectors, sel.(string)) - } - profile[context] = strings.Join(selectors, " ") - } - } - return profile, nil -} diff --git a/pkg/lorry/engines/register/managers.go b/pkg/lorry/engines/register/managers.go deleted file mode 100644 index 1d188b0ec95..00000000000 --- a/pkg/lorry/engines/register/managers.go +++ /dev/null @@ -1,238 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package register - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/afero" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/custom" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/etcd" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/foxlake" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mongodb" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/nebula" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/oceanbase" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/opengauss" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/oracle" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/polardbx" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres/apecloudpostgres" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/postgres/officalpostgres" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/pulsar" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/redis" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/wesql" -) - -type managerNewFunc func(engines.Properties) (engines.DBManager, error) - -var managerNewFuncs = make(map[string]managerNewFunc) - -// Lorry runs with a single database engine instance at a time, -// so only one dbManager is initialized and cached here during execution. -var dbManager engines.DBManager -var customManager engines.DBManager - -var fs = afero.NewOsFs() - -func init() { - RegisterEngine(models.MySQL, "consensus", wesql.NewManager, mysql.NewCommands) - RegisterEngine(models.MySQL, "replication", mysql.NewManager, mysql.NewCommands) - RegisterEngine(models.Redis, "replication", redis.NewManager, redis.NewCommands) - RegisterEngine(models.ETCD, "consensus", etcd.NewManager, nil) - RegisterEngine(models.MongoDB, "consensus", mongodb.NewManager, mongodb.NewCommands) - RegisterEngine(models.PolarDBX, "consensus", polardbx.NewManager, mysql.NewCommands) - RegisterEngine(models.PostgreSQL, "replication", officalpostgres.NewManager, postgres.NewCommands) - RegisterEngine(models.PostgreSQL, "consensus", apecloudpostgres.NewManager, postgres.NewCommands) - RegisterEngine(models.FoxLake, "", nil, foxlake.NewCommands) - RegisterEngine(models.Nebula, "", nil, nebula.NewCommands) - RegisterEngine(models.PulsarProxy, "", nil, pulsar.NewProxyCommands) - RegisterEngine(models.PulsarBroker, "", nil, pulsar.NewBrokerCommands) - RegisterEngine(models.Oceanbase, "", oceanbase.NewManager, oceanbase.NewCommands) - RegisterEngine(models.Oracle, "", nil, oracle.NewCommands) - RegisterEngine(models.OpenGauss, "", nil, opengauss.NewCommands) - - // support component definition without workloadType - RegisterEngine(models.WeSQL, "", wesql.NewManager, mysql.NewCommands) - RegisterEngine(models.MySQL, "", mysql.NewManager, mysql.NewCommands) - RegisterEngine(models.Redis, "", redis.NewManager, redis.NewCommands) - RegisterEngine(models.ETCD, "", etcd.NewManager, nil) - RegisterEngine(models.MongoDB, "", mongodb.NewManager, mongodb.NewCommands) - RegisterEngine(models.PolarDBX, "", polardbx.NewManager, mysql.NewCommands) - RegisterEngine(models.PostgreSQL, "", officalpostgres.NewManager, postgres.NewCommands) - RegisterEngine(models.OfficialPostgreSQL, "", officalpostgres.NewManager, postgres.NewCommands) - RegisterEngine(models.ApecloudPostgreSQL, "", apecloudpostgres.NewManager, postgres.NewCommands) - RegisterEngine(models.Custom, "", custom.NewManager, nil) -} - -func RegisterEngine(characterType models.EngineType, workloadType string, newFunc managerNewFunc, newCommand engines.NewCommandFunc) { - key := strings.ToLower(string(characterType) + "_" + workloadType) - managerNewFuncs[key] = newFunc - engines.NewCommandFuncs[string(characterType)] = newCommand -} - -func GetManagerNewFunc(characterType, workloadType string) managerNewFunc { - key := strings.ToLower(characterType + "_" + workloadType) - return managerNewFuncs[key] -} - -func SetCustomManager(manager engines.DBManager) { - customManager = manager -} - -func GetCustomManager(cmd []string) engines.DBManager { - return customManager -} - -func SetDBManager(manager engines.DBManager) { - dbManager = manager -} - -func GetDBManager(cmd []string) (engines.DBManager, error) { - if len(cmd) > 0 { - return customManager, nil - } - if dbManager != nil { - return dbManager, nil - } - - return nil, errors.Errorf("no db manager") -} - -func NewClusterCommands(typeName string) (engines.ClusterCommands, error) { - newFunc, ok := engines.NewCommandFuncs[typeName] - if !ok || newFunc == nil { - return nil, fmt.Errorf("unsupported engine type: %s", typeName) - } - - return newFunc(), nil -} - -func InitDBManager(configDir string) error { - if dbManager != nil { - return nil - } - - ctrl.Log.Info("Initialize DB manager") - workloadType := viper.GetString(constant.KBEnvWorkloadType) - if workloadType == "" { - ctrl.Log.Info(constant.KBEnvWorkloadType + " ENV not set") - } - - characterType := viper.GetString(constant.KBEnvCharacterType) - if viper.IsSet(constant.KBEnvBuiltinHandler) { - workloadType = "" - characterType = viper.GetString(constant.KBEnvBuiltinHandler) - } - if characterType == "" { - ctrl.Log.Info("BuiltinHandler not set, use the custom manager") - characterType = string(models.Custom) - } - - err := GetAllComponent(configDir) // find all builtin config file and read - if err != nil { // Handle errors reading the config file - return errors.Wrap(err, "fatal error config file") - } - - properties := GetProperties(characterType) - newFunc := GetManagerNewFunc(characterType, workloadType) - if newFunc == nil { - return errors.Errorf("no db manager for characterType %s and workloadType %s", characterType, workloadType) - } - mgr, err := newFunc(properties) - if err != nil { - return err - } - - dbManager = mgr - - newCustomFunc := GetManagerNewFunc(string(models.Custom), "") - if newCustomFunc == nil { - return errors.New("no custom db manager") - } - customManager, err = newCustomFunc(properties) - if err != nil { - return err - } - - return nil -} - -type Component struct { - Name string - Spec ComponentSpec -} - -type ComponentSpec struct { - Version string - Metadata []kv -} - -type kv struct { - Name string - Value string -} - -var Name2Property = map[string]engines.Properties{} - -func readConfig(filename string) (string, engines.Properties, error) { - viper.SetConfigType("yaml") - viper.SetConfigFile(filename) - if err := viper.ReadInConfig(); err != nil { - return "", nil, err - } - component := &Component{} - if err := viper.Unmarshal(component); err != nil { - return "", nil, err - } - properties := make(engines.Properties) - properties["version"] = component.Spec.Version - for _, pair := range component.Spec.Metadata { - properties[pair.Name] = pair.Value - } - return component.Name, properties, nil -} - -func GetAllComponent(dir string) error { - files, err := afero.ReadDir(fs, dir) - if err != nil { - return err - } - for _, file := range files { - name, properties, err := readConfig(dir + "/" + file.Name()) - if err != nil { - return err - } - Name2Property[name] = properties - } - return nil -} - -func GetProperties(name string) engines.Properties { - return Name2Property[name] -} diff --git a/pkg/lorry/engines/register/managers_test.go b/pkg/lorry/engines/register/managers_test.go deleted file mode 100644 index d6ac6b98509..00000000000 --- a/pkg/lorry/engines/register/managers_test.go +++ /dev/null @@ -1,218 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package register - -import ( - "fmt" - "os" - "testing" - - "github.com/spf13/afero" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - fakeCharacterType = "fake-db" - fakeWrongContent = "wrong" - fakeConfigContent = ` -name: fake-db -spec: - version: v1 - metadata: - - name: url # Required - value: "user=test password=test host=localhost"` - fakeConfigFile = "/fake-config-file" - fakeConfigDir = "fake-dir" -) - -func TestReadConfig(t *testing.T) { - fs = afero.NewMemMapFs() - viper.SetFs(fs) - defer func() { - fs = afero.NewOsFs() - viper.Reset() - }() - - t.Run("viper read in config failed", func(t *testing.T) { - name, property, err := readConfig(fakeConfigFile) - assert.NotNil(t, err) - assert.Nil(t, property) - assert.Equal(t, "", name) - }) - - file, err := fs.Create(fakeConfigFile) - assert.Nil(t, err) - _, err = file.WriteString(fakeConfigContent) - assert.Nil(t, err) - _ = file.Close() - - t.Run("read config successfully", func(t *testing.T) { - name, property, err := readConfig(fakeConfigFile) - assert.Nil(t, err) - assert.Equal(t, fakeCharacterType, name) - assert.Equal(t, "user=test password=test host=localhost", property["url"]) - }) -} - -func TestGetAllComponent(t *testing.T) { - fs = afero.NewMemMapFs() - viper.SetFs(fs) - defer func() { - fs = afero.NewOsFs() - viper.Reset() - }() - - t.Run("read dir failed", func(t *testing.T) { - err := GetAllComponent(fakeConfigDir) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "file does not exist") - }) - - err := fs.Mkdir(fakeConfigDir, os.ModeDir) - assert.Nil(t, err) - file, err := fs.Create(fakeConfigDir + fakeConfigFile) - assert.Nil(t, err) - _, err = file.WriteString(fakeWrongContent) - assert.Nil(t, err) - _ = file.Close() - - t.Run("read config failed", func(t *testing.T) { - err = GetAllComponent(fakeConfigDir) - assert.NotNil(t, err) - }) - - err = fs.Remove(fakeConfigDir + fakeConfigFile) - assert.Nil(t, err) - file, err = fs.Create(fakeConfigDir + fakeConfigFile) - assert.Nil(t, err) - _, err = file.WriteString(fakeConfigContent) - _ = file.Close() - - t.Run("get all component successfully", func(t *testing.T) { - err = GetAllComponent(fakeConfigDir) - assert.Nil(t, err) - - property := GetProperties(fakeCharacterType) - assert.Equal(t, "user=test password=test host=localhost", property["url"]) - }) -} - -func TestInitDBManager(t *testing.T) { - fs = afero.NewMemMapFs() - viper.SetFs(fs) - realDBManager := dbManager - defer func() { - fs = afero.NewOsFs() - viper.Reset() - dbManager = realDBManager - }() - configDir := fakeConfigDir - - t.Run("characterType not set", func(t *testing.T) { - err := InitDBManager(configDir) - - assert.NotNil(t, err) - // assert.ErrorContains(t, err, "KB_SERVICE_CHARACTER_TYPE not set") - _, err = GetDBManager(nil) - assert.NotNil(t, err) - // assert.ErrorContains(t, err, "no db manager") - }) - - viper.Set(constant.KBEnvBuiltinHandler, fakeCharacterType) - t.Run("get all component failed", func(t *testing.T) { - err := InitDBManager(configDir) - - assert.NotNil(t, err) - assert.ErrorContains(t, err, "fatal error config file") - _, err = GetDBManager(nil) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "no db manager") - }) - - err := fs.Mkdir(fakeConfigDir, os.ModeDir) - assert.Nil(t, err) - file, err := fs.Create(fakeConfigDir + fakeConfigFile) - assert.Nil(t, err) - _, err = file.WriteString(fakeConfigContent) - assert.Nil(t, err) - _ = file.Close() - - t.Run("new func nil", func(t *testing.T) { - err = InitDBManager(configDir) - - assert.NotNil(t, err) - assert.ErrorContains(t, err, "no db manager for characterType fake-db and workloadType ") - _, err = GetDBManager(nil) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "no db manager") - }) - - fakeNewFunc := func(engines.Properties) (engines.DBManager, error) { - return nil, fmt.Errorf("some error") - } - RegisterEngine(fakeCharacterType, "", fakeNewFunc, nil) - t.Run("new func failed", func(t *testing.T) { - err = InitDBManager(configDir) - - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - _, err = GetDBManager(nil) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "no db manager") - }) - - fakeNewFunc = func(engines.Properties) (engines.DBManager, error) { - return &engines.MockManager{}, nil - } - RegisterEngine(fakeCharacterType, "", fakeNewFunc, func() engines.ClusterCommands { - return nil - }) - RegisterEngine(models.Custom, "", fakeNewFunc, func() engines.ClusterCommands { - return nil - }) - t.Run("new func successfully", func(t *testing.T) { - err = InitDBManager(configDir) - - assert.Nil(t, err) - _, err = GetDBManager(nil) - assert.Nil(t, err) - }) - - SetDBManager(&engines.MockManager{}) - t.Run("db manager exists", func(t *testing.T) { - err = InitDBManager(configDir) - assert.Nil(t, err) - _, err = GetDBManager(nil) - assert.Nil(t, err) - }) - - t.Run("new cluster command", func(t *testing.T) { - _, err = NewClusterCommands("") - assert.NotNil(t, err) - assert.ErrorContains(t, err, "unsupported engine type: ") - _, err = NewClusterCommands(fakeCharacterType) - assert.Nil(t, err) - }) -} diff --git a/pkg/lorry/engines/suite_test.go b/pkg/lorry/engines/suite_test.go deleted file mode 100644 index 8882897375b..00000000000 --- a/pkg/lorry/engines/suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "engines Suite") -} diff --git a/pkg/lorry/engines/util.go b/pkg/lorry/engines/util.go deleted file mode 100644 index 4831fc455e5..00000000000 --- a/pkg/lorry/engines/util.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package engines - -import ( - "fmt" - "strconv" - "strings" -) - -const ( - // types for probe - CheckRunningType int = iota - CheckStatusType - CheckRoleChangedType -) - -func MaxInt64(x, y int64) int64 { - if x > y { - return x - } - return y -} - -func GetIndex(memberName string) (int, error) { - i := strings.LastIndex(memberName, "-") - if i < 0 { - return 0, fmt.Errorf("the format of member name is wrong: %s", memberName) - } - return strconv.Atoi(memberName[i+1:]) -} - -func AddSingleQuote(str string) string { - return "'" + str + "'" -} - -type Properties map[string]string diff --git a/pkg/lorry/engines/wesql/config.go b/pkg/lorry/engines/wesql/config.go deleted file mode 100644 index 70f442de683..00000000000 --- a/pkg/lorry/engines/wesql/config.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -type Config struct { - *mysql.Config -} - -var config *Config - -func NewConfig(properties map[string]string) (*Config, error) { - mysqlConfig, err := mysql.NewConfig(properties) - if err != nil { - return nil, err - } - config = &Config{ - Config: mysqlConfig, - } - return config, nil -} diff --git a/pkg/lorry/engines/wesql/config_test.go b/pkg/lorry/engines/wesql/config_test.go deleted file mode 100644 index 290a2523e21..00000000000 --- a/pkg/lorry/engines/wesql/config_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" -) - -var ( - fakeProperties = engines.Properties{ - "url": "root:@tcp(127.0.0.1:3306)/mysql?multiStatements=true", - "maxOpenConns": "5", - } - fakePropertiesWithWrongURL = engines.Properties{ - "url": "root:@tcp(127.0.0.1:3306)mysql", - } - fakePropertiesWithWrongPem = engines.Properties{ - "pemPath": "fake-path", - } -) - -func TestNewConfig(t *testing.T) { - t.Run("new config failed", func(t *testing.T) { - fakeConfig, err := NewConfig(fakePropertiesWithWrongPem) - - assert.Nil(t, fakeConfig) - assert.NotNil(t, err) - }) - - t.Run("new config successfully", func(t *testing.T) { - fakeConfig, err := NewConfig(fakeProperties) - - assert.NotNil(t, fakeConfig) - assert.Nil(t, err) - }) -} diff --git a/pkg/lorry/engines/wesql/conn.go b/pkg/lorry/engines/wesql/conn.go deleted file mode 100644 index 1bb663a2351..00000000000 --- a/pkg/lorry/engines/wesql/conn.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "database/sql" - "strings" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -// GetDBConnWithMember retrieves a database connection for a specific member of a cluster. -func (mgr *Manager) GetDBConnWithMember(cluster *dcs.Cluster, member *dcs.Member) (db *sql.DB, err error) { - if member != nil && member.Name != mgr.CurrentMemberName { - addr := cluster.GetMemberAddrWithPort(*member) - db, err = config.GetDBConnWithAddr(addr) - if err != nil { - return nil, errors.Wrap(err, "new db connection failed") - } - } else { - db = mgr.DB - } - return db, nil -} - -// GetLeaderConn retrieves a database connection to the leader member of a cluster. -func (mgr *Manager) GetLeaderConn(cluster *dcs.Cluster) (*sql.DB, error) { - leaderMember := cluster.GetLeaderMember() - if leaderMember == nil { - mgr.Logger.Info("Get leader from db cluster local") - leaderMember = mgr.GetLeaderMember(cluster) - } - if leaderMember == nil { - return nil, errors.New("the cluster has no leader") - } - return mgr.GetDBConnWithMember(cluster, leaderMember) -} - -// GetLeaderMember retrieves the leader member of a cluster -func (mgr *Manager) GetLeaderMember(cluster *dcs.Cluster) *dcs.Member { - clusterLocalInfo, err := mgr.GetClusterLocalInfo() - if err != nil || clusterLocalInfo == nil { - mgr.Logger.Error(err, "Get cluster local info failed") - return nil - } - - leaderAddr := clusterLocalInfo.GetString("CURRENT_LEADER") - if leaderAddr == "" { - return nil - } - leaderParts := strings.Split(leaderAddr, ".") - if len(leaderParts) > 0 { - return cluster.GetMemberWithName(leaderParts[0]) - } - - return nil -} diff --git a/pkg/lorry/engines/wesql/conn_test.go b/pkg/lorry/engines/wesql/conn_test.go deleted file mode 100644 index fee1e89e4ae..00000000000 --- a/pkg/lorry/engines/wesql/conn_test.go +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "fmt" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func TestGetDBConnWithMember(t *testing.T) { - manager, _, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - } - - t.Run("new db connection failed", func(t *testing.T) { - _, _ = NewConfig(fakePropertiesWithWrongURL) - db, err := manager.GetDBConnWithMember(cluster, &dcs.Member{}) - - assert.Nil(t, db) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "new db connection failed") - }) - - t.Run("return current member connection", func(t *testing.T) { - db, err := manager.GetDBConnWithMember(cluster, nil) - - assert.NotNil(t, db) - assert.Nil(t, err) - assert.Equal(t, db, manager.DB) - }) -} - -func TestGetLeaderMember(t *testing.T) { - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - { - Name: fakePodName, - }, - }, - } - - t.Run("Get cluster local info failed", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - leaderMember := manager.GetLeaderMember(cluster) - assert.Nil(t, leaderMember) - }) - - t.Run("leader addr is empty", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER"}).AddRow("")) - - leaderMember := manager.GetLeaderMember(cluster) - assert.Nil(t, leaderMember) - }) - - t.Run("get leader member success", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER"}).AddRow(fakePodName + ".test-wesql.headless")) //nolint:goconst - - leaderMember := manager.GetLeaderMember(cluster) - assert.NotNil(t, leaderMember) - assert.Equal(t, fakePodName, leaderMember.Name) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetLeaderConn(t *testing.T) { - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - } - - t.Run("the cluster has no leader", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - db, err := manager.GetLeaderConn(cluster) - assert.Nil(t, db) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "the cluster has no leader") - }) - - t.Run("get leader conn successfully", func(t *testing.T) { - _, _ = NewConfig(fakeProperties) - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER"}).AddRow(fakePodName + ".test-wesql.headless")) - cluster.Members = []dcs.Member{ - { - Name: fakePodName, - }, - } - - db, err := manager.GetLeaderConn(cluster) - assert.NotNil(t, db) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/wesql/get_replica_role.go b/pkg/lorry/engines/wesql/get_replica_role.go deleted file mode 100644 index 76e6084a4b7..00000000000 --- a/pkg/lorry/engines/wesql/get_replica_role.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "context" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -func (mgr *Manager) GetReplicaRole(ctx context.Context, _ *dcs.Cluster) (string, error) { - sql := "select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local" - - rows, err := mgr.DB.QueryContext(ctx, sql) - if err != nil { - mgr.Logger.Info("error executing query", "sql", sql, "error", err.Error()) - return "", errors.Wrapf(err, "error executing %s", sql) - } - - defer func() { - _ = rows.Close() - _ = rows.Err() - }() - - var curLeader string - var role string - var serverID string - var isReady bool - for rows.Next() { - if err = rows.Scan(&curLeader, &role, &serverID); err != nil { - mgr.Logger.Info("Role query failed", "error", err.Error()) - return role, err - } - isReady = true - } - if isReady { - return role, nil - } - return "", errors.Errorf("exec sql %s failed: no data returned", sql) -} - -func (mgr *Manager) GetClusterLocalInfo() (mysql.RowMap, error) { - var result mysql.RowMap - sql := "select * from information_schema.wesql_cluster_local;" - err := mysql.QueryRowsMap(mgr.DB, sql, func(rMap mysql.RowMap) error { - result = rMap - return nil - }) - if err != nil { - mgr.Logger.Info("error executing query", "sql", sql, "error", err.Error()) - return nil, err - } - return result, nil - -} diff --git a/pkg/lorry/engines/wesql/get_replica_role_test.go b/pkg/lorry/engines/wesql/get_replica_role_test.go deleted file mode 100644 index 2a7d0a224b3..00000000000 --- a/pkg/lorry/engines/wesql/get_replica_role_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "context" - "fmt" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" -) - -func TestGetRole(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("error executing sql", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnError(fmt.Errorf("some error")) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "", role) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("scan rows failed", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"}).AddRow("test-wesql-0", "leader")) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "", role) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "sql: expected 2 destination arguments in Scan, not 3") - }) - - t.Run("no data returned", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"})) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "", role) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "no data returned") - }) - - t.Run("get role successfully", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "leader", "1")) - - role, err := manager.GetReplicaRole(ctx, nil) - assert.Equal(t, "leader", role) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetClusterLocalInfo(t *testing.T) { - manager, mock, _ := mockDatabase(t) - - t.Run("error executing sql", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - clusterLocalInfo, err := manager.GetClusterLocalInfo() - assert.Nil(t, clusterLocalInfo) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("get cluster local info successfully", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "leader", "1")) - - clusterLocalInfo, err := manager.GetClusterLocalInfo() - assert.NotNil(t, clusterLocalInfo) - assert.Nil(t, err) - assert.Equal(t, "test-wesql-0", clusterLocalInfo.GetString("CURRENT_LEADER")) - assert.Equal(t, "leader", clusterLocalInfo.GetString("ROLE")) - assert.Equal(t, "1", clusterLocalInfo.GetString("SERVER_ID")) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/engines/wesql/manager.go b/pkg/lorry/engines/wesql/manager.go deleted file mode 100644 index 2b353eefeb2..00000000000 --- a/pkg/lorry/engines/wesql/manager.go +++ /dev/null @@ -1,308 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -const ( - Role = "ROLE" - CurrentLeader = "CURRENT_LEADER" - Leader = "Leader" -) - -type Manager struct { - mysql.Manager -} - -var _ engines.DBManager = &Manager{} - -func NewManager(properties engines.Properties) (engines.DBManager, error) { - logger := ctrl.Log.WithName("WeSQL") - _, err := NewConfig(properties) - if err != nil { - return nil, err - } - - mysqlMgr, err := mysql.NewManager(properties) - if err != nil { - return nil, err - } - - mgr := &Manager{ - Manager: *mysqlMgr.(*mysql.Manager), - } - - mgr.SetLogger(logger) - return mgr, nil -} - -func (mgr *Manager) InitializeCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) IsLeader(ctx context.Context, cluster *dcs.Cluster) (bool, error) { - role, err := mgr.GetReplicaRole(ctx, cluster) - - if err != nil { - return false, err - } - - if strings.EqualFold(role, Leader) { - return true, nil - } - - return false, nil -} - -func (mgr *Manager) IsLeaderMember(_ context.Context, cluster *dcs.Cluster, member *dcs.Member) (bool, error) { - if member == nil { - return false, nil - } - - leaderMember := mgr.GetLeaderMember(cluster) - if leaderMember == nil { - return false, nil - } - - if leaderMember.Name != member.Name { - return false, nil - } - - return true, nil -} - -func (mgr *Manager) InitiateCluster(_ *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) GetMemberAddrs(ctx context.Context, cluster *dcs.Cluster) []string { - addrs := make([]string, 0, 3) - clusterInfo := mgr.GetClusterInfo(ctx, cluster) - clusterInfo = strings.Split(clusterInfo, "@")[0] - for _, addr := range strings.Split(clusterInfo, ";") { - if !strings.Contains(addr, ":") { - continue - } - addrs = append(addrs, strings.Split(addr, "#")[0]) - } - - return addrs -} - -func (mgr *Manager) GetAddrWithMemberName(ctx context.Context, cluster *dcs.Cluster, memberName string) string { - addrs := mgr.GetMemberAddrs(ctx, cluster) - for _, addr := range addrs { - if strings.HasPrefix(addr, memberName) { - return addr - } - } - return "" -} - -func (mgr *Manager) IsCurrentMemberInCluster(ctx context.Context, cluster *dcs.Cluster) bool { - clusterInfo := mgr.GetClusterInfo(ctx, cluster) - return strings.Contains(clusterInfo, mgr.CurrentMemberName) -} - -func (mgr *Manager) IsMemberLagging(context.Context, *dcs.Cluster, *dcs.Member) (bool, int64) { - return false, 0 -} - -func (mgr *Manager) Recover(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) JoinCurrentMemberToCluster(context.Context, *dcs.Cluster) error { - return nil -} - -func (mgr *Manager) LeaveMemberFromCluster(ctx context.Context, cluster *dcs.Cluster, memberName string) error { - db, err := mgr.GetLeaderConn(cluster) - if err != nil { - mgr.Logger.Error(err, "Get leader conn failed") - return err - } - addr := mgr.GetAddrWithMemberName(ctx, cluster, memberName) - if addr == "" { - mgr.Logger.Info(fmt.Sprintf("member %s already deleted", memberName)) - return nil - } - - sql := fmt.Sprintf("call dbms_consensus.downgrade_follower('%s');"+ - "call dbms_consensus.drop_learner('%s');", addr, addr) - _, err = db.ExecContext(ctx, sql) - if err != nil { - mgr.Logger.Error(err, "delete member from db cluster failed") - return errors.Wrapf(err, "error executing %s", sql) - } - return nil -} - -func (mgr *Manager) IsClusterHealthy(_ context.Context, cluster *dcs.Cluster) bool { - db, err := mgr.GetLeaderConn(cluster) - if err != nil { - mgr.Logger.Error(err, "Get leader conn failed") - return false - } - - var leaderRecord mysql.RowMap - sql := "select * from information_schema.wesql_cluster_global;" - err = mysql.QueryRowsMap(db, sql, func(rMap mysql.RowMap) error { - if rMap.GetString(Role) == Leader { - leaderRecord = rMap - } - return nil - }) - if err != nil { - mgr.Logger.Error(err, fmt.Sprintf("error executing %s", sql)) - return false - } - - if len(leaderRecord) > 0 { - return true - } - return false -} - -// IsClusterInitialized is a method to check if cluster is initialized or not -func (mgr *Manager) IsClusterInitialized(ctx context.Context, _ *dcs.Cluster) (bool, error) { - clusterInfo := mgr.GetClusterInfo(ctx, nil) - if clusterInfo != "" { - return true, nil - } - - return false, nil -} - -func (mgr *Manager) GetClusterInfo(ctx context.Context, cluster *dcs.Cluster) string { - var db *sql.DB - var err error - if cluster != nil { - db, err = mgr.GetLeaderConn(cluster) - if err != nil { - mgr.Logger.Error(err, "Get leader conn failed") - return "" - } - } else { - db = mgr.DB - - } - var clusterID, clusterInfo string - err = db.QueryRowContext(ctx, "select cluster_id, cluster_info from mysql.consensus_info"). - Scan(&clusterID, &clusterInfo) - if err != nil { - mgr.Logger.Error(err, "Cluster info query failed") - } - return clusterInfo -} - -func (mgr *Manager) Promote(ctx context.Context, cluster *dcs.Cluster) error { - isLeader, _ := mgr.IsLeader(ctx, nil) - if isLeader { - return nil - } - - db, err := mgr.GetLeaderConn(cluster) - if err != nil { - return errors.Wrap(err, "Get leader conn failed") - } - - addr := mgr.GetAddrWithMemberName(ctx, cluster, mgr.CurrentMemberName) - if addr == "" { - return errors.New("get current member's addr failed") - } - resp, err := db.Exec(fmt.Sprintf("call dbms_consensus.change_leader('%s');", addr)) - if err != nil { - return err - } - - mgr.Logger.Info("promote success", "resp", resp) - return nil -} - -func (mgr *Manager) IsPromoted(ctx context.Context) bool { - isLeader, _ := mgr.IsLeader(ctx, nil) - return isLeader -} - -func (mgr *Manager) Demote(context.Context) error { - return nil -} - -func (mgr *Manager) Follow(_ context.Context, cluster *dcs.Cluster) error { - mgr.Logger.Info("current member still follow the leader", "leader name", cluster.Leader.Name) - return nil -} - -func (mgr *Manager) GetHealthiestMember(*dcs.Cluster, string) *dcs.Member { - return nil -} - -func (mgr *Manager) HasOtherHealthyLeader(_ context.Context, cluster *dcs.Cluster) *dcs.Member { - clusterLocalInfo, err := mgr.GetClusterLocalInfo() - if err != nil || clusterLocalInfo == nil { - mgr.Logger.Error(err, "Get cluster local info failed") - return nil - } - - if clusterLocalInfo.GetString(Role) == Leader { - // I am the leader, just return nil - return nil - } - - leaderAddr := clusterLocalInfo.GetString(CurrentLeader) - if leaderAddr == "" { - return nil - } - leaderParts := strings.Split(leaderAddr, ".") - if len(leaderParts) > 0 { - return cluster.GetMemberWithName(leaderParts[0]) - } - - return nil -} - -// HasOtherHealthyMembers checks if there are any healthy members, excluding the leader -func (mgr *Manager) HasOtherHealthyMembers(ctx context.Context, cluster *dcs.Cluster, leader string) []*dcs.Member { - members := make([]*dcs.Member, 0) - for _, member := range cluster.Members { - if member.Name == leader { - continue - } - if !mgr.IsMemberHealthy(ctx, cluster, &member) { - continue - } - members = append(members, &member) - } - - return members -} diff --git a/pkg/lorry/engines/wesql/manager_test.go b/pkg/lorry/engines/wesql/manager_test.go deleted file mode 100644 index 111f53000da..00000000000 --- a/pkg/lorry/engines/wesql/manager_test.go +++ /dev/null @@ -1,544 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package wesql - -import ( - "context" - "database/sql" - "fmt" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/mysql" -) - -const ( - fakePodName = "test-wesql-0" - fakeClusterCompName = "test-wesql" - fakeNamespace = "fake-namespace" -) - -func mockDatabase(t *testing.T) (*Manager, sqlmock.Sqlmock, error) { - manager := &Manager{ - mysql.Manager{ - DBManagerBase: engines.DBManagerBase{ - CurrentMemberName: fakePodName, - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - Logger: ctrl.Log.WithName("WeSQL-TEST"), - }, - }, - } - - db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) - if err != nil { - t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) - } - manager.DB = db - - return manager, mock, err -} - -func TestNewManager(t *testing.T) { - t.Run("new config failed", func(t *testing.T) { - manager, err := NewManager(fakePropertiesWithWrongPem) - - assert.Nil(t, manager) - assert.NotNil(t, err) - }) - - t.Run("new mysql manager failed", func(t *testing.T) { - manager, err := NewManager(fakeProperties) - - assert.Nil(t, manager) - assert.NotNil(t, err) - }) - - viper.Set(constant.KBEnvPodName, fakePodName) - defer viper.Reset() - t.Run("new manger successfully", func(t *testing.T) { - manager, err := NewManager(fakeProperties) - - assert.Nil(t, err) - assert.NotNil(t, manager) - }) -} - -func TestIsLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("get role failed", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.False(t, isLeader) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("get role leader", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "leader", "1")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.True(t, isLeader) - assert.Nil(t, err) - }) - - t.Run("get role follower", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-1", "follower", "2")) - - isLeader, err := manager.IsLeader(ctx, nil) - assert.False(t, isLeader) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsLeaderMember(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - { - Name: "test-wesql-1", - }, - }, - } - - t.Run("member is nil", func(t *testing.T) { - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, nil) - assert.False(t, isLeaderMember) - assert.Nil(t, err) - }) - - member := &dcs.Member{ - Name: fakePodName, - } - t.Run("leader member is nil", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, member) - assert.False(t, isLeaderMember) - assert.Nil(t, err) - }) - - t.Run("member is not Leader member", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER"}).AddRow("test-wesql-1.test-wesql.headless")) - - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, member) - assert.False(t, isLeaderMember) - assert.Nil(t, err) - }) - - cluster.Members = append(cluster.Members, *member) - t.Run("member is Leader member", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER"}).AddRow(fakePodName + ".test-wesql.headless")) - - isLeaderMember, err := manager.IsLeaderMember(ctx, cluster, member) - assert.True(t, isLeaderMember) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetClusterInfo(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("Get leader conn failed", func(t *testing.T) { - clusterInfo := manager.GetClusterInfo(ctx, &dcs.Cluster{}) - assert.Empty(t, clusterInfo) - }) - - t.Run("get cluster info failed", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnError(fmt.Errorf("some error")) - - clusterInfo := manager.GetClusterInfo(ctx, nil) - assert.Empty(t, clusterInfo) - }) - - t.Run("get cluster info success", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1")) - - clusterInfo := manager.GetClusterInfo(ctx, nil) - assert.Equal(t, "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1", clusterInfo) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetMemberAddrs(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306;test-wesql-2.test-wesql-headless@1")) - - addrs := manager.GetMemberAddrs(ctx, nil) - assert.Equal(t, 2, len(addrs)) - assert.Equal(t, "test-wesql-0.test-wesql-headless:13306", addrs[0]) - assert.Equal(t, "test-wesql-1.test-wesql-headless:13306", addrs[1]) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestGetAddrWithMemberName(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - memberNames := []string{"test-wesql-0", "test-wesql-2"} - expectAddrs := []string{"test-wesql-0.test-wesql-headless:13306", ""} - for i, name := range memberNames { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306;@1")) - - addr := manager.GetAddrWithMemberName(ctx, nil, name) - assert.Equal(t, expectAddrs[i], addr) - } - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsCurrentMemberInCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1")) - - assert.True(t, manager.IsCurrentMemberInCluster(ctx, nil)) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_LeaveMemberFromCluster(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - ClusterCompName: fakeClusterCompName, - Namespace: fakeNamespace, - } - memberName := fakePodName - - t.Run("Get leader conn failed", func(t *testing.T) { - err := manager.LeaveMemberFromCluster(ctx, cluster, memberName) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "the cluster has no leader") - }) - - cluster.Leader = &dcs.Leader{Name: fakePodName} - cluster.Members = []dcs.Member{{Name: fakePodName}} - t.Run("member already deleted", func(t *testing.T) { - err := manager.LeaveMemberFromCluster(ctx, cluster, memberName) - assert.Nil(t, err) - }) - - t.Run("delete member from db cluster failed", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1")) - mock.ExpectExec("call dbms_consensus"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.LeaveMemberFromCluster(ctx, cluster, memberName) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("delete member successfully", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1")) - mock.ExpectExec("call dbms_consensus"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.LeaveMemberFromCluster(ctx, cluster, memberName) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_IsClusterHealthy(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{} - - t.Run("Get leader conn failed", func(t *testing.T) { - isHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.False(t, isHealthy) - }) - - cluster.Leader = &dcs.Leader{Name: fakePodName} - cluster.Members = []dcs.Member{{Name: fakePodName}} - t.Run("get wesql cluster information failed", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - isHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.False(t, isHealthy) - }) - - t.Run("check cluster healthy status successfully", func(t *testing.T) { - roles := []string{Leader, "Follow"} - expectedRes := []bool{true, false} - - for i, role := range roles { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{Role}).AddRow(role)) - - isHealthy := manager.IsClusterHealthy(ctx, cluster) - assert.Equal(t, expectedRes[i], isHealthy) - } - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsClusterInitialized(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - t.Run("cluster is initialized", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}). - AddRow("1", "test-wesql-0.test-wesql-headless:13306;test-wesql-1.test-wesql-headless:13306@1")) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.True(t, isInitialized) - assert.Nil(t, err) - }) - - t.Run("cluster is not initialized", func(t *testing.T) { - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"})) - - isInitialized, err := manager.IsClusterInitialized(ctx, nil) - assert.False(t, isInitialized) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestIsPromoted(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "leader", "1")) - - assert.True(t, manager.IsPromoted(ctx)) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestHasOtherHealthyLeader(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{ - Members: []dcs.Member{}, - } - - t.Run("Get cluster local info failed", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnError(fmt.Errorf("some error")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - t.Run("current member is leader", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"}).AddRow(fakePodName, Leader)) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - t.Run("leader addr is empty", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"}).AddRow("", "follow")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - t.Run("member is not in the cluster", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"}).AddRow(fakePodName, "follow")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.Nil(t, member) - }) - - cluster.Members = append(cluster.Members, dcs.Member{ - Name: fakePodName, - }) - t.Run("get other healthy leader", func(t *testing.T) { - mock.ExpectQuery("select *"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE"}).AddRow(fakePodName+".test-wesql-headless", "follow")) - - member := manager.HasOtherHealthyLeader(ctx, cluster) - assert.NotNil(t, member) - assert.Equal(t, fakePodName, member.Name) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestHasOtherHealthyMembers(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - - cluster := &dcs.Cluster{ - Members: []dcs.Member{ - { - Name: "fake-pod-0", - }, - { - Name: "fake-pod-1", - }, - { - Name: fakePodName, - }, - }, - } - mock.ExpectQuery("select check_ts from kubeblocks.kb_health_check where type=1 limit 1"). - WillReturnError(sql.ErrNoRows) - _, _ = NewConfig(fakeProperties) - - members := manager.HasOtherHealthyMembers(ctx, cluster, "fake-pod-0") - assert.Len(t, members, 1) - assert.Equal(t, fakePodName, members[0].Name) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} - -func TestManager_Promote(t *testing.T) { - ctx := context.TODO() - manager, mock, _ := mockDatabase(t) - cluster := &dcs.Cluster{} - - t.Run("current member is leader", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "leader", "1")) - - err := manager.Promote(ctx, cluster) - assert.Nil(t, err) - }) - - t.Run("Get leader conn failed", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "follower", "1")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "Get leader conn failed") - }) - - cluster.Leader = &dcs.Leader{Name: fakePodName} - cluster.Members = []dcs.Member{{Name: fakePodName}} - t.Run("get addr failed", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "follower", "1")) - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}).AddRow("1", "test-wesql-1.test-wesql-headless:13306;")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "get current member's addr failed") - }) - - t.Run("promote failed", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "follower", "1")) - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}).AddRow("1", "test-wesql-0.test-wesql-headless:13306;")) - mock.ExpectExec("call dbms_consensus"). - WillReturnError(fmt.Errorf("some error")) - - err := manager.Promote(ctx, cluster) - assert.NotNil(t, err) - assert.ErrorContains(t, err, "some error") - }) - - t.Run("promote successfully", func(t *testing.T) { - mock.ExpectQuery("select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local"). - WillReturnRows(sqlmock.NewRows([]string{"CURRENT_LEADER", "ROLE", "SERVER_ID"}).AddRow("test-wesql-0", "follower", "1")) - mock.ExpectQuery("select cluster_id, cluster_info from mysql.consensus_info"). - WillReturnRows(sqlmock.NewRows([]string{"cluster_id", "cluster_info"}).AddRow("1", "test-wesql-0.test-wesql-headless:13306;")) - mock.ExpectExec("call dbms_consensus"). - WillReturnResult(sqlmock.NewResult(1, 1)) - - err := manager.Promote(ctx, cluster) - assert.Nil(t, err) - }) - - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expectations: %v", err) - } -} diff --git a/pkg/lorry/grpcserver/gprc_server.go b/pkg/lorry/grpcserver/gprc_server.go deleted file mode 100644 index 05f213754b0..00000000000 --- a/pkg/lorry/grpcserver/gprc_server.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package grpcserver - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "net" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - health "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/status" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type GRPCServer struct { - logger logr.Logger - checkRoleOperation operations.Operation -} - -var ( - grpcPort int -) - -const ( - DefaultGRPCPort = 50001 -) - -func init() { - flag.IntVar(&grpcPort, "grpcport", DefaultGRPCPort, "lorry grpc default port") -} - -func (s *GRPCServer) Check(ctx context.Context, in *health.HealthCheckRequest) (*health.HealthCheckResponse, error) { - resp, err := s.checkRoleOperation.Do(ctx, nil) - - var status = health.HealthCheckResponse_SERVING - if err != nil { - status = health.HealthCheckResponse_NOT_SERVING - if _, ok := err.(util.ProbeError); !ok { - s.logger.Error(err, "role probe failed") - return &health.HealthCheckResponse{Status: status}, err - } else { - body, _ := json.Marshal(resp.Data) - s.logger.Info("Role changed event detected", "role", string(body)) - return &health.HealthCheckResponse{Status: status}, errors.New(string(body)) - } - } - - s.logger.Info("No event detected", "response", resp) - return &health.HealthCheckResponse{Status: status}, nil -} - -func (s *GRPCServer) Watch(in *health.HealthCheckRequest, _ health.Health_WatchServer) error { - // didn't implement the `watch` function - return status.Error(codes.Unimplemented, "unimplemented") -} - -func (s *GRPCServer) StartNonBlocking() error { - listen, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort)) - if err != nil { - return errors.Wrap(err, "grpc server listen failed") - } - server := grpc.NewServer() - health.RegisterHealthServer(server, s) - - go func() { - err = server.Serve(listen) - if err != nil { - s.logger.Error(err, "grpcserver serve failed") - } - }() - return nil -} - -func NewGRPCServer() (*GRPCServer, error) { - checkRoleOperation, ok := operations.Operations()[strings.ToLower(string(util.CheckRoleOperation))] - if !ok { - return nil, errors.New("check role operation not found") - } - err := checkRoleOperation.Init(context.Background()) - if err != nil { - return nil, err - } - - return &GRPCServer{ - logger: ctrl.Log.WithName("grpc"), - checkRoleOperation: checkRoleOperation, - }, nil -} diff --git a/pkg/lorry/grpcserver/grpc_server_test.go b/pkg/lorry/grpcserver/grpc_server_test.go deleted file mode 100644 index 21a18af787d..00000000000 --- a/pkg/lorry/grpcserver/grpc_server_test.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package grpcserver - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - health "google.golang.org/grpc/health/grpc_health_v1" - - "github.com/apecloud/kubeblocks/pkg/common" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/custom" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/operations/replica" - "github.com/apecloud/kubeblocks/pkg/lorry/util" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var _ = Describe("GRPC Server", func() { - Context("new GRPC server", func() { - It("fail -- no check role operation", func() { - delete(operations.Operations(), strings.ToLower(string(util.CheckRoleOperation))) - _, err := NewGRPCServer() - Expect(err).Should(HaveOccurred()) - }) - - It("success", func() { - err := operations.Register(strings.ToLower(string(util.CheckRoleOperation)), &replica.CheckRole{}) - Expect(err).ShouldNot(HaveOccurred()) - server, err := NewGRPCServer() - Expect(err).ShouldNot(HaveOccurred()) - Expect(server).ShouldNot(BeNil()) - Expect(server.Watch(nil, nil)).ShouldNot(Succeed()) - }) - }) - - Context("check role", func() { - It("role changed", func() { - // set up the host - s := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - _, _ = w.Write([]byte("leader")) - }), - ) - addr := s.Listener.Addr().String() - index := strings.LastIndex(addr, ":") - portStr := addr[index+1:] - // set up the environment - viper.Set("KB_RSM_ACTION_SVC_LIST", "["+portStr+"]") - viper.Set("KB_RSM_ROLE_UPDATE_MECHANISM", "ReadinessProbeEventUpdate") - - customManager, err := custom.NewManager(nil) - Expect(err).Should(BeNil()) - register.SetDBManager(customManager) - - server, _ := NewGRPCServer() - check, err := server.Check(context.Background(), nil) - - Expect(err).Should(HaveOccurred()) - Expect(check.Status).Should(Equal(health.HealthCheckResponse_NOT_SERVING)) - - // set up the expected answer - result := map[string]string{} - body := err.Error() - Expect(json.Unmarshal([]byte(body), &result)).Should(Succeed()) - roleSnapShotStr := result["role"] - roleSnapShot := common.GlobalRoleSnapshot{} - Expect(json.Unmarshal([]byte(roleSnapShotStr), &roleSnapShot)).Should(Succeed()) - Expect(roleSnapShot.PodRoleNamePairs[0].RoleName).Should(Equal("leader")) - }) - }) -}) diff --git a/pkg/lorry/grpcserver/suite_test.go b/pkg/lorry/grpcserver/suite_test.go deleted file mode 100644 index 5dfc060aa6c..00000000000 --- a/pkg/lorry/grpcserver/suite_test.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package grpcserver - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestVolumeOperations(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "GRPC Server. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock dcs store - InitMockDCSStore() -}) - -var _ = AfterSuite(func() { -}) - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore := dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - mockDCSStore.EXPECT().GetMembers().Return([]dcs.Member{{Name: "pod-test", UID: "123"}}, nil).AnyTimes() - dcs.SetStore(mockDCSStore) -} diff --git a/pkg/lorry/highavailability/ha.go b/pkg/lorry/highavailability/ha.go deleted file mode 100644 index e537e6127e2..00000000000 --- a/pkg/lorry/highavailability/ha.go +++ /dev/null @@ -1,372 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package highavailability - -import ( - "context" - "fmt" - "time" - - "github.com/go-logr/logr" - ctrl "sigs.k8s.io/controller-runtime" - - dcs3 "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Ha struct { - ctx context.Context - dbManager engines.DBManager - dcs dcs3.DCS - logger logr.Logger - disableDNSChecker bool -} - -var ha *Ha - -func NewHa(disableDNSChecker bool) *Ha { - logger := ctrl.Log.WithName("HA") - - dcs := dcs3.GetStore() - manager, err := register.GetDBManager(nil) - if err != nil { - logger.Error(err, "No DB Manager") - return nil - } - - ha = &Ha{ - ctx: context.Background(), - dcs: dcs, - logger: logger, - dbManager: manager, - disableDNSChecker: disableDNSChecker, - } - return ha -} - -func GetHa() *Ha { - return ha -} - -func (ha *Ha) RunCycle() { - cluster, err := ha.dcs.GetCluster() - if err != nil { - ha.logger.Error(err, "Get Cluster failed") - return - } - - if !cluster.HaConfig.IsEnable() { - return - } - - currentMember := cluster.GetMemberWithName(ha.dbManager.GetCurrentMemberName()) - - if cluster.HaConfig.IsDeleting(currentMember) { - ha.logger.Info("Current Member is deleted!") - return - } - - if !ha.dbManager.IsRunning() { - ha.logger.Info("DB Service is not running, wait for hypervisor to start it") - if ha.dcs.HasLease() { - _ = ha.dcs.ReleaseLease() - } - _ = ha.dbManager.Start(ha.ctx, cluster) - return - } - - DBState := ha.dbManager.GetDBState(ha.ctx, cluster) - // store leader's db state in dcs - if cluster.Leader != nil && cluster.Leader.Name == ha.dbManager.GetCurrentMemberName() { - cluster.Leader.DBState = DBState - } - - switch { - // IsClusterHealthy is just for consensus cluster healthy check. - // For Replication cluster IsClusterHealthy will always return true, - // and its cluster's healthy is equal to leader member's healthy. - case !ha.dbManager.IsClusterHealthy(ha.ctx, cluster): - ha.logger.Error(nil, "The cluster is not healthy, wait...") - - case !ha.dbManager.IsCurrentMemberInCluster(ha.ctx, cluster) && int(cluster.Replicas) > len(ha.dbManager.GetMemberAddrs(ha.ctx, cluster)): - ha.logger.Info("Current member is not in cluster, add it to cluster") - _ = ha.dbManager.JoinCurrentMemberToCluster(ha.ctx, cluster) - - case !ha.dbManager.IsCurrentMemberHealthy(ha.ctx, cluster): - ha.logger.Info("DB Service is not healthy, do some recover") - if ha.dcs.HasLease() { - _ = ha.dcs.ReleaseLease() - } - err = ha.dbManager.Recover(ha.ctx, cluster) - if err != nil { - ha.logger.Info("recover member failed", "error", err.Error()) - } - - case !cluster.IsLocked(): - ha.logger.Info("Cluster has no leader, attempt to take the leader") - if !ha.IsHealthiestMember(ha.ctx, cluster) { - break - } - - cluster.Leader.DBState = DBState - if ha.dcs.AttemptAcquireLease() != nil { - break - } - - err := ha.dbManager.Promote(ha.ctx, cluster) - if err != nil { - ha.logger.Error(err, "Take the leader failed") - _ = ha.dcs.ReleaseLease() - break - } - cluster.Leader.Name = ha.dbManager.GetCurrentMemberName() - - ha.logger.Info("Take the leader success!") - fallthrough - - case ha.dcs.HasLease(): - ha.logger.Info("This member is Cluster's leader") - if cluster.Switchover != nil { - if cluster.Switchover.Leader == ha.dbManager.GetCurrentMemberName() || - (cluster.Switchover.Candidate != "" && cluster.Switchover.Candidate != ha.dbManager.GetCurrentMemberName()) { - if ha.HasOtherHealthyMember(cluster) { - _ = ha.dbManager.Demote(ha.ctx) - _ = ha.dcs.ReleaseLease() - break - } - ha.logger.Info("The cluster has no other helathy members!") - - } else if cluster.Switchover.Candidate == "" || cluster.Switchover.Candidate == ha.dbManager.GetCurrentMemberName() { - if !ha.dbManager.IsPromoted(ha.ctx) { - // wait and retry - break - } - _ = ha.dcs.DeleteSwitchover() - } - } - - if ha.dbManager.HasOtherHealthyLeader(ha.ctx, cluster) != nil { - // this case is applicable only to consensus cluster, where the db's internal - // role services as the source of truth. - // for replicationSet cluster, HasOtherHealthyLeader will always be false. - ha.logger.Info("Release leader") - _ = ha.dcs.ReleaseLease() - break - } - err = ha.dbManager.Promote(ha.ctx, cluster) - if err != nil { - ha.logger.Error(err, "promote failed") - break - } - - ha.logger.Info("Refresh leader ttl") - _ = ha.dcs.UpdateLease() - - case !ha.dcs.HasLease(): - if cluster.Switchover != nil { - break - } - // TODO: In the event that the database service and SQL channel both go down concurrently, eg. Pod deleted, - // there is no healthy leader node and the lock remains unreleased, attempt to acquire the leader lock. - - // leaderMember := cluster.GetLeaderMember() - // lockOwnerIsLeader, _ := ha.dbManager.IsLeaderMember(ha.ctx, cluster, leaderMember) - // currentMemberIsLeader, _ := ha.dbManager.IsLeader(context.TODO(), cluster) - // if lockOwnerIsLeader && currentMemberIsLeader { - // ha.logger.Info("Lock owner is real Leader, demote myself and follow the real leader") - err = ha.dbManager.Demote(ha.ctx) - if err != nil { - ha.logger.Info("promote failed", "error", err) - } - - err = ha.dbManager.Follow(ha.ctx, cluster) - if err != nil { - ha.logger.Info("follow failed", "error", err) - } - } -} - -func (ha *Ha) Start() { - ha.logger.Info("HA starting") - cluster, err := ha.dcs.GetCluster() - for cluster == nil { - ha.logger.Info("Get Cluster failed.", "cluster-name", ha.dcs.GetClusterName(), "error", err.Error()) - time.Sleep(10 * time.Second) - cluster, err = ha.dcs.GetCluster() - } - - if !ha.disableDNSChecker { - util.WaitForPodReady(false) - } - - // Delete duplicate member deletion records. - removed := cluster.HaConfig.TryToRemoveDeleteRecord(cluster.GetMemberWithName(ha.dbManager.GetCurrentMemberName())) - if removed { - ha.logger.Info("Found previous duplicated delete record, remove it") - _ = ha.dcs.UpdateHaConfig() - } - - ha.logger.Info(fmt.Sprintf("cluster: %v", cluster)) - isInitialized, err := ha.dbManager.IsClusterInitialized(context.TODO(), cluster) - for err != nil || !isInitialized { - ha.logger.Info("Waiting for the database cluster to be initialized.") - // TODO: implement DBManager initialize to replace pod's entrypoint scripts - // if I am the node of index 0, then do initialization - if err == nil && !isInitialized && ha.dbManager.IsFirstMember() { - ha.logger.Info("Initialize cluster.") - err := ha.dbManager.InitializeCluster(ha.ctx, cluster) - if err != nil { - ha.logger.Info("Cluster initialize failed", "error", err.Error()) - // update cluster, in case of the info is out of date - cluster, _ = ha.dcs.GetCluster() - } - } else if err != nil { - ha.logger.Info("Initialize the database cluster Failed.", "error", err.Error()) - } - time.Sleep(5 * time.Second) - isInitialized, err = ha.dbManager.IsClusterInitialized(context.TODO(), cluster) - } - ha.logger.Info("The database cluster is initialized.") - - isRootCreated, err := ha.dbManager.IsRootCreated(ha.ctx) - for err != nil || !isRootCreated { - if err == nil && !isRootCreated && ha.dbManager.IsFirstMember() { - ha.logger.Info("Create Root.") - err := ha.dbManager.CreateRoot(ha.ctx) - if err != nil { - ha.logger.Error(err, "Cluster initialize failed") - } - } - time.Sleep(5 * time.Second) - isRootCreated, err = ha.dbManager.IsRootCreated(ha.ctx) - } - - isExist, _ := ha.dcs.IsLeaseExist() - for !isExist { - if ok, _ := ha.dbManager.IsLeader(context.Background(), cluster); ok { - err := ha.dcs.Initialize() - if err != nil { - ha.logger.Error(err, "DCS initialize failed") - time.Sleep(5 * time.Second) - continue - } - break - } - - ha.logger.Info("Waiting for the database Leader to be ready.") - time.Sleep(5 * time.Second) - isExist, _ = ha.dcs.IsLeaseExist() - } - - for { - startAt := time.Now() - ha.RunCycle() - duration := time.Since(startAt) - if duration < 10*time.Second { - time.Sleep(10*time.Second - duration) - } - } -} - -func (ha *Ha) IsHealthiestMember(ctx context.Context, cluster *dcs3.Cluster) bool { - currentMemberName := ha.dbManager.GetCurrentMemberName() - currentMember := cluster.GetMemberWithName(currentMemberName) - if cluster.Switchover != nil { - switchover := cluster.Switchover - leader := switchover.Leader - candidate := switchover.Candidate - - if leader == currentMemberName { - ha.logger.Info("manual switchover to other member") - return false - } - - if candidate == currentMemberName { - ha.logger.Info("manual switchover to current member", "member", candidate) - isCurrentLagging, _ := ha.dbManager.IsMemberLagging(ctx, cluster, currentMember) - return !isCurrentLagging - } - - if candidate != "" { - ha.logger.Info("manual switchover to new leader", "new leader", candidate) - return false - } - return ha.isMinimumLag(ctx, cluster, currentMember) - } - - if member := ha.dbManager.HasOtherHealthyLeader(ctx, cluster); member != nil { - ha.logger.Info("there is a healthy leader exists", "leader", member.Name) - return false - } - - return ha.isMinimumLag(ctx, cluster, currentMember) -} - -func (ha *Ha) HasOtherHealthyMember(cluster *dcs3.Cluster) bool { - var otherMembers = make([]*dcs3.Member, 0, 1) - if cluster.Switchover != nil && cluster.Switchover.Candidate != "" { - candidate := cluster.Switchover.Candidate - if candidate != ha.dbManager.GetCurrentMemberName() { - otherMembers = append(otherMembers, cluster.GetMemberWithName(candidate)) - } - } else { - for i, member := range cluster.Members { - if member.Name == ha.dbManager.GetCurrentMemberName() { - continue - } - otherMembers = append(otherMembers, &cluster.Members[i]) - } - } - - for _, other := range otherMembers { - if ha.dbManager.IsMemberHealthy(ha.ctx, cluster, other) { - if isLagging, _ := ha.dbManager.IsMemberLagging(ha.ctx, cluster, other); !isLagging { - return true - } - } - } - - return false -} - -func (ha *Ha) isMinimumLag(ctx context.Context, cluster *dcs3.Cluster, member *dcs3.Member) bool { - isCurrentLagging, currentLag := ha.dbManager.IsMemberLagging(ctx, cluster, member) - if isCurrentLagging { - return false - } - - for _, m := range cluster.Members { - if m.Name != member.Name { - isLagging, lag := ha.dbManager.IsMemberLagging(ctx, cluster, &m) - // There are other members with smaller lag - if !isLagging && lag < currentLag { - return false - } - } - } - - return true -} - -func (ha *Ha) ShutdownWithWait() { - ha.dbManager.ShutDownWithWait() -} diff --git a/pkg/lorry/highavailability/util.go b/pkg/lorry/highavailability/util.go deleted file mode 100644 index ed25375bf01..00000000000 --- a/pkg/lorry/highavailability/util.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package highavailability - -import ( - "strings" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" -) - -const ( - WorkloadTypeKey = "workloadType" - Replication = "Replication" - Consensus = "Consensus" -) - -func IsHAAvailable(characterType, workloadType string) bool { - switch models.EngineType(strings.ToLower(characterType)) { - case models.MongoDB: - return true - case models.MySQL: - return false - case models.WeSQL: - return true - case models.PostgreSQL: - if strings.EqualFold(workloadType, Consensus) { - return true - } - case models.ApecloudPostgreSQL: - // apecloud-pg use syncer to support ha - return false - case models.OfficialPostgreSQL: - return true - } - return false -} diff --git a/pkg/lorry/httpserver/apis.go b/pkg/lorry/httpserver/apis.go deleted file mode 100644 index 067da2e39c5..00000000000 --- a/pkg/lorry/httpserver/apis.go +++ /dev/null @@ -1,180 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/pkg/errors" - "github.com/valyala/fasthttp" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -const ( - jsonContentTypeHeader = "application/json" - version = "v1.0" -) - -type option = func(ctx *fasthttp.RequestCtx) - -type OperationAPI interface { - Endpoints() []Endpoint - RegisterOperations(map[string]operations.Operation) -} - -type api struct { - endpoints []Endpoint - ready bool -} - -func (a *api) Endpoints() []Endpoint { - return a.endpoints -} - -func (a *api) RegisterOperations(ops map[string]operations.Operation) { - endpoints := make([]Endpoint, 0, len(ops)) - - for key, op := range ops { - err := op.Init(context.Background()) - if err != nil { - logger.Error(err, "operation init failed", "operation", key) - continue - } - - endpoint := Endpoint{ - Version: version, - } - - if op.IsReadonly(context.Background()) { - endpoint.Method = fasthttp.MethodGet - } else { - endpoint.Method = fasthttp.MethodPost - } - - // opType := reflect.TypeOf(op) - endpoint.Route = key - endpoint.Handler = OperationWrapper(op) - endpoints = append(endpoints, endpoint) - } - a.endpoints = endpoints - a.ready = true -} - -func OperationWrapper(op operations.Operation) fasthttp.RequestHandler { - return func(reqCtx *fasthttp.RequestCtx) { - ctx := context.Background() - body := reqCtx.PostBody() - - var req Request - if len(body) > 0 { - err := json.Unmarshal(body, &req) - if err != nil { - msg := NewErrorResponse("ERR_MALFORMED_REQUEST", fmt.Sprintf("unmarshal HTTP body failed: %v", err)) - respond(reqCtx, withError(fasthttp.StatusBadRequest, msg)) - return - } - } - - b, err := json.Marshal(req.Data) - if err != nil { - msg := NewErrorResponse("ERR_MALFORMED_REQUEST_DATA", fmt.Sprintf("marshal request data field: %v", err)) - respond(reqCtx, withError(fasthttp.StatusInternalServerError, msg)) - logger.Info("marshal request data field", "error", err.Error()) - return - } - opsReq := &operations.OpsRequest{ - Parameters: req.Parameters, - Data: b, - } - - if err := op.PreCheck(ctx, opsReq); err != nil { - msg := NewErrorResponse("ERR_PRECHECK_FAILED", fmt.Sprintf("operation precheck failed: %v", err)) - respond(reqCtx, withError(fasthttp.StatusInternalServerError, msg)) - logger.Info("operation precheck failed", "error", err.Error()) - return - } - - resp, err := op.Do(ctx, opsReq) - - statusCode := fasthttp.StatusOK - if err != nil { - if ok := errors.As(err, &util.ProbeError{}); ok { - statusCode = fasthttp.StatusUnavailableForLegalReasons - } else { - if errors.Is(err, models.ErrNotImplemented) { - statusCode = fasthttp.StatusNotImplemented - } else { - statusCode = fasthttp.StatusInternalServerError - logger.Info("operation exec failed", "error", err.Error()) - } - msg := NewErrorResponse("ERR_OPERATION_FAILED", fmt.Sprintf("operation exec failed: %v", err)) - respond(reqCtx, withError(statusCode, msg)) - return - } - } - - if resp == nil { - respond(reqCtx, withEmpty()) - } else { - body, _ = json.Marshal(resp.Data) - respond(reqCtx, withMetadata(resp.Metadata), withJSON(statusCode, body)) - } - } -} - -// withJSON overrides the content-type with application/json. -func withJSON(code int, obj []byte) option { - return func(ctx *fasthttp.RequestCtx) { - ctx.Response.SetStatusCode(code) - ctx.Response.SetBody(obj) - ctx.Response.Header.SetContentType(jsonContentTypeHeader) - } -} - -// withError sets error code and jsonify error message. -func withError(code int, resp ErrorResponse) option { - b, _ := json.Marshal(&resp) - return withJSON(code, b) -} - -func withEmpty() option { - return func(ctx *fasthttp.RequestCtx) { - ctx.Response.SetBody(nil) - ctx.Response.SetStatusCode(fasthttp.StatusNoContent) - } -} - -func withMetadata(metadata map[string]string) option { - return func(ctx *fasthttp.RequestCtx) { - for k, v := range metadata { - ctx.Response.Header.Set("KB."+k, v) - } - } -} -func respond(ctx *fasthttp.RequestCtx, options ...option) { - for _, option := range options { - option(ctx) - } -} diff --git a/pkg/lorry/httpserver/apis_test.go b/pkg/lorry/httpserver/apis_test.go deleted file mode 100644 index 4646ccf60a8..00000000000 --- a/pkg/lorry/httpserver/apis_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/operations" -) - -func TestRegisterOperations(t *testing.T) { - fakeAPI := &api{} - fakeOps := map[string]operations.Operation{ - "fake-1": operations.NewFakeOperations(operations.FakeInit, func(ctx context.Context) error { - return fmt.Errorf("some error") - }), - "fake-2": operations.NewFakeOperations(operations.FakeIsReadOnly, func(ctx context.Context) bool { - return true - }), - "fake-3": operations.NewFakeOperations(operations.FakeDefault, nil), - } - - fakeAPI.RegisterOperations(fakeOps) - assert.True(t, fakeAPI.ready) - assert.Equal(t, 2, len(fakeAPI.endpoints)) - assert.Equal(t, "v1.0", fakeAPI.endpoints[0].Version) -} diff --git a/pkg/lorry/httpserver/config.go b/pkg/lorry/httpserver/config.go deleted file mode 100644 index a63e914f917..00000000000 --- a/pkg/lorry/httpserver/config.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "github.com/spf13/pflag" - ctrl "sigs.k8s.io/controller-runtime" -) - -type Config struct { - Port int - Address string - MaxRequestBodySize int - UnixDomainSocket string - ReadBufferSize int - APILogging bool -} - -var config Config -var logger = ctrl.Log.WithName("HTTPServer") - -func init() { - pflag.IntVar(&config.Port, "port", 3501, "The HTTP Server listen port for Lorry service.") - pflag.StringVar(&config.Address, "address", "0.0.0.0", "The HTTP Server listen address for Lorry service.") - pflag.BoolVar(&config.APILogging, "api-logging", true, "Enable api logging for Lorry request.") -} diff --git a/pkg/lorry/httpserver/endpoint.go b/pkg/lorry/httpserver/endpoint.go deleted file mode 100644 index 70d059cfaa8..00000000000 --- a/pkg/lorry/httpserver/endpoint.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "github.com/valyala/fasthttp" -) - -type Endpoint struct { - Method string - Route string - Version string - Duplicate string - Handler fasthttp.RequestHandler -} - -type Request struct { - Data interface{} `json:"data,omitempty"` - Parameters map[string]any `json:"parameters,omitempty"` -} diff --git a/pkg/lorry/httpserver/errors.go b/pkg/lorry/httpserver/errors.go deleted file mode 100644 index 1cbf2edf057..00000000000 --- a/pkg/lorry/httpserver/errors.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -// ErrorResponse is an HTTP response message sent back to calling clients. -type ErrorResponse struct { - ErrorCode string `json:"errorCode"` - Message string `json:"message"` -} - -// NewErrorResponse returns a new ErrorResponse. -func NewErrorResponse(errorCode, message string) ErrorResponse { - return ErrorResponse{ - ErrorCode: errorCode, - Message: message, - } -} diff --git a/pkg/lorry/httpserver/server.go b/pkg/lorry/httpserver/server.go deleted file mode 100644 index d6e66f8b39d..00000000000 --- a/pkg/lorry/httpserver/server.go +++ /dev/null @@ -1,168 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "errors" - "fmt" - "io" - "net" - "time" - - fasthttprouter "github.com/fasthttp/router" - "github.com/valyala/fasthttp" - - "github.com/apecloud/kubeblocks/pkg/lorry/operations" -) - -// Server is an interface for the Lorry HTTP server. -type Server interface { - io.Closer - Router() fasthttp.RequestHandler - StartNonBlocking() error -} - -type server struct { - config Config - api OperationAPI - servers []*fasthttp.Server -} - -// NewServer returns a new HTTP server. -func NewServer(ops map[string]operations.Operation) Server { - a := &api{} - a.RegisterOperations(ops) - return &server{ - api: a, - config: config, - } -} - -// StartNonBlocking starts a new server in a goroutine. -func (s *server) StartNonBlocking() error { - logger.Info("Starting HTTP Server") - handler := s.Router() - - APILogging := s.config.APILogging - if APILogging { - handler = s.apiLogger(handler) - } - - var listeners []net.Listener - if s.config.UnixDomainSocket != "" { - socket := fmt.Sprintf("%s/lorry.socket", s.config.UnixDomainSocket) - l, err := net.Listen("unix", socket) - if err != nil { - return err - } - listeners = append(listeners, l) - } else { - apiListenAddress := s.config.Address - l, err := net.Listen("tcp", fmt.Sprintf("%s:%v", apiListenAddress, s.config.Port)) - if err != nil { - logger.Error(err, "listen address", apiListenAddress, "port", s.config.Port) - } else { - listeners = append(listeners, l) - } - } - - if len(listeners) == 0 { - return errors.New("could not listen on any endpoint") - } - - for _, listener := range listeners { - // customServer is created in a loop because each instance - // has a handle on the underlying listener. - customServer := &fasthttp.Server{ - Handler: handler, - MaxRequestBodySize: s.config.MaxRequestBodySize * 1024 * 1024, - ReadBufferSize: s.config.ReadBufferSize * 1024, - } - s.servers = append(s.servers, customServer) - - go func(l net.Listener) { - if err := customServer.Serve(l); err != nil { - panic(err) - } - }(listener) - } - - return nil -} - -func (s *server) apiLogger(next fasthttp.RequestHandler) fasthttp.RequestHandler { - return func(ctx *fasthttp.RequestCtx) { - reqLogger := logger - if userAgent := string(ctx.Request.Header.Peek("User-Agent")); userAgent != "" { - reqLogger = logger.WithValues("useragent", userAgent) - } - start := time.Now() - path := string(ctx.Path()) - if path == "/v1.0/checkrole" { - // do not log for checkrole - next(ctx) - return - } - - reqLogger.Info("HTTP API Called", "method", string(ctx.Method()), "path", path) - next(ctx) - elapsed := float64(time.Since(start) / time.Millisecond) - reqLogger.Info("HTTP API Called", "status code", ctx.Response.StatusCode(), "cost", elapsed) - } -} - -func (s *server) Router() fasthttp.RequestHandler { - endpoints := s.api.Endpoints() - router := s.getRouter(endpoints) - - return router.Handler -} - -func (s *server) getRouter(endpoints []Endpoint) *fasthttprouter.Router { - router := fasthttprouter.New() - for _, e := range endpoints { - path := fmt.Sprintf("/%s/%s", e.Version, e.Route) - router.Handle(e.Method, path, e.Handler) - - if e.Duplicate != "" { - path := fmt.Sprintf("/%s/%s", e.Version, e.Duplicate) - router.Handle(e.Method, path, e.Handler) - } - } - for method, path := range router.List() { - logger.Info("API route path", "method", method, "path", path) - } - - return router -} - -func (s *server) Close() error { - errs := make([]error, len(s.servers)) - - for i, ln := range s.servers { - // This calls `Close()` on the underlying listener. - if err := ln.Shutdown(); err != nil { - logger.Error(err, "server close failed") - errs[i] = err - } - } - - return errors.Join() -} diff --git a/pkg/lorry/httpserver/server_test.go b/pkg/lorry/httpserver/server_test.go deleted file mode 100644 index 1718905d12c..00000000000 --- a/pkg/lorry/httpserver/server_test.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package httpserver - -import ( - "context" - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/valyala/fasthttp" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -func mockServer(t *testing.T) *server { - fakeOps := map[string]operations.Operation{ - "fake-1": operations.NewFakeOperations(operations.FakeDefault, nil), - "fake-2": operations.NewFakeOperations(operations.FakePreCheck, func(ctx context.Context, request *operations.OpsRequest) error { - return fmt.Errorf("fake pre check error") - }), - "fake-3": operations.NewFakeOperations(operations.FakeDo, func(ctx context.Context, request *operations.OpsRequest) (*operations.OpsResponse, error) { - return nil, models.ErrNotImplemented - }), - "fake-4": operations.NewFakeOperations(operations.FakeDo, func(ctx context.Context, request *operations.OpsRequest) (*operations.OpsResponse, error) { - return nil, util.NewProbeError("fake probe error") - }), - "fake-5": operations.NewFakeOperations(operations.FakeDo, func(ctx context.Context, request *operations.OpsRequest) (*operations.OpsResponse, error) { - return nil, fmt.Errorf("fake do error") - }), - "fake-6": operations.NewFakeOperations(operations.FakeDo, func(ctx context.Context, request *operations.OpsRequest) (*operations.OpsResponse, error) { - return &operations.OpsResponse{ - Data: map[string]any{ - "data": request.Data, - }, - Metadata: map[string]string{ - "fake-meta": "fake", - }, - }, nil - }), - } - - s := NewServer(fakeOps) - assert.NotNil(t, s) - fakeServer, ok := s.(*server) - assert.True(t, ok) - - return fakeServer -} - -func mockHTTPRequest(url string, method string, body string) *fasthttp.RequestCtx { - ctx := new(fasthttp.RequestCtx) - ctx.Request.SetRequestURI(url) - ctx.Request.Header.SetMethod(method) - ctx.Request.SetBodyString(body) - ctx.Request.Header.Set("User-Agent", "fake-agent") - - return ctx -} - -func parseErrorResponse(t *testing.T, rawErrorResponse []byte) *ErrorResponse { - resp := &ErrorResponse{} - err := json.Unmarshal(rawErrorResponse, resp) - assert.Nil(t, err) - - return resp -} - -func TestRouter(t *testing.T) { - fakeServer := mockServer(t) - - handler := fakeServer.Router() - assert.NotNil(t, handler) - fakeRouterHandler := fakeServer.apiLogger(handler) - - t.Run("unmarshal HTTP body failed", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-1", fasthttp.MethodPost, `test`) - fakeRouterHandler(ctx) - - response := parseErrorResponse(t, ctx.Response.Body()) - assert.Equal(t, fasthttp.StatusBadRequest, ctx.Response.StatusCode()) - assert.Equal(t, "ERR_MALFORMED_REQUEST", response.ErrorCode) - assert.Equal(t, "unmarshal HTTP body failed: invalid character 'e' in literal true (expecting 'r')", response.Message) - }) - - t.Run("pre check failed", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-2", fasthttp.MethodPost, `{"data": "test"}`) - fakeRouterHandler(ctx) - - response := parseErrorResponse(t, ctx.Response.Body()) - assert.Equal(t, fasthttp.StatusInternalServerError, ctx.Response.StatusCode()) - assert.Equal(t, "ERR_PRECHECK_FAILED", response.ErrorCode) - assert.Equal(t, "operation precheck failed: fake pre check error", response.Message) - }) - - t.Run("do check not implemented", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-3", fasthttp.MethodPost, `{"data": "test"}`) - fakeRouterHandler(ctx) - - response := parseErrorResponse(t, ctx.Response.Body()) - assert.Equal(t, fasthttp.StatusNotImplemented, ctx.Response.StatusCode()) - assert.Equal(t, "ERR_OPERATION_FAILED", response.ErrorCode) - assert.Equal(t, "operation exec failed: not implemented", response.Message) - }) - - t.Run("do check probe error", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-4", fasthttp.MethodPost, `{"data": "test"}`) - fakeRouterHandler(ctx) - - assert.Equal(t, fasthttp.StatusNoContent, ctx.Response.StatusCode()) - assert.Empty(t, ctx.Response.Body()) - }) - - t.Run("do check failed", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-5", fasthttp.MethodPost, `{"data": "test"}`) - fakeRouterHandler(ctx) - - response := parseErrorResponse(t, ctx.Response.Body()) - assert.Equal(t, fasthttp.StatusInternalServerError, ctx.Response.StatusCode()) - assert.Equal(t, "ERR_OPERATION_FAILED", response.ErrorCode) - assert.Equal(t, "operation exec failed: fake do error", response.Message) - }) - - t.Run("return meta data", func(t *testing.T) { - ctx := mockHTTPRequest("/v1.0/fake-6", fasthttp.MethodPost, `{"data": "test"}`) - fakeRouterHandler(ctx) - - assert.Equal(t, fasthttp.StatusOK, ctx.Response.StatusCode()) - assert.Equal(t, string(ctx.Response.Body()), `{"data":"InRlc3Qi"}`) - assert.Equal(t, []byte("fake"), ctx.Response.Header.Peek("KB.fake-meta")) - }) -} - -func TestStartNonBlocking(t *testing.T) { - fakeServer := mockServer(t) - - err := fakeServer.StartNonBlocking() - assert.Nil(t, err) - err = fakeServer.Close() - assert.Nil(t, err) -} diff --git a/pkg/lorry/operations/component/post_provision.go b/pkg/lorry/operations/component/post_provision.go deleted file mode 100644 index 25b99125f71..00000000000 --- a/pkg/lorry/operations/component/post_provision.go +++ /dev/null @@ -1,97 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package component - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type PostProvision struct { - operations.Base - logger logr.Logger - Timeout time.Duration - Command []string -} - -type PostProvisionManager interface { - PostProvision(ctx context.Context, componentNames, podNames, podIPs, podHostNames, podHostIPs string) error -} - -var postProvision operations.Operation = &PostProvision{} - -func init() { - err := operations.Register(strings.ToLower(string(util.PostProvisionOperation)), postProvision) - if err != nil { - panic(err.Error()) - } -} - -func (s *PostProvision) Init(_ context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - postProvisionCmd, ok := actionCommands[constant.PostProvisionAction] - if ok && len(postProvisionCmd) > 0 { - s.Command = postProvisionCmd - } - } - return nil -} - -func (s *PostProvision) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - return nil -} - -func (s *PostProvision) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - componentNames := req.GetString("componentNames") - podNames := req.GetString("podNames") - podIPs := req.GetString("podIPs") - podHostNames := req.GetString("podHostNames") - podHostIPs := req.GetString("podHostIPs") - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "get manager failed") - } - - ppManager, ok := manager.(PostProvisionManager) - if !ok { - return nil, models.ErrNotImplemented - } - err = ppManager.PostProvision(ctx, componentNames, podNames, podIPs, podHostNames, podHostIPs) - return nil, err -} diff --git a/pkg/lorry/operations/component/pre_terminate.go b/pkg/lorry/operations/component/pre_terminate.go deleted file mode 100644 index 11d6256d932..00000000000 --- a/pkg/lorry/operations/component/pre_terminate.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package component - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type PreTerminate struct { - operations.Base - logger logr.Logger - Timeout time.Duration - Command []string -} - -type PreTerminateManager interface { - PreTerminate(ctx context.Context) error -} - -var preTerminate operations.Operation = &PreTerminate{} - -func init() { - err := operations.Register(strings.ToLower(string(util.PreTerminateOperation)), preTerminate) - if err != nil { - panic(err.Error()) - } -} - -func (s *PreTerminate) Init(_ context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - preTermianteCmd, ok := actionCommands[constant.PreTerminateAction] - if ok && len(preTermianteCmd) > 0 { - s.Command = preTermianteCmd - } - } - return nil -} - -func (s *PreTerminate) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - return nil -} - -func (s *PreTerminate) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "get manager failed") - } - - ptManager, ok := manager.(PreTerminateManager) - if !ok { - return nil, models.ErrNotImplemented - } - err = ptManager.PreTerminate(ctx) - return nil, err -} diff --git a/pkg/lorry/operations/fake.go b/pkg/lorry/operations/fake.go deleted file mode 100644 index 37117470612..00000000000 --- a/pkg/lorry/operations/fake.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package operations - -import ( - "context" - "time" -) - -type FakeFuncType string - -const ( - FakeInit FakeFuncType = "fake-init" - FakeIsReadOnly FakeFuncType = "fake-is-read-only" - FakePreCheck FakeFuncType = "fake-pre-check" - FakeDo FakeFuncType = "fake-do" - FakeDefault FakeFuncType = "fake-default" -) - -type FakeOperations struct { - InitFunc func(ctx context.Context) error - IsReadOnlyFunc func(ctx context.Context) bool - PreCheckFunc func(ctx context.Context, request *OpsRequest) error - DoFunc func(ctx context.Context, request *OpsRequest) (*OpsResponse, error) -} - -func NewFakeOperations(funcType FakeFuncType, fakeFunc interface{}) *FakeOperations { - op := &FakeOperations{ - InitFunc: func(ctx context.Context) error { - return nil - }, - IsReadOnlyFunc: func(ctx context.Context) bool { - return false - }, - PreCheckFunc: func(ctx context.Context, request *OpsRequest) error { - return nil - }, - DoFunc: func(ctx context.Context, request *OpsRequest) (*OpsResponse, error) { - return nil, nil - }, - } - - switch funcType { - case FakeInit: - op.InitFunc = fakeFunc.(func(ctx context.Context) error) - case FakeIsReadOnly: - op.IsReadOnlyFunc = fakeFunc.(func(ctx context.Context) bool) - case FakePreCheck: - op.PreCheckFunc = fakeFunc.(func(ctx context.Context, request *OpsRequest) error) - case FakeDo: - op.DoFunc = fakeFunc.(func(ctx context.Context, request *OpsRequest) (*OpsResponse, error)) - } - return op -} - -func (f *FakeOperations) Init(ctx context.Context) error { - return f.InitFunc(ctx) -} - -func (f *FakeOperations) SetTimeout(timeout time.Duration) { -} - -func (f *FakeOperations) IsReadonly(ctx context.Context) bool { - return f.IsReadOnlyFunc(ctx) -} - -func (f *FakeOperations) PreCheck(ctx context.Context, request *OpsRequest) error { - return f.PreCheckFunc(ctx, request) -} - -func (f *FakeOperations) Do(ctx context.Context, request *OpsRequest) (*OpsResponse, error) { - return f.DoFunc(ctx, request) -} diff --git a/pkg/lorry/operations/interface.go b/pkg/lorry/operations/interface.go deleted file mode 100644 index fbd1890c5fb..00000000000 --- a/pkg/lorry/operations/interface.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package operations - -import ( - "context" - "time" - - "github.com/pkg/errors" -) - -type Operation interface { - Init(context.Context) error - SetTimeout(timeout time.Duration) - IsReadonly(context.Context) bool - PreCheck(context.Context, *OpsRequest) error - Do(context.Context, *OpsRequest) (*OpsResponse, error) -} - -type Base struct { - Timeout time.Duration - Command []string -} - -func (b *Base) Init(ctx context.Context) error { - return nil -} - -func (b *Base) SetTimeout(timeout time.Duration) { - b.Timeout = timeout -} - -func (b *Base) IsReadonly(ctx context.Context) bool { - return false -} - -func (b *Base) PreCheck(ctx context.Context, request *OpsRequest) error { - return nil -} - -func (b *Base) Do(ctx context.Context, request *OpsRequest) (*OpsResponse, error) { - return nil, errors.New("not implemented") -} diff --git a/pkg/lorry/operations/operations.go b/pkg/lorry/operations/operations.go deleted file mode 100644 index faa7e4853b1..00000000000 --- a/pkg/lorry/operations/operations.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package operations - -import ( - "sync" - - "github.com/pkg/errors" -) - -type Ops struct { - ops map[string]Operation - lock sync.Mutex -} - -var ops Ops - -func (ops *Ops) Register(name string, op Operation) error { - if _, ok := ops.ops[name]; ok { - return errors.New("Operation already registered: " + name) - } - - ops.lock.Lock() - defer ops.lock.Unlock() - if ops.ops == nil { - ops.ops = make(map[string]Operation) - } - - ops.ops[name] = op - return nil -} - -func (ops *Ops) Operations() map[string]Operation { - return ops.ops -} - -func Register(name string, op Operation) error { - return ops.Register(name, op) -} - -func Operations() map[string]Operation { - return ops.Operations() -} diff --git a/pkg/lorry/operations/register/register.go b/pkg/lorry/operations/register/register.go deleted file mode 100644 index df770ff871a..00000000000 --- a/pkg/lorry/operations/register/register.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package register - -import ( - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - _ "github.com/apecloud/kubeblocks/pkg/lorry/operations/component" - _ "github.com/apecloud/kubeblocks/pkg/lorry/operations/replica" - _ "github.com/apecloud/kubeblocks/pkg/lorry/operations/sql" - _ "github.com/apecloud/kubeblocks/pkg/lorry/operations/user" - _ "github.com/apecloud/kubeblocks/pkg/lorry/operations/volume" -) - -func Register(name string, op operations.Operation) error { - return operations.Register(name, op) -} - -func Operations() map[string]operations.Operation { - return operations.Operations() -} diff --git a/pkg/lorry/operations/replica/checkrole.go b/pkg/lorry/operations/replica/checkrole.go deleted file mode 100644 index f955dcd028a..00000000000 --- a/pkg/lorry/operations/replica/checkrole.go +++ /dev/null @@ -1,259 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - "time" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/common" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -// AccessMode defines SVC access mode enums. -// +enum -type AccessMode string - -type CheckRole struct { - operations.Base - logger logr.Logger - dcsStore dcs.DCS - OriRole string - CheckRoleFailedCount int - FailedEventReportFrequency int - Timeout time.Duration - DBRoles map[string]AccessMode - Command []string -} - -var checkrole operations.Operation = &CheckRole{} - -func init() { - err := operations.Register(strings.ToLower(string(util.CheckRoleOperation)), checkrole) - if err != nil { - panic(err.Error()) - } -} - -func (s *CheckRole) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - s.logger = ctrl.Log.WithName("checkrole") - val := viper.GetString(constant.KBEnvServiceRoles) - if val != "" { - if err := json.Unmarshal([]byte(val), &s.DBRoles); err != nil { - s.logger.Info("KB_DB_ROLES env format error", "error", err) - } - } - - s.FailedEventReportFrequency = viper.GetInt("KB_FAILED_EVENT_REPORT_FREQUENCY") - if s.FailedEventReportFrequency < 300 { - s.FailedEventReportFrequency = 300 - } else if s.FailedEventReportFrequency > 3600 { - s.FailedEventReportFrequency = 3600 - } - - timeoutSeconds := util.DefaultProbeTimeoutSeconds - if viper.IsSet(constant.KBEnvRoleProbeTimeout) { - timeoutSeconds = viper.GetInt(constant.KBEnvRoleProbeTimeout) - } - // lorry utilizes the pod readiness probe to trigger role probe and 'timeoutSeconds' is directly copied from the 'probe.timeoutSeconds' field of pod. - // here we give 80% of the total time to role probe job and leave the remaining 20% to kubelet to handle the readiness probe related tasks. - s.Timeout = time.Duration(timeoutSeconds) * (800 * time.Millisecond) - s.OriRole = "waitForStart" - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - roleProbeCmd, ok := actionCommands[constant.RoleProbeAction] - if ok && len(roleProbeCmd) > 0 { - s.Command = roleProbeCmd - } - } - return nil -} - -func (s *CheckRole) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *CheckRole) Do(ctx context.Context, _ *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = util.CheckRoleOperation - resp.Data["originalRole"] = s.OriRole - var role string - var err error - - manager, err1 := register.GetDBManager(s.Command) - if err1 != nil { - return nil, errors.Wrap(err1, "get manager failed") - } - - if !manager.IsDBStartupReady() { - resp.Data["message"] = "db not ready" - return resp, nil - } - - cluster := s.dcsStore.GetClusterFromCache() - - ctx1, cancel := context.WithTimeout(ctx, s.Timeout) - defer cancel() - role, err = manager.GetReplicaRole(ctx1, cluster) - - if err != nil { - s.logger.Info("executing checkRole error", "error", err.Error()) - // do not return err, as it will cause readinessprobe to fail - err = nil - if s.CheckRoleFailedCount%s.FailedEventReportFrequency == 0 { - s.logger.Info("role checks failed continuously", "times", s.CheckRoleFailedCount) - // if err is not nil, send event through kubelet readinessprobe - err = util.SentEventForProbe(ctx, resp.Data) - } - s.CheckRoleFailedCount++ - return resp, err - } - - s.CheckRoleFailedCount = 0 - if isValid, message := s.roleValidate(role); !isValid { - resp.Data["message"] = message - return resp, nil - } - - if s.OriRole == role { - return nil, nil - } - - // When network partition occurs, the new primary needs to send global role change information to the controller. - isLeader, err := manager.IsLeader(ctx, cluster) - if err != nil { - if err != models.ErrNotImplemented { - return nil, err - } - isLeader = models.IsLikelyPrimaryRole(role) - } - - if isLeader { - // we need to get latest member info to build global role snapshot - members, err := s.dcsStore.GetMembers() - if err != nil { - return nil, err - } - cluster.Members = members - resp.Data["role"] = s.buildGlobalRoleSnapshot(cluster, manager, role) - } else { - resp.Data["role"] = role - } - - resp.Data["event"] = util.OperationSuccess - s.OriRole = role - err = util.SentEventForProbe(ctx, resp.Data) - return resp, err -} - -// Component may have some internal roles that needn't be exposed to end user, -// and not configured in cluster definition, e.g. ETCD's Candidate. -// roleValidate is used to filter the internal roles and decrease the number -// of report events to reduce the possibility of event conflicts. -func (s *CheckRole) roleValidate(role string) (bool, string) { - if role == "" { - // some time db replica may not have role, e.g. oceanbase - return true, "" - } - // do not validate them when db roles setting is missing - if len(s.DBRoles) == 0 { - return true, "" - } - - var msg string - isValid := false - for r := range s.DBRoles { - if strings.EqualFold(r, role) { - isValid = true - break - } - } - if !isValid { - msg = fmt.Sprintf("role %s is not configured in cluster definition %v", role, s.DBRoles) - } - return isValid, msg -} - -func (s *CheckRole) buildGlobalRoleSnapshot(cluster *dcs.Cluster, mgr engines.DBManager, role string) string { - currentMemberName := mgr.GetCurrentMemberName() - roleSnapshot := &common.GlobalRoleSnapshot{ - Version: strconv.FormatInt(metav1.NowMicro().UnixMicro(), 10), - PodRoleNamePairs: []common.PodRoleNamePair{ - { - PodName: currentMemberName, - RoleName: role, - PodUID: cluster.GetMemberWithName(currentMemberName).UID, - }, - }, - } - - for _, member := range cluster.Members { - s.logger.V(1).Info("check member", "member", member.Name, "role", member.Role) - if member.Name != currentMemberName { - // get old primary and set it's role to none - if strings.EqualFold(member.Role, role) { - s.logger.Info("there is a another leader", "member", member.Name) - if member.IsLorryReady() { - s.logger.Info("another leader's lorry is online, just ignore", "member", member.Name) - continue - } - s.logger.Info("reset old leader role to none", "member", member.Name) - roleSnapshot.PodRoleNamePairs = append(roleSnapshot.PodRoleNamePairs, common.PodRoleNamePair{ - PodName: member.Name, - RoleName: "", - PodUID: cluster.GetMemberWithName(member.Name).UID, - }) - } - } - } - - b, _ := json.Marshal(roleSnapshot) - return string(b) -} diff --git a/pkg/lorry/operations/replica/checkrunning.go b/pkg/lorry/operations/replica/checkrunning.go deleted file mode 100644 index eb530b71fab..00000000000 --- a/pkg/lorry/operations/replica/checkrunning.go +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "fmt" - "net" - "strconv" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -// CheckRunning checks whether the binding service is in running status, -// If check fails continuously, report an event at FailedEventReportFrequency frequency -type CheckRunning struct { - operations.Base - logger logr.Logger - Timeout time.Duration - DBAddress string - CheckRunningFailedCount int - FailedEventReportFrequency int -} - -var checkrunning operations.Operation = &CheckRunning{} - -func init() { - err := operations.Register("checkrunning", checkrunning) - if err != nil { - panic(err.Error()) - } -} - -func (s *CheckRunning) Init(ctx context.Context) error { - s.FailedEventReportFrequency = viper.GetInt("KB_FAILED_EVENT_REPORT_FREQUENCY") - if s.FailedEventReportFrequency < 300 { - s.FailedEventReportFrequency = 300 - } else if s.FailedEventReportFrequency > 3600 { - s.FailedEventReportFrequency = 3600 - } - - timeoutSeconds := util.DefaultProbeTimeoutSeconds - if viper.IsSet(constant.KBEnvRoleProbeTimeout) { - timeoutSeconds = viper.GetInt(constant.KBEnvRoleProbeTimeout) - } - // lorry utilizes the pod readiness probe to trigger probe and 'timeoutSeconds' is directly copied from the 'probe.timeoutSeconds' field of pod. - // here we give 80% of the total time to probe job and leave the remaining 20% to kubelet to handle the readiness probe related tasks. - s.Timeout = time.Duration(timeoutSeconds) * (800 * time.Millisecond) - s.DBAddress = s.getAddress() - s.logger = ctrl.Log.WithName("checkrunning") - return nil -} - -func (s *CheckRunning) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(nil) - if err != nil { - return nil, errors.Wrap(err, "get manager failed") - } - - var message string - opsRsp := &operations.OpsResponse{} - opsRsp.Data["operation"] = util.CheckRunningOperation - - dbPort, err := manager.GetPort() - if err != nil { - return nil, errors.Wrap(err, "get db port failed") - } - - host := net.JoinHostPort(s.DBAddress, strconv.Itoa(dbPort)) - // sql exec timeout needs to be less than httpget's timeout which by default 1s. - conn, err := net.DialTimeout("tcp", host, 500*time.Millisecond) - if err != nil { - message = fmt.Sprintf("running check %s error", host) - s.logger.Error(err, message) - opsRsp.Data["event"] = util.OperationFailed - opsRsp.Data["message"] = message - if s.CheckRunningFailedCount%s.FailedEventReportFrequency == 0 { - s.logger.Info("running checks failed continuously", "times", s.CheckRunningFailedCount) - // resp.Metadata[StatusCode] = OperationFailedHTTPCode - err = util.SentEventForProbe(ctx, opsRsp.Data) - } - s.CheckRunningFailedCount++ - return opsRsp, err - } - defer conn.Close() - s.CheckRunningFailedCount = 0 - message = "TCP Connection Established Successfully!" - if tcpCon, ok := conn.(*net.TCPConn); ok { - err := tcpCon.SetLinger(0) - s.logger.Error(err, "running check, set tcp linger failed") - } - opsRsp.Data["event"] = util.OperationSuccess - opsRsp.Data["message"] = message - return opsRsp, nil -} - -// getAddress returns component service address, if component is not listening on -// 127.0.0.1, the Operation needs to overwrite this function and set ops.DBAddress -func (s *CheckRunning) getAddress() string { - return "127.0.0.1" -} diff --git a/pkg/lorry/operations/replica/data_dump.go b/pkg/lorry/operations/replica/data_dump.go deleted file mode 100644 index b9a615498af..00000000000 --- a/pkg/lorry/operations/replica/data_dump.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "strings" - - "github.com/go-logr/logr" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type dataDump struct { - operations.Base - logger logr.Logger - Command []string -} - -func init() { - err := operations.Register(strings.ToLower(string(util.DataDumpOperation)), &dataDump{}) - if err != nil { - panic(err.Error()) - } -} - -func (s *dataDump) Init(_ context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - cmd, ok := actionCommands[constant.DataDumpAction] - if ok && len(cmd) > 0 { - s.Command = cmd - } - } - return nil -} - -func (s *dataDump) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - return nil -} - -func (s *dataDump) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - return nil, doCommonAction(ctx, s.logger, "dataDump", s.Command) -} - -func doCommonAction(ctx context.Context, logger logr.Logger, action string, commands []string) error { - envs, err := util.GetGlobalSharedEnvs() - if err != nil { - return err - } - output, err := util.ExecCommand(ctx, commands, envs) - if output != "" { - logger.Info(action, "output", output) - } - return err -} diff --git a/pkg/lorry/operations/replica/data_load.go b/pkg/lorry/operations/replica/data_load.go deleted file mode 100644 index 643b4fef891..00000000000 --- a/pkg/lorry/operations/replica/data_load.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "strings" - - "github.com/go-logr/logr" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type dataLoad struct { - operations.Base - logger logr.Logger - Command []string -} - -func init() { - err := operations.Register(strings.ToLower(string(util.DataLoadOperation)), &dataLoad{}) - if err != nil { - panic(err.Error()) - } -} - -func (s *dataLoad) Init(_ context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - cmd, ok := actionCommands[constant.DataLoadAction] - if ok && len(cmd) > 0 { - s.Command = cmd - } - } - return nil -} - -func (s *dataLoad) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - return nil -} - -func (s *dataLoad) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - return nil, doCommonAction(ctx, s.logger, "dataLoad", s.Command) -} diff --git a/pkg/lorry/operations/replica/getlag.go b/pkg/lorry/operations/replica/getlag.go deleted file mode 100644 index a0c3a1920e4..00000000000 --- a/pkg/lorry/operations/replica/getlag.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type GetLag struct { - operations.Base - dcsStore dcs.DCS - dbManager engines.DBManager - logger logr.Logger -} - -var getlag operations.Operation = &GetLag{} - -func init() { - err := operations.Register("getlag", getlag) - if err != nil { - panic(err.Error()) - } -} - -func (s *GetLag) Init(context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("getlag") - return nil -} - -func (s *GetLag) IsReadonly(context.Context) bool { - return false -} - -func (s *GetLag) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - sql := req.GetString("sql") - if sql == "" { - return nil, errors.New("no sql provided") - } - - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = util.ExecOperation - k8sStore := s.dcsStore.(*dcs.KubernetesStore) - cluster := k8sStore.GetClusterFromCache() - - lag, err := s.dbManager.GetLag(ctx, cluster) - if err != nil { - s.logger.Info("executing getlag error", "error", err) - return resp, err - } - - resp.Data["lag"] = lag - return resp, err -} diff --git a/pkg/lorry/operations/replica/getrole.go b/pkg/lorry/operations/replica/getrole.go deleted file mode 100644 index 187892b4db8..00000000000 --- a/pkg/lorry/operations/replica/getrole.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type GetRole struct { - operations.Base - dcsStore dcs.DCS - dbManager engines.DBManager - logger logr.Logger -} - -var getrole operations.Operation = &GetRole{} - -func init() { - err := operations.Register("getrole", getrole) - if err != nil { - panic(err.Error()) - } -} - -func (s *GetRole) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - s.logger = ctrl.Log.WithName("getrole") - - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - roleProbeCmd, ok := actionCommands[constant.RoleProbeAction] - if ok && len(roleProbeCmd) > 0 { - s.Command = roleProbeCmd - } - } - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - - s.dbManager = dbManager - return nil -} - -func (s *GetRole) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *GetRole) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = util.GetRoleOperation - - cluster := s.dcsStore.GetClusterFromCache() - role, err := s.dbManager.GetReplicaRole(ctx, cluster) - if err != nil { - s.logger.Info("executing getrole error", "error", err) - return resp, err - } - - resp.Data["role"] = role - return resp, err -} diff --git a/pkg/lorry/operations/replica/healthcheck.go b/pkg/lorry/operations/replica/healthcheck.go deleted file mode 100644 index a082483bf4a..00000000000 --- a/pkg/lorry/operations/replica/healthcheck.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type CheckStatus struct { - operations.Base - LeaderFailedCount int - FailureThreshold int - dcsStore dcs.DCS - dbManager engines.DBManager - checkFailedCount int - failedEventReportFrequency int - logger logr.Logger -} - -type FailoverManager interface { - Failover(ctx context.Context, cluster *dcs.Cluster, candidate string) error -} - -var checkstatus operations.Operation = &CheckStatus{} - -func init() { - err := operations.Register(strings.ToLower(string(util.HealthyCheckOperation)), checkstatus) - if err != nil { - panic(err.Error()) - } -} - -func (s *CheckStatus) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - s.failedEventReportFrequency = viper.GetInt("KB_FAILED_EVENT_REPORT_FREQUENCY") - if s.failedEventReportFrequency < 300 { - s.failedEventReportFrequency = 300 - } else if s.failedEventReportFrequency > 3600 { - s.failedEventReportFrequency = 3600 - } - - s.FailureThreshold = 3 - s.logger = ctrl.Log.WithName("checkstatus") - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - healthyCheckCmd, ok := actionCommands[constant.HealthyCheckAction] - if ok && len(healthyCheckCmd) > 0 { - s.Command = healthyCheckCmd - } - } - dbManager, err := register.GetDBManager(s.Command) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - - return nil -} - -func (s *CheckStatus) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *CheckStatus) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = util.HealthyCheckOperation - - k8sStore := s.dcsStore.(*dcs.KubernetesStore) - cluster := k8sStore.GetClusterFromCache() - err := s.dbManager.CurrentMemberHealthyCheck(ctx, cluster) - if err != nil { - return s.handlerError(ctx, err) - } - - isLeader, err := s.dbManager.IsLeader(ctx, cluster) - if err != nil { - return s.handlerError(ctx, err) - } - - if isLeader { - s.LeaderFailedCount = 0 - s.checkFailedCount = 0 - resp.Data["event"] = util.OperationSuccess - return resp, nil - } - err = s.dbManager.LeaderHealthyCheck(ctx, cluster) - if err != nil { - s.LeaderFailedCount++ - if s.LeaderFailedCount > s.FailureThreshold { - err = s.failover(ctx, cluster) - if err != nil { - return s.handlerError(ctx, err) - } - } - return s.handlerError(ctx, err) - } - s.LeaderFailedCount = 0 - s.checkFailedCount = 0 - resp.Data["event"] = util.OperationSuccess - return resp, nil -} - -func (s *CheckStatus) failover(ctx context.Context, cluster *dcs.Cluster) error { - failoverManger, ok := s.dbManager.(FailoverManager) - if !ok { - return errors.New("failover manager not found") - } - err := failoverManger.Failover(ctx, cluster, s.dbManager.GetCurrentMemberName()) - if err != nil { - return errors.Wrap(err, "failover failed") - } - return nil -} - -func (s *CheckStatus) handlerError(ctx context.Context, err error) (*operations.OpsResponse, error) { - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - message := err.Error() - s.logger.Info("healthy checks failed", "error", message) - resp.Data["event"] = util.OperationFailed - resp.Data["message"] = message - if s.checkFailedCount%s.failedEventReportFrequency == 0 { - s.logger.Info("healthy checks failed continuously", "times", s.checkFailedCount) - _ = util.SentEventForProbe(ctx, resp.Data) - } - s.checkFailedCount++ - return resp, err -} diff --git a/pkg/lorry/operations/replica/join.go b/pkg/lorry/operations/replica/join.go deleted file mode 100644 index 4700194e163..00000000000 --- a/pkg/lorry/operations/replica/join.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Join struct { - operations.Base - dcsStore dcs.DCS - logger logr.Logger - Timeout time.Duration - Command []string -} - -var join operations.Operation = &Join{} - -func init() { - err := operations.Register(strings.ToLower(string(util.JoinMemberOperation)), join) - if err != nil { - panic(err.Error()) - } -} - -func (s *Join) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - memberJoinCmd, ok := actionCommands[constant.MemberJoinAction] - if ok && len(memberJoinCmd) > 0 { - s.Command = memberJoinCmd - } - } - return nil -} - -func (s *Join) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "get manager failed") - } - - cluster, err := s.dcsStore.GetCluster() - if err != nil { - s.logger.Error(err, "get cluster failed") - return nil, err - } - - // join current member to db cluster - err = manager.JoinCurrentMemberToCluster(ctx, cluster) - if err != nil { - s.logger.Error(err, "join member to cluster failed") - return nil, err - } - - return nil, nil -} diff --git a/pkg/lorry/operations/replica/leave.go b/pkg/lorry/operations/replica/leave.go deleted file mode 100644 index e363a6f52b9..00000000000 --- a/pkg/lorry/operations/replica/leave.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Leave struct { - operations.Base - dcsStore dcs.DCS - logger logr.Logger - Timeout time.Duration - Command []string -} - -var leave operations.Operation = &Leave{} - -func init() { - err := operations.Register(strings.ToLower(string(util.LeaveMemberOperation)), leave) - if err != nil { - panic(err.Error()) - } -} - -func (s *Leave) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - memberLeaveCmd, ok := actionCommands[constant.MemberLeaveAction] - if ok && len(memberLeaveCmd) > 0 { - s.Command = memberLeaveCmd - } - } - return nil -} - -func (s *Leave) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "get manager failed") - } - - cluster, err := s.dcsStore.GetCluster() - if err != nil { - s.logger.Error(err, "get cluster failed") - return nil, err - } - - currentMember := cluster.GetMemberWithName(manager.GetCurrentMemberName()) - if !cluster.HaConfig.IsDeleting(currentMember) { - cluster.HaConfig.AddMemberToDelete(currentMember) - _ = s.dcsStore.UpdateHaConfig() - } - - // remove current member from db cluster - err = manager.LeaveMemberFromCluster(ctx, cluster, manager.GetCurrentMemberName()) - if err != nil { - s.logger.Error(err, "Leave member from cluster failed") - return nil, err - } - - if cluster.HaConfig.IsDeleting(currentMember) { - cluster.HaConfig.FinishDeleted(currentMember) - _ = s.dcsStore.UpdateHaConfig() - } - - return nil, nil -} diff --git a/pkg/lorry/operations/replica/rebuild.go b/pkg/lorry/operations/replica/rebuild.go deleted file mode 100644 index 8ac52340eba..00000000000 --- a/pkg/lorry/operations/replica/rebuild.go +++ /dev/null @@ -1,130 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" -) - -type Rebuild struct { - operations.Base - dcsStore dcs.DCS - dbManager engines.DBManager - logger logr.Logger -} - -var rebuild operations.Operation = &Rebuild{} - -func init() { - err := operations.Register("rebuild", rebuild) - if err != nil { - panic(err.Error()) - } -} - -func (s *Rebuild) Init(ctx context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - s.logger = ctrl.Log.WithName("Rebuild") - - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - rebuildCmd, ok := actionCommands[constant.RebuildAction] - if ok && len(rebuildCmd) > 0 { - s.Command = rebuildCmd - } - } - dbManager, err := register.GetDBManager(s.Command) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - - s.dbManager = dbManager - - return nil -} - -func (s *Rebuild) IsReadonly(ctx context.Context) bool { - return false -} - -func (s *Rebuild) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = constant.RebuildAction - - cluster := s.dcsStore.GetClusterFromCache() - currentMember := cluster.GetMemberWithName(s.dbManager.GetCurrentMemberName()) - if currentMember == nil || currentMember.HAPort == "" { - return nil, errors.Errorf("current node does not support rebuild, there is no ha service yet") - } - - haAddr := fmt.Sprintf("http://127.0.0.1:%s/v1.0/rebuild", currentMember.HAPort) - httpResp, err := http.Post(haAddr, "application/json", nil) - if err != nil { - return nil, errors.Wrap(err, "request ha service failed") - } - bodyBytes, err := io.ReadAll(httpResp.Body) - if err != nil { - return resp, errors.Wrap(err, "error reading response body") - } - bodyString := string(bodyBytes) - if httpResp.StatusCode/100 == 2 { - resp.Data["message"] = bodyString - return resp, nil - } - - s.logger.Info("request ha service failed", "status code", httpResp.StatusCode, "body", bodyString) - errResult := make(map[string]string) - err = json.Unmarshal(bodyBytes, &errResult) - if err != nil { - return nil, errors.New(bodyString) - } - if msg, ok := errResult["message"]; ok { - return nil, errors.New(msg) - } - - return nil, errors.New(bodyString) -} diff --git a/pkg/lorry/operations/replica/switchover.go b/pkg/lorry/operations/replica/switchover.go deleted file mode 100644 index c76012419c0..00000000000 --- a/pkg/lorry/operations/replica/switchover.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package replica - -import ( - "context" - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Switchover struct { - operations.Base - dcsStore dcs.DCS -} - -type SwitchoverManager interface { - Switchover(ctx context.Context, cluster *dcs.Cluster, primary, candidate string, force bool) error -} - -var switchover operations.Operation = &Switchover{} - -func init() { - err := operations.Register(strings.ToLower(string(util.SwitchoverOperation)), switchover) - if err != nil { - panic(err.Error()) - } -} - -func (s *Switchover) Init(_ context.Context) error { - s.dcsStore = dcs.GetStore() - if s.dcsStore == nil { - return errors.New("dcs store init failed") - } - - return nil -} - -func (s *Switchover) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - primary := req.GetString("primary") - candidate := req.GetString("candidate") - if primary == "" && candidate == "" { - return errors.New("primary or candidate must be set") - } - - cluster, err := s.dcsStore.GetCluster() - if cluster == nil { - return errors.Wrap(err, "get cluster failed") - } - - manager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - - if cluster.HaConfig == nil || !cluster.HaConfig.IsEnable() { - return errors.New("cluster's ha is disabled") - } - - if primary != "" { - leaderMember := cluster.GetMemberWithName(primary) - if leaderMember == nil { - message := fmt.Sprintf("primary %s not exists", primary) - return errors.New(message) - } - - if ok, err := manager.IsLeaderMember(ctx, cluster, leaderMember); err != nil || !ok { - message := fmt.Sprintf("%s is not the primary", primary) - return errors.New(message) - } - } - - if candidate != "" { - candidateMember := cluster.GetMemberWithName(candidate) - if candidateMember == nil { - message := fmt.Sprintf("candidate %s not exists", candidate) - return errors.New(message) - } - - if !manager.IsMemberHealthy(ctx, cluster, candidateMember) { - message := fmt.Sprintf("candidate %s is unhealthy", candidate) - return errors.New(message) - } - } else if len(manager.HasOtherHealthyMembers(ctx, cluster, primary)) == 0 { - return errors.New("candidate is not set and has no other healthy members") - } - - return nil -} - -func (s *Switchover) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - primary := req.GetString("primary") - candidate := req.GetString("candidate") - // force := req.GetBool("force") - // if swManager, ok := manager.(SwitchoverManager); ok { - // cluster, err := s.dcsStore.GetCluster() - // if cluster == nil { - // return nil, errors.Wrap(err, "get cluster failed") - // } - - // err = swManager.Switchover(ctx, cluster, primary, candidate, force) - // return nil, err - // } - - err := s.dcsStore.CreateSwitchover(primary, candidate) - if err != nil { - message := fmt.Sprintf("Create switchover failed: %v", err) - return nil, errors.New(message) - } - - return nil, nil -} diff --git a/pkg/lorry/operations/sql/exec.go b/pkg/lorry/operations/sql/exec.go deleted file mode 100644 index 2fecf29b3bd..00000000000 --- a/pkg/lorry/operations/sql/exec.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package sql - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Exec struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var exec operations.Operation = &Exec{} - -func init() { - err := operations.Register("exec", exec) - if err != nil { - panic(err.Error()) - } -} - -func (s *Exec) Init(context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("exec") - return nil -} - -func (s *Exec) IsReadonly(context.Context) bool { - return false -} - -func (s *Exec) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - sql := req.GetString("sql") - if sql == "" { - return nil, errors.New("no sql provided") - } - - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - resp.Data["operation"] = util.ExecOperation - - count, err := s.dbManager.Exec(ctx, sql) - if err != nil { - s.logger.Info("executing exec error", "error", err) - return resp, err - } - - resp.Data["count"] = count - return resp, err -} diff --git a/pkg/lorry/operations/sql/query.go b/pkg/lorry/operations/sql/query.go deleted file mode 100644 index 111fb0a4344..00000000000 --- a/pkg/lorry/operations/sql/query.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package sql - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Query struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var query operations.Operation = &Query{} - -func init() { - err := operations.Register("query", query) - if err != nil { - panic(err.Error()) - } -} - -func (s *Query) Init(context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("query") - return nil -} - -func (s *Query) IsReadonly(context.Context) bool { - return true -} - -func (s *Query) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - sql := req.GetString("sql") - if sql == "" { - return nil, errors.New("no sql provided") - } - - resp := operations.NewOpsResponse(util.QueryOperation) - - result, err := s.dbManager.Query(ctx, sql) - if err != nil { - s.logger.Info("executing query error", "error", err) - return resp, err - } - - resp.Data["result"] = string(result) - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/types.go b/pkg/lorry/operations/types.go deleted file mode 100644 index 2de69d45f99..00000000000 --- a/pkg/lorry/operations/types.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package operations - -import ( - "time" - - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -// OpsRequest is the request for Operation -type OpsRequest struct { - Data []byte `json:"data,omitempty"` - Parameters map[string]any `json:"parameters,omitempty"` -} - -func (r *OpsRequest) GetString(key string) string { - value, ok := r.Parameters[key] - if ok { - val, ok := value.(string) - if ok { - return val - } - } - return "" -} - -func (r *OpsRequest) GetBool(key string) bool { - value, ok := r.Parameters[key] - if ok { - val, ok := value.(bool) - if ok { - return val - } - } - return false -} - -// OpsResponse is the response for Operation -type OpsResponse struct { - Data map[string]any `json:"data,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -type OpsMetadata struct { - Operation util.OperationKind `json:"operation,omitempty"` - StartTime string `json:"startTime,omitempty"` - EndTime string `json:"endTime,omitempty"` - Extra string `json:"extra,omitempty"` -} - -func getAndFormatNow() string { - return time.Now().Format(time.RFC3339Nano) -} - -func NewOpsResponse(operation util.OperationKind) *OpsResponse { - resp := &OpsResponse{ - Data: map[string]any{}, - Metadata: map[string]string{}, - } - - resp.Metadata["startTime"] = getAndFormatNow() - resp.Metadata["operation"] = string(operation) - return resp -} - -func (resp *OpsResponse) WithSuccess(msg string) (*OpsResponse, error) { - resp.Metadata["endTime"] = getAndFormatNow() - resp.Data[util.RespFieldEvent] = util.OperationSuccess - if msg != "" { - resp.Data[util.RespFieldMessage] = msg - } - - return resp, nil -} - -func (resp *OpsResponse) WithError(err error) (*OpsResponse, error) { - resp.Metadata["endTime"] = getAndFormatNow() - resp.Data[util.RespFieldEvent] = util.OperationFailed - resp.Data[util.RespFieldMessage] = err.Error() - return resp, err -} diff --git a/pkg/lorry/operations/user/create.go b/pkg/lorry/operations/user/create.go deleted file mode 100644 index e5b162bab58..00000000000 --- a/pkg/lorry/operations/user/create.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - "encoding/json" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type CreateUser struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var createUser operations.Operation = &CreateUser{} - -func init() { - err := operations.Register(strings.ToLower(string(util.CreateUserOp)), createUser) - if err != nil { - panic(err.Error()) - } -} - -func (s *CreateUser) Init(ctx context.Context) error { - s.logger = ctrl.Log.WithName("CreateUser") - - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - accoutProvisionCmd, ok := actionCommands[constant.AccountProvisionAction] - if ok && len(accoutProvisionCmd) > 0 { - s.Command = accoutProvisionCmd - } - } - dbManager, err := register.GetDBManager(s.Command) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - return nil -} - -func (s *CreateUser) IsReadonly(ctx context.Context) bool { - return false -} - -func (s *CreateUser) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - userInfo, err := UserInfoParser(req) - if err != nil { - return err - } - - return userInfo.UserNameAndPasswdValidator() -} - -func (s *CreateUser) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - userInfo, _ := UserInfoParser(req) - resp := operations.NewOpsResponse(util.CreateUserOp) - - user, err := s.dbManager.DescribeUser(ctx, userInfo.UserName) - if err == nil && user != nil { - return resp.WithSuccess("account already exists") - } - - // for compatibility with old addons that specify accoutprovision action but not work actually. - err = s.dbManager.CreateUser(ctx, userInfo.UserName, userInfo.Password, userInfo.Statement) - if err != nil { - err = errors.Cause(err) - s.logger.Info("executing CreateUser error", "error", err.Error()) - return resp, err - } - - if userInfo.RoleName != "" { - err := s.dbManager.GrantUserRole(ctx, userInfo.UserName, userInfo.RoleName) - if err != nil && err != models.ErrNotImplemented { - s.logger.Info("executing grantRole error", "error", err.Error()) - return resp, err - } - } - - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/user/delete.go b/pkg/lorry/operations/user/delete.go deleted file mode 100644 index 8dbb38daa7b..00000000000 --- a/pkg/lorry/operations/user/delete.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type DeleteUser struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var deleteUser operations.Operation = &DeleteUser{} - -func init() { - err := operations.Register("deleteuser", deleteUser) - if err != nil { - panic(err.Error()) - } -} - -func (s *DeleteUser) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("DeleteUser") - return nil -} - -func (s *DeleteUser) IsReadonly(ctx context.Context) bool { - return false -} - -func (s *DeleteUser) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - userInfo, err := UserInfoParser(req) - if err != nil { - return err - } - - return userInfo.UserNameValidator() -} - -func (s *DeleteUser) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - userInfo, _ := UserInfoParser(req) - resp := operations.NewOpsResponse(util.DeleteUserOp) - - err := s.dbManager.DeleteUser(ctx, userInfo.UserName) - if err != nil { - s.logger.Info("executing DeleteUser error", "error", err) - return resp, err - } - - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/user/describe.go b/pkg/lorry/operations/user/describe.go deleted file mode 100644 index f154e2ef3c3..00000000000 --- a/pkg/lorry/operations/user/describe.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/models" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type DescribeUser struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var describeUser operations.Operation = &DescribeUser{} - -func init() { - err := operations.Register("describeuser", describeUser) - if err != nil { - panic(err.Error()) - } -} - -func (s *DescribeUser) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("describeUser") - return nil -} - -func (s *DescribeUser) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *DescribeUser) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - userInfo, err := UserInfoParser(req) - if err != nil { - return err - } - - return userInfo.UserNameValidator() -} - -func (s *DescribeUser) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - userInfo, _ := UserInfoParser(req) - resp := operations.NewOpsResponse(util.DescribeUserOp) - - result, err := s.dbManager.DescribeUser(ctx, userInfo.UserName) - if err != nil { - s.logger.Info("executing describeUser error", "error", err) - return resp, err - } - - resp.Data["user"] = result - return resp.WithSuccess("") -} - -func UserInfoParser(req *operations.OpsRequest) (*models.UserInfo, error) { - user := &models.UserInfo{} - if req == nil || req.Parameters == nil { - return nil, fmt.Errorf("no Parameters provided") - } else if jsonData, err := json.Marshal(req.Parameters); err != nil { - return nil, err - } else if err = json.Unmarshal(jsonData, user); err != nil { - return nil, err - } - return user, nil -} diff --git a/pkg/lorry/operations/user/describe_test.go b/pkg/lorry/operations/user/describe_test.go deleted file mode 100644 index 0caf76d5efe..00000000000 --- a/pkg/lorry/operations/user/describe_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/apecloud/kubeblocks/pkg/lorry/operations" -) - -func TestUserInfoParser(t *testing.T) { - req := &operations.OpsRequest{ - Parameters: map[string]interface{}{ - "userName": "john", - "age": 30, - }, - } - - user, err := UserInfoParser(req) - assert.Nil(t, err) - assert.Equal(t, "john", user.UserName) -} diff --git a/pkg/lorry/operations/user/grant_role.go b/pkg/lorry/operations/user/grant_role.go deleted file mode 100644 index 67ee4353460..00000000000 --- a/pkg/lorry/operations/user/grant_role.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type GrantRole struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var grantRole operations.Operation = &GrantRole{} - -func init() { - err := operations.Register(strings.ToLower(string(util.GrantUserRoleOp)), grantRole) - if err != nil { - panic(err.Error()) - } -} - -func (s *GrantRole) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("grantRole") - return nil -} - -func (s *GrantRole) IsReadonly(ctx context.Context) bool { - return false -} - -func (s *GrantRole) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - userInfo, err := UserInfoParser(req) - if err != nil { - return err - } - - return userInfo.UserNameValidator() -} - -func (s *GrantRole) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - userInfo, _ := UserInfoParser(req) - resp := operations.NewOpsResponse(util.GrantUserRoleOp) - - err := s.dbManager.GrantUserRole(ctx, userInfo.UserName, userInfo.RoleName) - if err != nil { - s.logger.Info("executing grantRole error", "error", err) - return resp, err - } - - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/user/list.go b/pkg/lorry/operations/user/list.go deleted file mode 100644 index b453838688e..00000000000 --- a/pkg/lorry/operations/user/list.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type ListUsers struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var listusers operations.Operation = &ListUsers{} - -func init() { - err := operations.Register("listusers", listusers) - if err != nil { - panic(err.Error()) - } -} - -func (s *ListUsers) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("listusers") - return nil -} - -func (s *ListUsers) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *ListUsers) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := operations.NewOpsResponse(util.ListUsersOp) - - result, err := s.dbManager.ListUsers(ctx) - if err != nil { - s.logger.Info("executing listusers error", "error", err) - return resp, err - } - - resp.Data["users"] = result - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/user/list_system_accounts.go b/pkg/lorry/operations/user/list_system_accounts.go deleted file mode 100644 index 3b643274c95..00000000000 --- a/pkg/lorry/operations/user/list_system_accounts.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type ListSystemAccounts struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var listSystemAccounts operations.Operation = &ListSystemAccounts{} - -func init() { - err := operations.Register("listsystemaccounts", listSystemAccounts) - if err != nil { - panic(err.Error()) - } -} - -func (s *ListSystemAccounts) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("listSystemAccounts") - return nil -} - -func (s *ListSystemAccounts) IsReadonly(ctx context.Context) bool { - return true -} - -func (s *ListSystemAccounts) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - resp := operations.NewOpsResponse(util.ListSystemAccountsOp) - - result, err := s.dbManager.ListSystemAccounts(ctx) - if err != nil { - s.logger.Info("executing ListSystemAccounts error", "error", err) - return resp, err - } - - resp.Data["systemAccounts"] = result - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/user/revoke_role.go b/pkg/lorry/operations/user/revoke_role.go deleted file mode 100644 index 42fabfc51ef..00000000000 --- a/pkg/lorry/operations/user/revoke_role.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package user - -import ( - "context" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type RevokeRole struct { - operations.Base - dbManager engines.DBManager - logger logr.Logger -} - -var revokeRole operations.Operation = &RevokeRole{} - -func init() { - err := operations.Register(strings.ToLower(string(util.RevokeUserRoleOp)), revokeRole) - if err != nil { - panic(err.Error()) - } -} - -func (s *RevokeRole) Init(ctx context.Context) error { - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - s.dbManager = dbManager - s.logger = ctrl.Log.WithName("revokeRole") - return nil -} - -func (s *RevokeRole) IsReadonly(ctx context.Context) bool { - return false -} - -func (s *RevokeRole) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - userInfo, err := UserInfoParser(req) - if err != nil { - return err - } - - return userInfo.UserNameAndRoleValidator() -} - -func (s *RevokeRole) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - userInfo, _ := UserInfoParser(req) - resp := operations.NewOpsResponse(util.RevokeUserRoleOp) - - err := s.dbManager.RevokeUserRole(ctx, userInfo.UserName, userInfo.RoleName) - if err != nil { - s.logger.Info("executing RevokeRole error", "error", err) - return resp, err - } - - return resp.WithSuccess("") -} diff --git a/pkg/lorry/operations/volume/lock.go b/pkg/lorry/operations/volume/lock.go deleted file mode 100644 index da872c34ba0..00000000000 --- a/pkg/lorry/operations/volume/lock.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package volume - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Lock struct { - operations.Base - logger logr.Logger - Timeout time.Duration - Command []string -} - -var lock operations.Operation = &Lock{} - -func init() { - err := operations.Register(strings.ToLower(string(util.LockOperation)), lock) - if err != nil { - panic(err.Error()) - } -} - -func (s *Lock) Init(ctx context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - readonlyCmd, ok := actionCommands[constant.ReadonlyAction] - if ok && len(readonlyCmd) > 0 { - s.Command = readonlyCmd - } - } - return nil -} - -func (s *Lock) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "Get DB manager failed") - } - - err = manager.Lock(ctx, "disk full") - if err != nil { - return nil, errors.Wrap(err, "Lock DB failed") - } - - return nil, nil -} diff --git a/pkg/lorry/operations/volume/protect.go b/pkg/lorry/operations/volume/protect.go deleted file mode 100644 index 63bcad8cb9e..00000000000 --- a/pkg/lorry/operations/volume/protect.go +++ /dev/null @@ -1,457 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package volume - -import ( - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strconv" - "strings" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1" - ctrl "sigs.k8s.io/controller-runtime" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -const ( - kubeletStatsSummaryURL = "https://%s:%s/stats/summary" - - certFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" - - reasonLock = "HighVolumeWatermark" - reasonUnlock = "LowVolumeWatermark" // TODO -) - -type volumeStatsRequester interface { - init(ctx context.Context) error - request(ctx context.Context) ([]byte, error) -} - -var protection operations.Operation = &Protection{} - -func init() { - err := operations.Register(strings.ToLower(string(util.VolumeProtection)), protection) - if err != nil { - panic(err.Error()) - } -} - -type volumeExt struct { - Name string - HighWatermark int - Stats statsv1alpha1.VolumeStats -} - -type Protection struct { - operations.Base - dbManager engines.DBManager - Requester volumeStatsRequester - Pod string - HighWatermark int - Volumes map[string]volumeExt - Readonly bool - SendEvent bool // to disable event for testing - Logger logr.Logger -} - -func (p *Protection) Init(ctx context.Context) error { - p.Logger = ctrl.Log.WithName("Volume-Protection") - if p.Requester == nil { - p.Requester = &httpsVolumeStatsRequester{ - logger: p.Logger, - } - } - p.SendEvent = true - - dbManager, err := register.GetDBManager(nil) - if err != nil { - return errors.Wrap(err, "get manager failed") - } - p.dbManager = dbManager - - if err := p.Requester.init(ctx); err != nil { - return err - } - - p.Pod = viper.GetString(constant.KBEnvPodName) - if err := p.initVolumes(); err != nil { - p.Logger.Error(err, "init volumes to monitor error") - } - p.Logger.Info("succeed to init volume protection", "pod", p.Pod, "spec", p.buildVolumesMsg()) - return nil -} - -func (p *Protection) PreCheck(ctx context.Context, req *operations.OpsRequest) error { - return nil -} - -func (p *Protection) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - if p.disabled() { - p.Logger.Info("the volume protection operation is disabled") - return nil, nil - } - - summary, err := p.Requester.request(ctx) - if err != nil { - p.Logger.Error(err, "request stats summary from kubelet error") - return nil, err - } - - if err = p.updateVolumeStats(summary); err != nil { - return nil, err - } - - volumeUsages, err := p.checkUsage(ctx) - resp := &operations.OpsResponse{ - Data: map[string]any{}, - } - if err == nil { - resp.Data["protect"] = volumeUsages - } - return resp, err -} - -func (p *Protection) initVolumes() error { - spec := &appsv1alpha1.VolumeProtectionSpec{} - raw := viper.GetString(constant.KBEnvVolumeProtectionSpec) - if raw == "" { - return nil - } - - if err := json.Unmarshal([]byte(raw), spec); err != nil { - p.Logger.Error(err, "unmarshal volume protection spec error", "raw spec", raw) - return err - } - - p.HighWatermark = normalizeVolumeWatermark(&spec.HighWatermark, 0) - - if p.Volumes == nil { - p.Volumes = make(map[string]volumeExt) - } - for _, v := range spec.Volumes { - p.Volumes[v.Name] = volumeExt{ - Name: v.Name, - HighWatermark: normalizeVolumeWatermark(v.HighWatermark, p.HighWatermark), - Stats: statsv1alpha1.VolumeStats{ - Name: v.Name, - }, - } - } - return nil -} - -func (p *Protection) disabled() bool { - // TODO: check the role and skip secondary instances. - if len(p.Pod) == 0 || len(p.Volumes) == 0 { - return true - } - for _, v := range p.Volumes { - // take (0, 100] as enabled - if v.HighWatermark > 0 && v.HighWatermark <= 100 { - return false - } - } - return true -} - -func (p *Protection) updateVolumeStats(payload []byte) error { - summary := &statsv1alpha1.Summary{} - if err := json.Unmarshal(payload, summary); err != nil { - p.Logger.Error(err, "stats summary obtained from kubelet error") - return err - } - for _, pod := range summary.Pods { - if pod.PodRef.Name == p.Pod { - for _, stats := range pod.VolumeStats { - if _, ok := p.Volumes[stats.Name]; !ok { - continue - } - v := p.Volumes[stats.Name] - v.Stats = stats - p.Volumes[stats.Name] = v - } - break - } - } - return nil -} - -func (p *Protection) checkUsage(ctx context.Context) (map[string]any, error) { - lower := make([]string, 0) - higher := make([]string, 0) - for name, v := range p.Volumes { - ret := p.checkVolumeWatermark(v) - if ret == 0 { - lower = append(lower, name) - } else { - higher = append(higher, name) - } - } - - volumeUsages := p.buildVolumesMsg() - readonly := p.Readonly - // the instance is running normally and there have volume(s) over the space usage threshold. - if !readonly && len(higher) > 0 { - if err := p.highWatermark(ctx, volumeUsages); err != nil { - return volumeUsages, err - } - } - // the instance is protected in RO mode, and all volumes' space usage are under the threshold. - if readonly && len(lower) == len(p.Volumes) { - if err := p.lowWatermark(ctx, volumeUsages); err != nil { - return volumeUsages, err - } - } - return volumeUsages, nil -} - -// checkVolumeWatermark checks whether the volume's space usage is over the threshold. -// -// returns 0 if the volume will not be taken in account or its space usage is under the threshold -// returns non-zero if the volume space usage is over the threshold -func (p *Protection) checkVolumeWatermark(v volumeExt) int { - if v.HighWatermark == 0 { // disabled - return 0 - } - if v.Stats.CapacityBytes == nil || v.Stats.UsedBytes == nil { - return 0 - } - thresholdBytes := *v.Stats.CapacityBytes / 100 * uint64(v.HighWatermark) - if *v.Stats.UsedBytes < thresholdBytes { - return 0 - } - return 1 -} - -func (p *Protection) highWatermark(ctx context.Context, volumeUsages map[string]any) error { - if p.Readonly { // double check - return nil - } - if err := p.lockInstance(ctx); err != nil { - p.Logger.Error(err, "set instance to read-only error", "volumes", volumeUsages) - return err - } - - p.Logger.Info("set instance to read-only OK", "msg", volumeUsages) - p.Readonly = true - - if err := p.sendEvent(ctx, reasonLock, volumeUsages); err != nil { - p.Logger.Error(err, "send volume protection (lock) event error", "volumes", volumeUsages) - return err - } - return nil -} - -func (p *Protection) lowWatermark(ctx context.Context, volumeUsages map[string]any) error { - if !p.Readonly { // double check - return nil - } - if err := p.unlockInstance(ctx); err != nil { - p.Logger.Error(err, "reset instance to read-write error", "volumes", volumeUsages) - return err - } - - p.Logger.Info("reset instance to read-write OK", "msg", volumeUsages) - p.Readonly = false - - if err := p.sendEvent(ctx, reasonUnlock, volumeUsages); err != nil { - p.Logger.Error(err, "send volume protection (unlock) event error", "volumes", volumeUsages) - return err - } - return nil -} - -func (p *Protection) lockInstance(ctx context.Context) error { - return p.dbManager.Lock(ctx, "disk full") -} - -func (p *Protection) unlockInstance(ctx context.Context) error { - return p.dbManager.Unlock(ctx) -} - -func (p *Protection) buildVolumesMsg() map[string]any { - volumes := make([]map[string]string, 0) - for _, v := range p.Volumes { - usage := make(map[string]string) - if v.HighWatermark != p.HighWatermark { - usage["highWatermark"] = fmt.Sprintf("%d", v.HighWatermark) - } - stats := v.Stats - if stats.UsedBytes == nil || stats.CapacityBytes == nil { - usage[v.Name] = "" - } else { - usage[v.Name] = fmt.Sprintf("%d%%", int(*stats.UsedBytes*100 / *stats.CapacityBytes)) - } - volumes = append(volumes, usage) - } - usages := map[string]any{ - "highWatermark": fmt.Sprintf("%d", p.HighWatermark), - "volumes": volumes, - } - return usages -} - -func (p *Protection) sendEvent(ctx context.Context, reason string, volumeUsages map[string]any) error { - if p.SendEvent { - event, err := util.CreateEvent(reason, volumeUsages) - if err != nil { - return errors.Wrap(err, "create volume protection event failed") - } - return util.SendEvent(ctx, event) - } - return nil -} - -type httpsVolumeStatsRequester struct { - logger logr.Logger - cli *http.Client - req *http.Request -} - -var _ volumeStatsRequester = &httpsVolumeStatsRequester{} - -func (r *httpsVolumeStatsRequester) init(ctx context.Context) error { - var err error - if r.cli, err = httpClient(); err != nil { - r.logger.Error(err, "build HTTP client error at setup") - return err - } - // if r.req, err = httpRequest(ctx); err != nil { - // r.logger.Error(err, "build HTTP request error at setup, will try it later") - // } - return nil -} - -func (r *httpsVolumeStatsRequester) request(ctx context.Context) ([]byte, error) { - if r.cli == nil { - return nil, fmt.Errorf("HTTP client for kubelet is unavailable") - } - if r.req == nil { - // try to build http request again - var err error - r.req, err = httpRequest(ctx) - if err != nil { - r.logger.Error(err, "build HTTP request to query kubelet error") - return nil, err - } - } - - req := r.req.WithContext(ctx) - rsp, err := r.cli.Do(req) - if err != nil { - r.logger.Error(err, "issue request to kubelet error") - return nil, err - } - if rsp.StatusCode != 200 { - r.logger.Error(nil, fmt.Sprintf("HTTP response from kubelet error: %s", rsp.Status)) - return nil, fmt.Errorf(rsp.Status) - } - - defer rsp.Body.Close() - return io.ReadAll(rsp.Body) -} - -func httpClient() (*http.Client, error) { - cert, err := os.ReadFile(certFile) - if err != nil { - return nil, err - } - certPool := x509.NewCertPool() - certPool.AppendCertsFromPEM(cert) - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPool, - }, - }, - }, nil -} - -func httpRequest(ctx context.Context) (*http.Request, error) { - host, err := kubeletEndpointHost(ctx) - if err != nil { - return nil, err - } - port, err := kubeletEndpointPort(ctx) - if err != nil { - return nil, err - } - url := fmt.Sprintf(kubeletStatsSummaryURL, host, port) - - accessToken, err := os.ReadFile(tokenFile) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - if len(accessToken) > 0 { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) - } - return req, nil -} - -func kubeletEndpointHost(ctx context.Context) (string, error) { - return viper.GetString(constant.KBEnvHostIP), nil -} - -func kubeletEndpointPort(ctx context.Context) (string, error) { - config, err := rest.InClusterConfig() - if err != nil { - return "", err - } - cliset, err := kubernetes.NewForConfig(config) - if err != nil { - return "", err - } - node, err := cliset.CoreV1().Nodes().Get(ctx, viper.GetString(constant.KBEnvNodeName), metav1.GetOptions{}) - if err != nil { - return "", err - } - return strconv.Itoa(int(node.Status.DaemonEndpoints.KubeletEndpoint.Port)), nil -} - -func normalizeVolumeWatermark(watermark *int, defaultVal int) int { - if watermark == nil || *watermark < 0 || *watermark > 100 { - return defaultVal - } - return *watermark -} diff --git a/pkg/lorry/operations/volume/protect_test.go b/pkg/lorry/operations/volume/protect_test.go deleted file mode 100644 index 11134695296..00000000000 --- a/pkg/lorry/operations/volume/protect_test.go +++ /dev/null @@ -1,424 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package volume - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/golang/mock/gomock" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/viper" - "k8s.io/apimachinery/pkg/util/rand" - statsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" -) - -type mockVolumeStatsRequester struct { - summary []byte -} - -var _ volumeStatsRequester = &mockVolumeStatsRequester{} - -func (r *mockVolumeStatsRequester) init(ctx context.Context) error { - return nil -} - -func (r *mockVolumeStatsRequester) request(ctx context.Context) ([]byte, error) { - return r.summary, nil -} - -type mockErrorVolumeStatsRequester struct { - initErr bool - requestErr bool -} - -var _ volumeStatsRequester = &mockErrorVolumeStatsRequester{} - -func (r *mockErrorVolumeStatsRequester) init(ctx context.Context) error { - if r.initErr { - return fmt.Errorf("error") - } - return nil -} - -func (r *mockErrorVolumeStatsRequester) request(ctx context.Context) ([]byte, error) { - if r.requestErr { - return nil, fmt.Errorf("error") - } - return nil, nil -} - -var _ = Describe("Volume Protection Operation", func() { - var ( - podName = rand.String(8) - volumeName = rand.String(8) - defaultThreshold = 90 - zeroThreshold = 0 - fullThreshold = 100 - invalidThresholdLower = -1 - invalidThresholdHigher = 101 - capacityBytes = uint64(10 * 1024 * 1024 * 1024) - usedBytesUnderThreshold = capacityBytes * uint64(defaultThreshold-3) / 100 - usedBytesOverThreshold = capacityBytes * uint64(defaultThreshold+3) / 100 - usedBytesOverThresholdHigher = capacityBytes * uint64(defaultThreshold+4) / 100 - volumeProtectionSpec = &appsv1alpha1.VolumeProtectionSpec{ - HighWatermark: defaultThreshold, - Volumes: []appsv1alpha1.ProtectedVolume{ - { - Name: volumeName, - HighWatermark: &defaultThreshold, - }, - }, - } - ) - - setup := func() { - raw, _ := json.Marshal(volumeProtectionSpec) - viper.SetDefault(constant.KBEnvVolumeProtectionSpec, string(raw)) - viper.SetDefault(constant.KBEnvPodName, podName) - } - - cleanAll := func() { - } - - BeforeEach(setup) - - AfterEach(cleanAll) - - resetVolumeProtectionSpecEnv := func(spec appsv1alpha1.VolumeProtectionSpec) { - raw, _ := json.Marshal(spec) - viper.SetDefault(constant.KBEnvVolumeProtectionSpec, string(raw)) - } - - newProtection := func() *Protection { - protection := &Protection{ - Requester: &mockVolumeStatsRequester{}, - } - Expect(protection.Init(context.Background())).Should(Succeed()) - protection.SendEvent = false - return protection - } - - Context("Volume Protection", func() { - It("init - succeed", func() { - protection := &Protection{ - Requester: &mockVolumeStatsRequester{}, - } - Expect(protection.Init(context.Background())).Should(Succeed()) - Expect(protection.Pod).Should(Equal(podName)) - Expect(protection.HighWatermark).Should(Equal(volumeProtectionSpec.HighWatermark)) - Expect(len(protection.Volumes)).Should(Equal(len(volumeProtectionSpec.Volumes))) - }) - - It("init - empty volume protection spec env", func() { - viper.SetDefault(constant.KBEnvVolumeProtectionSpec, "") - protection := &Protection{ - Requester: &mockVolumeStatsRequester{}, - } - Expect(protection.initVolumes()).Should(BeNil()) - }) - - It("init - init requester error", func() { - protection := &Protection{ - Requester: &mockErrorVolumeStatsRequester{initErr: true}, - } - - Expect(protection.Init(context.Background())).Should(HaveOccurred()) - }) - - It("init - normalize watermark", func() { - By("normalize global watermark") - for _, val := range []int{-1, 0, 101} { - resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ - HighWatermark: val, - }) - obj := newProtection() - Expect(obj.initVolumes()).Should(Succeed()) - Expect(obj.HighWatermark).Should(Equal(0)) - } - - By("normalize volume watermark") - spec := appsv1alpha1.VolumeProtectionSpec{ - HighWatermark: defaultThreshold, - Volumes: []appsv1alpha1.ProtectedVolume{ - { - Name: "01", - HighWatermark: &invalidThresholdLower, - }, - { - Name: "02", - HighWatermark: &invalidThresholdHigher, - }, - { - Name: "03", - HighWatermark: &zeroThreshold, - }, - { - Name: "04", - HighWatermark: &fullThreshold, - }, - }, - } - resetVolumeProtectionSpecEnv(spec) - obj := newProtection() - Expect(obj.initVolumes()).Should(Succeed()) - Expect(obj.HighWatermark).Should(Equal(spec.HighWatermark)) - for _, v := range spec.Volumes { - if *v.HighWatermark >= 0 && *v.HighWatermark <= 100 { - Expect(obj.Volumes[v.Name].HighWatermark).Should(Equal(*v.HighWatermark)) - } else { - Expect(obj.Volumes[v.Name].HighWatermark).Should(Equal(obj.HighWatermark)) - } - } - }) - - It("disabled - empty pod name", func() { - viper.SetDefault(constant.KBEnvPodName, "") - obj := newProtection() - obj.Pod = viper.GetString(constant.KBEnvPodName) - Expect(obj.disabled()).Should(BeTrue()) - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("disabled - empty volume", func() { - resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ - HighWatermark: defaultThreshold, - Volumes: []appsv1alpha1.ProtectedVolume{}, - }) - obj := newProtection() - obj.Volumes = nil - Expect(obj.initVolumes()).Should(Succeed()) - Expect(obj.disabled()).Should(BeTrue()) - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("disabled - volumes with zero watermark", func() { - resetVolumeProtectionSpecEnv(appsv1alpha1.VolumeProtectionSpec{ - HighWatermark: defaultThreshold, - Volumes: []appsv1alpha1.ProtectedVolume{ - { - Name: "data", - HighWatermark: &zeroThreshold, - }, - }, - }) - obj := newProtection() - obj.Volumes = nil - Expect(obj.initVolumes()).Should(Succeed()) - Expect(obj.disabled()).Should(BeTrue()) - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - - }) - - It("query stats summary - request error", func() { - obj := newProtection() - obj.Requester = &mockErrorVolumeStatsRequester{requestErr: true} - _, err := obj.Do(context.Background(), nil) - Expect(err).Should(HaveOccurred()) - }) - - It("query stats summary - format error", func() { - // default summary is empty string - obj := newProtection() - _, err := obj.Do(context.Background(), nil) - Expect(err).Should(HaveOccurred()) - }) - - It("query stats summary - ok", func() { - obj := newProtection() - mock := obj.Requester.(*mockVolumeStatsRequester) - stats := statsv1alpha1.Summary{} - mock.summary, _ = json.Marshal(stats) - Expect(obj.Init(context.Background())).Should(Succeed()) - - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("update volume stats summary", func() { - obj := newProtection() - mock := obj.Requester.(*mockVolumeStatsRequester) - stats := statsv1alpha1.Summary{ - Pods: []statsv1alpha1.PodStats{ - { - PodRef: statsv1alpha1.PodReference{ - Name: podName, - }, - VolumeStats: []statsv1alpha1.VolumeStats{ - { - Name: volumeName, - FsStats: statsv1alpha1.FsStats{ - CapacityBytes: &capacityBytes, - UsedBytes: &usedBytesUnderThreshold, - }, - }, - }, - }, - }, - } - mock.summary, _ = json.Marshal(stats) - - // nil capacity and used bytes - stats.Pods[0].VolumeStats[0].CapacityBytes = nil - stats.Pods[0].VolumeStats[0].UsedBytes = nil - - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - - stats.Pods[0].VolumeStats[0].CapacityBytes = &capacityBytes - stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold - _, err = obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - Expect(obj.Volumes[volumeName].Stats).Should(Equal(stats.Pods[0].VolumeStats[0])) - Expect(obj.Readonly).Should(BeFalse()) - }) - - It("volume over high watermark", func() { - ctrl := gomock.NewController(GinkgoT()) - mockDBManager := engines.NewMockDBManager(ctrl) - mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil) - register.SetDBManager(mockDBManager) - - obj := newProtection() - mock := obj.Requester.(*mockVolumeStatsRequester) - stats := statsv1alpha1.Summary{ - Pods: []statsv1alpha1.PodStats{ - { - PodRef: statsv1alpha1.PodReference{ - Name: podName, - }, - VolumeStats: []statsv1alpha1.VolumeStats{ - { - Name: volumeName, - FsStats: statsv1alpha1.FsStats{ - CapacityBytes: &capacityBytes, - UsedBytes: &usedBytesOverThreshold, - }, - }, - }, - }, - }, - } - mock.summary, _ = json.Marshal(stats) - - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - Expect(obj.Readonly).Should(BeTrue()) - - // check again and the usage is higher, no lock action triggered again - stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesOverThresholdHigher - mock.summary, _ = json.Marshal(stats) - _, err = obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - Expect(obj.Readonly).Should(BeTrue()) - }) - - It("volume under high watermark", func() { - ctrl := gomock.NewController(GinkgoT()) - mockDBManager := engines.NewMockDBManager(ctrl) - mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil) - mockDBManager.EXPECT().Unlock(gomock.Any()).Return(nil) - register.SetDBManager(mockDBManager) - - obj := newProtection() - mock := obj.Requester.(*mockVolumeStatsRequester) - stats := statsv1alpha1.Summary{ - Pods: []statsv1alpha1.PodStats{ - { - PodRef: statsv1alpha1.PodReference{ - Name: podName, - }, - VolumeStats: []statsv1alpha1.VolumeStats{ - { - Name: volumeName, - FsStats: statsv1alpha1.FsStats{ - CapacityBytes: &capacityBytes, - UsedBytes: &usedBytesOverThreshold, - }, - }, - }, - }, - }, - } - mock.summary, _ = json.Marshal(stats) - _, err := obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - Expect(obj.Readonly).Should(BeTrue()) - - // drops down the usage, and trigger unlock action - stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold - mock.summary, _ = json.Marshal(stats) - _, err = obj.Do(context.Background(), nil) - Expect(err).ShouldNot(HaveOccurred()) - Expect(obj.Readonly).Should(BeFalse()) - }) - - It("lock/unlock error", func() { - ctrl := gomock.NewController(GinkgoT()) - mockDBManager := engines.NewMockDBManager(ctrl) - mockDBManager.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(fmt.Errorf("test")) - mockDBManager.EXPECT().Unlock(gomock.Any()).Return(fmt.Errorf("test")) - register.SetDBManager(mockDBManager) - - obj := newProtection() - mock := obj.Requester.(*mockVolumeStatsRequester) - stats := statsv1alpha1.Summary{ - Pods: []statsv1alpha1.PodStats{ - { - PodRef: statsv1alpha1.PodReference{ - Name: podName, - }, - VolumeStats: []statsv1alpha1.VolumeStats{ - { - Name: volumeName, - FsStats: statsv1alpha1.FsStats{ - CapacityBytes: &capacityBytes, - UsedBytes: &usedBytesOverThreshold, - }, - }, - }, - }, - }, - } - mock.summary, _ = json.Marshal(stats) - _, err := obj.Do(context.Background(), nil) - Expect(err).Should(HaveOccurred()) - Expect(obj.Readonly).Should(BeFalse()) // unchanged - - // drops down the usage, and trigger unlock action - stats.Pods[0].VolumeStats[0].UsedBytes = &usedBytesUnderThreshold - mock.summary, _ = json.Marshal(stats) - obj.Readonly = true // hack it as locked - _, err = obj.Do(context.Background(), nil) - Expect(err).Should(HaveOccurred()) - Expect(obj.Readonly).Should(BeTrue()) // unchanged - }) - }) -}) diff --git a/pkg/lorry/operations/volume/suite_test.go b/pkg/lorry/operations/volume/suite_test.go deleted file mode 100644 index e224705b1a6..00000000000 --- a/pkg/lorry/operations/volume/suite_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package volume - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" - "github.com/spf13/viper" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/dcs" - "github.com/apecloud/kubeblocks/pkg/lorry/engines" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" -) - -var ( - dbManager engines.DBManager - mockDBManager *engines.MockDBManager - dcsStore dcs.DCS - mockDCSStore *dcs.MockDCS -) - -func init() { - viper.AutomaticEnv() - viper.SetDefault(constant.KBEnvPodName, "pod-test") - viper.SetDefault(constant.KBEnvClusterCompName, "cluster-component-test") - viper.SetDefault(constant.KBEnvNamespace, "namespace-test") - ctrl.SetLogger(zap.New()) -} - -func TestVolumeOperations(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Volume Operations. Suite") -} - -var _ = BeforeSuite(func() { - // Init mock db manager - InitMockDBManager() - - // Init mock dcs store - InitMockDCSStore() -}) - -var _ = AfterSuite(func() { -}) - -func InitMockDBManager() { - ctrl := gomock.NewController(GinkgoT()) - mockDBManager = engines.NewMockDBManager(ctrl) - register.SetDBManager(mockDBManager) - dbManager = mockDBManager -} - -func InitMockDCSStore() { - ctrl := gomock.NewController(GinkgoT()) - mockDCSStore = dcs.NewMockDCS(ctrl) - mockDCSStore.EXPECT().GetClusterFromCache().Return(&dcs.Cluster{}).AnyTimes() - dcs.SetStore(mockDCSStore) - dcsStore = mockDCSStore -} diff --git a/pkg/lorry/operations/volume/unlock.go b/pkg/lorry/operations/volume/unlock.go deleted file mode 100644 index 7403a47fea4..00000000000 --- a/pkg/lorry/operations/volume/unlock.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package volume - -import ( - "context" - "encoding/json" - "strings" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/lorry/engines/register" - "github.com/apecloud/kubeblocks/pkg/lorry/operations" - "github.com/apecloud/kubeblocks/pkg/lorry/util" -) - -type Unlock struct { - operations.Base - logger logr.Logger - Timeout time.Duration - Command []string -} - -var unlock operations.Operation = &Unlock{} - -func init() { - err := operations.Register(strings.ToLower(string(util.UnlockOperation)), unlock) - if err != nil { - panic(err.Error()) - } -} - -func (s *Unlock) Init(ctx context.Context) error { - actionJSON := viper.GetString(constant.KBEnvActionCommands) - if actionJSON != "" { - actionCommands := map[string][]string{} - err := json.Unmarshal([]byte(actionJSON), &actionCommands) - if err != nil { - s.logger.Info("get action commands failed", "error", err.Error()) - return err - } - readWriteCmd, ok := actionCommands[constant.ReadWriteAction] - if ok && len(readWriteCmd) > 0 { - s.Command = readWriteCmd - } - } - return nil -} - -func (s *Unlock) Do(ctx context.Context, req *operations.OpsRequest) (*operations.OpsResponse, error) { - manager, err := register.GetDBManager(s.Command) - if err != nil { - return nil, errors.Wrap(err, "Get DB manager failed") - } - - err = manager.Unlock(ctx) - if err != nil { - return nil, errors.Wrap(err, "Unlock DB failed") - } - - return nil, nil -} diff --git a/pkg/lorry/probe.proto b/pkg/lorry/probe.proto deleted file mode 100644 index 12e3267abf9..00000000000 --- a/pkg/lorry/probe.proto +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -syntax = "proto3"; - -package probe.proto.v1; - -// Probe service provides APIs to kubeblocks operator to exec component operation. -service Probe { - // ProbeComponent take a specify operation to a component - rpc ProbeComponent(ProbeRequest) returns (stream ProbeResponse) {} -} - -// ProbeRequest is the message to send data to output bindings -message ProbeRequest { - // The name of the output binding to invoke. - string name = 1; - - // The data which will be sent to output binding. - bytes data = 2; - - // The metadata passing to output binding components - // - // Common metadata property: - // - ttlInSeconds : the time to live in seconds for the message. - // If set in the binding definition will cause all messages to - // have a default time to live. The message ttl overrides any value - // in the binding definition. - map metadata = 3; - - // The name of the operation type for the binding to invoke - string operation = 4; -} - -// ProbeResponse is the message returned from an output binding invocation -message ProbeResponse { - // The data which will be sent to output binding. - bytes data = 1; - - // The metadata returned from an external system - map metadata = 2; -} diff --git a/pkg/lorry/util/command.go b/pkg/lorry/util/command.go deleted file mode 100644 index bc98329e142..00000000000 --- a/pkg/lorry/util/command.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package util - -import ( - "context" - "os" - "os/exec" - "strings" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/apecloud/kubeblocks/pkg/constant" -) - -func ExecCommand(ctx context.Context, command []string, envs []string) (string, error) { - if len(command) == 0 { - return "", errors.New("command can not be empty") - } - cmd := exec.CommandContext(ctx, command[0], command[1:]...) - cmd.Env = envs - bytes, err := cmd.Output() - if exitErr, ok := err.(*exec.ExitError); ok { - err = errors.New(string(exitErr.Stderr)) - } - return string(bytes), err -} - -func GetGlobalSharedEnvs() ([]string, error) { - envSetRequired := sets.New( - constant.KBEnvPodFQDN, - constant.KBEnvServicePort, - constant.KBEnvServiceUser, - constant.KBEnvServicePassword, - ) - envSetGot := sets.KeySet(map[string]string{}) - envs := make([]string, 0, envSetRequired.Len()) - Es := os.Environ() - for _, env := range Es { - keys := strings.SplitN(env, "=", 2) - if len(keys) != 2 { - continue - } - if envSetRequired.Has(keys[0]) { - envs = append(envs, env) - envSetGot.Insert(keys[0]) - } - } - // if len(envs) != envSetRequired.Len() { - // return nil, errors.Errorf("%v envs is unset", sets.List(envSetRequired.Difference(envSetGot))) - // } - - return envs, nil -} diff --git a/pkg/lorry/util/config/decode.go b/pkg/lorry/util/config/decode.go deleted file mode 100644 index 864ad2e1c51..00000000000 --- a/pkg/lorry/util/config/decode.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package config - -import ( - "fmt" - "reflect" - "strconv" - "time" - - "github.com/mitchellh/mapstructure" -) - -var ( - typeDuration = reflect.TypeOf(time.Duration(5)) //nolint: gochecknoglobals - typeTime = reflect.TypeOf(time.Time{}) //nolint: gochecknoglobals - typeStringDecoder = reflect.TypeOf((*StringDecoder)(nil)).Elem() //nolint: gochecknoglobals -) - -// StringDecoder is used as a way for custom types (or alias types) to -// override the basic decoding function in the `decodeString` -// DecodeHook. `encoding.TextMashaller` was not used because it -// matches many Go types and would have potentially unexpected results. -// Specifying a custom decoding func should be very intentional. -type StringDecoder interface { - DecodeString(value string) error -} - -// Decode decodes generic map values from `input` to `output`, while providing helpful error information. -// `output` must be a pointer to a Go struct that contains `mapstructure` struct tags on fields that should -// be decoded. This function is useful when decoding values from configuration files parsed as -// `map[string]interface{}` or component metadata as `map[string]string`. -// -// Most of the heavy lifting is handled by the mapstructure library. A custom decoder is used to handle -// decoding string values to the supported primitives. -func Decode(input interface{}, output interface{}) error { - decoder, err := mapstructure.NewDecoder( - &mapstructure.DecoderConfig{ //nolint: exhaustruct - Result: output, - DecodeHook: decodeString, - }) - if err != nil { - return err - } - - return decoder.Decode(input) -} - -//nolint:cyclop -func decodeString(f reflect.Type, t reflect.Type, data any) (any, error) { - if t.Kind() == reflect.String && f.Kind() != reflect.String { - return fmt.Sprintf("%v", data), nil - } - if f.Kind() == reflect.Ptr { - f = f.Elem() - data = reflect.ValueOf(data).Elem().Interface() - } - if f.Kind() != reflect.String { - return data, nil - } - - dataString, ok := data.(string) - if !ok { - return nil, fmt.Errorf("expected string: got %T", data) - } - - var result any - var decoder StringDecoder - - if t.Implements(typeStringDecoder) { - result = reflect.New(t.Elem()).Interface() - decoder = result.(StringDecoder) - } else if reflect.PtrTo(t).Implements(typeStringDecoder) { - result = reflect.New(t).Interface() - decoder = result.(StringDecoder) - } - - if decoder != nil { - if err := decoder.DecodeString(dataString); err != nil { - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - return nil, fmt.Errorf("invalid %s %q: %v", t.Name(), dataString, err) - } - - return result, nil - } - - switch t { - case typeDuration: - // Check for simple integer values and treat them - // as milliseconds - if val, err := strconv.Atoi(dataString); err == nil { - return time.Duration(val) * time.Millisecond, nil - } - - // Convert it by parsing - d, err := time.ParseDuration(dataString) - - return d, invalidError(err, "duration", dataString) - case typeTime: - // Convert it by parsing - t, err := time.Parse(time.RFC3339Nano, dataString) - if err == nil { - return t, nil - } - t, err = time.Parse(time.RFC3339, dataString) - - return t, invalidError(err, "time", dataString) - } - - switch t.Kind() { - case reflect.Uint: - val, err := strconv.ParseUint(dataString, 10, 32) - - return uint(val), invalidError(err, "uint", dataString) - case reflect.Uint64: - val, err := strconv.ParseUint(dataString, 10, 64) - - return val, invalidError(err, "uint64", dataString) - case reflect.Uint32: - val, err := strconv.ParseUint(dataString, 10, 32) - - return uint32(val), invalidError(err, "uint32", dataString) - case reflect.Uint16: - val, err := strconv.ParseUint(dataString, 10, 16) - - return uint16(val), invalidError(err, "uint16", dataString) - case reflect.Uint8: - val, err := strconv.ParseUint(dataString, 10, 8) - - return uint8(val), invalidError(err, "uint8", dataString) - - case reflect.Int: - val, err := strconv.Atoi(dataString) - - return val, invalidError(err, "int", dataString) - case reflect.Int64: - val, err := strconv.ParseInt(dataString, 10, 64) - - return val, invalidError(err, "int64", dataString) - case reflect.Int32: - val, err := strconv.ParseInt(dataString, 10, 32) - - return int32(val), invalidError(err, "int32", dataString) - case reflect.Int16: - val, err := strconv.ParseInt(dataString, 10, 16) - - return int16(val), invalidError(err, "int16", dataString) - case reflect.Int8: - val, err := strconv.ParseInt(dataString, 10, 8) - - return int8(val), invalidError(err, "int8", dataString) - - case reflect.Float32: - val, err := strconv.ParseFloat(dataString, 32) - - return float32(val), invalidError(err, "float32", dataString) - case reflect.Float64: - val, err := strconv.ParseFloat(dataString, 64) - - return val, invalidError(err, "float64", dataString) - - case reflect.Bool: - val, err := strconv.ParseBool(dataString) - - return val, invalidError(err, "bool", dataString) - - default: - return data, nil - } -} - -func invalidError(err error, msg, value string) error { - if err == nil { - return nil - } - - return fmt.Errorf("invalid %s %q", msg, value) -} diff --git a/pkg/lorry/util/config/deepcopy.go b/pkg/lorry/util/config/deepcopy.go deleted file mode 100644 index fff953953d4..00000000000 --- a/pkg/lorry/util/config/deepcopy.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package config - -import ( - "errors" - "reflect" -) - -func Clone(s any) (any, error) { - sValue := reflect.Indirect(reflect.ValueOf(s)) - sType := sValue.Type() - d := reflect.New(sType).Interface() - err := DeepCopy(s, d) - if err != nil { - return nil, err - } - return d, nil -} - -// DeepCopy make a compele copy for a struct value -func DeepCopy(s, d any) error { - sValue := reflect.Indirect(reflect.ValueOf(s)) - sType := sValue.Type() - - dType := reflect.TypeOf(d) - dValue := reflect.Indirect(reflect.ValueOf(d)) - if dType.Kind() != reflect.Pointer { - return errors.New("dest object must be an Pointer") - } - dType = dType.Elem() - - if sType != dType { - return errors.New("source and dest object type is not match") - } - - if sType.Kind() != reflect.Struct { - return errors.New("object type is not struct") - } - - return deepCopy(sValue, dValue) -} - -func deepCopy(s, d reflect.Value) error { - kind := d.Kind() - var err error - switch kind { - case reflect.Struct: - err = deepCopyStruct(s, d) - case reflect.Slice: - err = deepCopySlice(s, d) - case reflect.Map: - err = deepCopyMap(s, d) - case reflect.String: - err = deepCopyString(s, d) - case reflect.Pointer: - err = deepCopyPointer(s, d) - // case reflect.Func: - // break - default: - d.Set(s) - } - - return err -} - -func deepCopyStruct(s, d reflect.Value) error { - // var structs = make([]any, 1, 5) - // structs[0] = d - // type field struct { - // field reflect.StructField - // val reflect.Value - // } - // - // fields := []field{} - // - // for len(structs) > 0 { - // structData := structs[0] - // structs = structs[1:] - dValue := reflect.Indirect(d) - sValue := reflect.Indirect(s) - dType := dValue.Type() - - for i := 0; i < dType.NumField(); i++ { - dfieldType := dType.Field(i) - if !dfieldType.IsExported() { - continue - } - - dfieldValue := dValue.Field(i) - sfieldValue := sValue.Field(i) - err := deepCopy(sfieldValue, dfieldValue) - if err != nil { - return err - } - } - - return nil -} - -func deepCopyString(s, d reflect.Value) error { - d.SetString(s.String()) - return nil -} - -func deepCopyPointer(s, d reflect.Value) error { - if s.IsNil() { - return nil - } - sValue := reflect.Indirect(s) - dType := sValue.Type() - newValue := reflect.New(dType) - - err := deepCopy(sValue, reflect.Indirect(newValue)) - if err != nil { - return err - } - d.Set(newValue) - - return nil -} - -func deepCopySlice(s, d reflect.Value) error { - dType := d.Type() - valElemType := dType.Elem() - sliceType := reflect.SliceOf(valElemType) - sliceVar := reflect.MakeSlice(sliceType, s.Len(), s.Len()) - for i := 0; i < s.Len(); i++ { - err := deepCopy(s.Index(i), sliceVar.Index(i)) - if err != nil { - return err - } - } - d.Set(sliceVar) - return nil -} - -func deepCopyMap(s, d reflect.Value) error { - valType := d.Type() - valKeyType := valType.Key() - valElemType := valType.Elem() - mapType := reflect.MapOf(valKeyType, valElemType) - valMap := reflect.MakeMap(mapType) - for _, k := range s.MapKeys() { - currentKey := reflect.Indirect(reflect.New(valKeyType)) - err := deepCopy(k, currentKey) - if err != nil { - return err - } - currentElem := reflect.Indirect(reflect.New(valElemType)) - err = deepCopy(s.MapIndex(k), currentElem) - if err != nil { - return err - } - valMap.SetMapIndex(currentKey, currentElem) - } - d.Set(valMap) - return nil -} diff --git a/pkg/lorry/util/config/deepcopy_test.go b/pkg/lorry/util/config/deepcopy_test.go deleted file mode 100644 index 8d37f886b52..00000000000 --- a/pkg/lorry/util/config/deepcopy_test.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package config - -import ( - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -type stringStruct struct { - Key string - Value string -} - -type sliceStruct struct { - Key string - A []int -} - -type mapStruct struct { - Key string - M map[string]string -} - -type privateStruct struct { - Key string - value string -} - -type pointerStruct struct { - Key string - P *stringStruct -} - -func Print(a any) { - v := reflect.ValueOf(a) - fmt.Print(v.Kind()) - b := v.Interface() - c, ok := b.(*stringStruct) - if ok { - c.Key = "key2" - fmt.Printf("Key: %v\n", v) - fmt.Printf("Key: %v\n", c) - } - -} - -func TestClone(t *testing.T) { - s := &stringStruct{ - Key: "key1", - Value: "values", - } - dd, _ := Clone(s) - d := dd.(*stringStruct) - fmt.Printf("s: %v\n", s) - fmt.Printf("d: %v\n", d) - assert.Equal(t, s, d) - d.Value = "values1" - assert.NotEqual(t, s, d) -} - -func TestDeepCopy(t *testing.T) { - t.Run("test struct with String", func(t *testing.T) { - s := &stringStruct{ - Key: "key1", - Value: "values", - } - d := &stringStruct{} - _ = DeepCopy(s, d) - t.Logf("s: %v\n", s) - t.Logf("d: %v\n", d) - assert.Equal(t, s, d) - - a := *s - Print(a) - Print(s) - }) - - t.Run("test struct with alice", func(t *testing.T) { - s := sliceStruct{ - Key: "slice", - A: []int{1, 3}, - } - func(s any) { - sv := reflect.ValueOf(s) - // p1 := unsafe.Pointer(sv.UnsafeAddr()) - // s1 := (*sliceStruct)(p1) - fmt.Printf("sliceStruct s: %v \n", s) - s1 := s.(sliceStruct) - s1.Key = "slice1" - s1.A[1] = 2 - fmt.Printf("sliceStruct s: %v \n", s) - fmt.Printf("sliceStruct s1: %v \n", s1) - - s2 := sv.Interface().(sliceStruct) - s2.A[1] = 4 - s2.Key = "slice2" - fmt.Printf("sliceStruct s: %v \n", s) - fmt.Printf("sliceStruct s2: %v \n", s2) - }(s) - fmt.Printf("sliceStruct s: %v \n", s) - - d := &sliceStruct{} - _ = DeepCopy(&s, d) - fmt.Printf("sliceStruct d: %v \n", d) - assert.Equal(t, *d, s) - d.A[0] = 2 - fmt.Printf("sliceStruct s: %v \n", s) - fmt.Printf("sliceStruct d: %v \n", d) - assert.NotEqual(t, *d, s) - }) - - t.Run("test struct with map", func(t *testing.T) { - s := mapStruct{ - Key: "map1", - M: map[string]string{"key": "value"}, - } - fmt.Printf("s: %v\n", s) - d := &mapStruct{} - _ = DeepCopy(&s, d) - fmt.Printf("d: %v\n", d) - assert.Equal(t, s, *d) - - d.M["key2"] = "value2" - fmt.Printf("s: %v\n", s) - fmt.Printf("d: %v\n", d) - assert.NotEqual(t, s, *d) - }) - - t.Run("test struct with unexported field", func(t *testing.T) { - s := privateStruct{ - Key: "private1", - value: "values1", - } - fmt.Printf("s: %v\n", s) - d := &privateStruct{} - _ = DeepCopy(&s, d) - fmt.Printf("d: %v\n", d) - assert.Equal(t, d.Key, s.Key) - assert.NotEqual(t, d.value, s.value) - - d.Key = "private2" - d.value = "values2" - fmt.Printf("s: %v\n", s) - fmt.Printf("d: %v\n", d) - assert.NotEqual(t, d.Key, s.Key) - assert.NotEqual(t, d.value, s.value) - }) - - t.Run("test struct with pointer", func(t *testing.T) { - s := &stringStruct{ - Key: "private1", - Value: "values1", - } - p := pointerStruct{ - Key: "pointer1", - P: s, - } - - fmt.Printf("p: %v\n", p) - d := &pointerStruct{} - _ = DeepCopy(&p, d) - fmt.Printf("d: %v\n", d) - assert.Equal(t, d.Key, p.Key) - assert.Equal(t, *d.P, *p.P) - - d.P.Key = "pointer2" - d.P.Value = "values2" - fmt.Printf("p: %v\n", p.P) - fmt.Printf("d: %v\n", d.P) - assert.NotEqual(t, d.P.Key, p.P.Key) - assert.NotEqual(t, d.P.Value, p.P.Value) - }) -} diff --git a/pkg/lorry/util/event.go b/pkg/lorry/util/event.go deleted file mode 100644 index 59bb515677e..00000000000 --- a/pkg/lorry/util/event.go +++ /dev/null @@ -1,139 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package util - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "time" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/client-go/kubernetes" - ctlruntime "sigs.k8s.io/controller-runtime" - - workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/constant" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var logger = ctlruntime.Log.WithName("event") - -func SentEventForProbe(ctx context.Context, data map[string]any) error { - logger.Info(fmt.Sprintf("send event: %v", data)) - roleUpdateMechanism := workloads.DirectAPIServerEventUpdate - if viper.IsSet(constant.KBEnvRsmRoleUpdateMechanism) { - roleUpdateMechanism = workloads.RoleUpdateMechanism(viper.GetString(constant.KBEnvRsmRoleUpdateMechanism)) - } - - switch roleUpdateMechanism { - case workloads.ReadinessProbeEventUpdate: - return NewProbeError("not sending event directly, use readiness probe instand") - case workloads.DirectAPIServerEventUpdate: - operation, ok := data["operation"] - if !ok { - return errors.New("operation failed must be set") - } - event, err := CreateEvent(string(operation.(OperationKind)), data) - if err != nil { - logger.Info("generate event failed", "error", err.Error()) - return err - } - - go func() { - _ = SendEvent(ctx, event) - }() - default: - logger.Info(fmt.Sprintf("no event sent, RoleUpdateMechanism: %s", roleUpdateMechanism)) - } - - return nil -} - -func CreateEvent(reason string, data map[string]any) (*corev1.Event, error) { - // get pod object - podName := viper.GetString(constant.KBEnvPodName) - podUID := viper.GetString(constant.KBEnvPodUID) - nodeName := viper.GetString(constant.KBEnvNodeName) - namespace := viper.GetString(constant.KBEnvNamespace) - msg, err := json.Marshal(data) - if err != nil { - return nil, err - } - - event := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s.%s", podName, rand.String(16)), - Namespace: namespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Pod", - Namespace: namespace, - Name: podName, - UID: types.UID(podUID), - FieldPath: "spec.containers{lorry}", - }, - Reason: reason, - Message: string(msg), - Source: corev1.EventSource{ - Component: "lorry", - Host: nodeName, - }, - FirstTimestamp: metav1.Now(), - LastTimestamp: metav1.Now(), - EventTime: metav1.NowMicro(), - ReportingController: "lorry", - ReportingInstance: podName, - Action: reason, - Type: "Normal", - } - return event, nil -} - -func SendEvent(ctx context.Context, event *corev1.Event) error { - ctx1 := context.Background() - config, err := ctlruntime.GetConfig() - if err != nil { - logger.Info("get k8s client config failed", "error", err.Error()) - return err - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - logger.Info("k8s client create failed", "error", err.Error()) - return err - } - namespace := os.Getenv(constant.KBEnvNamespace) - for i := 0; i < 30; i++ { - _, err = clientset.CoreV1().Events(namespace).Create(ctx1, event, metav1.CreateOptions{}) - if err == nil { - logger.Info("send event success", "message", event.Message) - break - } - logger.Info("send event failed", "error", err.Error()) - time.Sleep(10 * time.Second) - } - return err -} diff --git a/pkg/lorry/util/kubernetes/client.go b/pkg/lorry/util/kubernetes/client.go deleted file mode 100644 index 436df5fd3ad..00000000000 --- a/pkg/lorry/util/kubernetes/client.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package kubernetes - -import ( - "github.com/pkg/errors" - "k8s.io/client-go/kubernetes" - clientsetscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - ctlruntime "sigs.k8s.io/controller-runtime" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" -) - -// GetClientSet returns a kubernetes clientSet. -func GetClientSet() (*kubernetes.Clientset, error) { - restConfig, err := ctlruntime.GetConfig() - if err != nil { - return nil, errors.Wrap(err, "get kubeConfig failed") - } - clientSet, err := kubernetes.NewForConfig(restConfig) - if err != nil { - return nil, err - } - - return clientSet, nil -} - -// GetRESTClientForKB returns a kubernetes restClient for KubeBlocks types. -func GetRESTClientForKB() (*rest.RESTClient, error) { - restConfig, err := ctlruntime.GetConfig() - if err != nil { - return nil, errors.Wrap(err, "get kubeConfig failed") - } - _ = appsv1alpha1.AddToScheme(clientsetscheme.Scheme) - restConfig.GroupVersion = &appsv1alpha1.GroupVersion - restConfig.APIPath = "/apis" - restConfig.NegotiatedSerializer = clientsetscheme.Codecs.WithoutConversion() - client, err := rest.RESTClientFor(restConfig) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/pkg/lorry/util/ping.go b/pkg/lorry/util/ping.go deleted file mode 100644 index cdcf2ea9162..00000000000 --- a/pkg/lorry/util/ping.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package util - -import ( - "net" - "time" - - ctlruntime "sigs.k8s.io/controller-runtime" - - "github.com/apecloud/kubeblocks/pkg/constant" - viper "github.com/apecloud/kubeblocks/pkg/viperx" -) - -var pingerLogger = ctlruntime.Log.WithName("pinger") - -// IsDNSReady checks if dns and ip is ready, it can successfully resolve dns. -// Since the vast majority of container runtimes currently prohibit the NET_RAW capability, -// we can't rely on ICMP protocol to detect DNS resolution. -// Instead, we directly depend on TCP for port detection. -func IsDNSReady(dns string) (bool, error) { - // get the port where the Lorry HTTP service is listening - port := viper.GetString("port") - return IsTCPReady(dns, port) -} - -func IsTCPReady(host, port string) (bool, error) { - address := net.JoinHostPort(host, port) - timeout := 2 * time.Second - conn, err := net.DialTimeout("tcp", address, timeout) - if err != nil { - return false, err - } - defer func() { - _ = conn.Close() - }() - - return true, nil -} - -func CheckDNSReadyWithRetry(dns string, times int) (bool, error) { - var ready bool - var err error - for i := 0; i < times; i++ { - ready, err = IsDNSReady(dns) - if err == nil && ready { - return true, nil - } - } - pingerLogger.Info("dns resolution is ready", "dns", dns) - return ready, err -} - -// WaitForDNSReady checks if dns is ready -func WaitForDNSReady(dns string) { - pingerLogger.Info("Waiting for dns resolution to be ready") - isPodReady, err := IsDNSReady(dns) - for err != nil || !isPodReady { - if err != nil { - pingerLogger.Info("dns check failed", "error", err.Error()) - } - time.Sleep(3 * time.Second) - isPodReady, err = IsDNSReady(dns) - } - pingerLogger.Info("dns resolution is ready", "dns", dns) -} - -// WaitForPodReady checks if pod is ready -func WaitForPodReady(checkPodHeadless bool) { - domain := viper.GetString(constant.KBEnvPodFQDN) - WaitForDNSReady(domain) - if checkPodHeadless { - domain := viper.GetString(constant.KBEnvPodName) - WaitForDNSReady(domain) - } -} diff --git a/pkg/lorry/util/types.go b/pkg/lorry/util/types.go deleted file mode 100644 index 9f3bc30a054..00000000000 --- a/pkg/lorry/util/types.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package util - -import ( - "errors" - "strings" -) - -type OperationKind string - -const ( - RespFieldEvent = "event" - RespFieldMessage = "message" - RespTypMeta = "metadata" - RespEveSucc = "Success" - RespEveFail = "Failed" - - GetOperation OperationKind = "get" - CreateOperation OperationKind = "create" - DeleteOperation OperationKind = "delete" - ListOperation OperationKind = "list" - - CheckRunningOperation OperationKind = "checkRunning" - HealthyCheckOperation OperationKind = "healthyCheck" - CheckRoleOperation OperationKind = "checkRole" - GetRoleOperation OperationKind = "getRole" - GetLagOperation OperationKind = "getLag" - SwitchoverOperation OperationKind = "switchover" - ExecOperation OperationKind = "exec" - QueryOperation OperationKind = "query" - CloseOperation OperationKind = "close" - - LockOperation OperationKind = "lockInstance" - UnlockOperation OperationKind = "unlockInstance" - VolumeProtection OperationKind = "volumeProtection" - - // for component - PostProvisionOperation OperationKind = "postProvision" - PreTerminateOperation OperationKind = "preTerminate" - - // actions for cluster accounts management - ListUsersOp OperationKind = "listUsers" - CreateUserOp OperationKind = "createUser" - DeleteUserOp OperationKind = "deleteUser" - DescribeUserOp OperationKind = "describeUser" - GrantUserRoleOp OperationKind = "grantUserRole" - RevokeUserRoleOp OperationKind = "revokeUserRole" - ListSystemAccountsOp OperationKind = "listSystemAccounts" - - JoinMemberOperation OperationKind = "joinMember" - LeaveMemberOperation OperationKind = "leaveMember" - - OperationNotImplemented = "NotImplemented" - OperationInvalid = "Invalid" - OperationSuccess = "Success" - OperationFailed = "Failed" - DefaultProbeTimeoutSeconds = 2 - - DataDumpOperation OperationKind = "dataDump" - DataLoadOperation OperationKind = "dataLoad" - - LegacyEventFieldPath = "spec.containers{kb-checkrole}" - LorryEventFieldPath = "spec.containers{lorry}" - - // this is a general script template, which can be used for all kinds of exec request to databases. - DataScriptRequestTpl string = ` - response=$(curl -s -X POST -H 'Content-Type: application/json' http://%s:3501/v1.0/bindings/%s -d '%s') - result=$(echo $response | jq -r '.event') - message=$(echo $response | jq -r '.message') - if [ "$result" == "Failed" ]; then - echo $message - exit 1 - else - echo "$result" - exit 0 - fi - ` -) - -type RoleType string - -func (r RoleType) EqualTo(role string) bool { - return strings.EqualFold(string(r), role) -} - -func (r RoleType) GetWeight() int32 { - switch r { - case SuperUserRole: - return 1 << 3 - case ReadWriteRole: - return 1 << 2 - case ReadOnlyRole: - return 1 << 1 - case CustomizedRole: - return 1 - default: - return 0 - } -} - -const ( - SuperUserRole RoleType = "superuser" - ReadWriteRole RoleType = "readwrite" - ReadOnlyRole RoleType = "readonly" - NoPrivileges RoleType = "" - CustomizedRole RoleType = "customized" - InvalidRole RoleType = "invalid" -) - -// ProbeError is the error for Lorry probe api, it implements error interface -type ProbeError struct { - message string -} - -var _ error = ProbeError{} - -func (e ProbeError) Error() string { - return e.message -} - -func NewProbeError(msg string) error { - return ProbeError{ - message: msg, - } -} - -var ErrNotImplemented = errors.New("not implemented") diff --git a/pkg/lorry/vault/database.go b/pkg/lorry/vault/database.go deleted file mode 100644 index 6f56f736484..00000000000 --- a/pkg/lorry/vault/database.go +++ /dev/null @@ -1,168 +0,0 @@ -/* -Copyright (C) 2022-2024 ApeCloud Co., Ltd - -This file is part of KubeBlocks project - -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 . -*/ - -package vault - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - "time" - - "github.com/hashicorp/go-hclog" - dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5" - "github.com/hashicorp/vault/sdk/database/helper/credsutil" - - "github.com/apecloud/kubeblocks/pkg/lorry/client" -) - -const ( - lorryTypeName = "lorry" - defaultLorryUserRule = "readonly" - defaultTimeout = 20000 * time.Millisecond - maxKeyLength = 32 -) - -var _ dbplugin.Database = &LorryDB{} - -type LorryDB struct { - credsutil.CredentialsProducer - lorryClient client.Client - logger hclog.Logger - config map[string]any - sync.Mutex -} - -// New implements builtinplugins.BuiltinFactory -func New() (interface{}, error) { - db := new() - // Wrap the plugin with middleware to sanitize errors - dbType := dbplugin.NewDatabaseErrorSanitizerMiddleware(db, nil) - return dbType, nil -} - -func new() *LorryDB { - db := &LorryDB{ - logger: hclog.New(&hclog.LoggerOptions{}), - } - - return db -} - -func (db *LorryDB) Initialize(ctx context.Context, req dbplugin.InitializeRequest) (dbplugin.InitializeResponse, error) { - db.logger.Info("initialize", "config", req.Config) - resp := dbplugin.InitializeResponse{ - Config: req.Config, - } - lorryURL, ok := req.Config["url"] - if !ok { - msg := "lorry url is not set" - db.logger.Info(msg) - return resp, errors.New(msg) - } - - lorryClient, err := client.NewHTTPClientWithURL(lorryURL.(string)) - if err != nil { - db.logger.Info("new lorry http client failed", "error", err) - return resp, err - } - db.lorryClient = lorryClient - db.config = req.Config - - return resp, nil -} - -func (db *LorryDB) NewUser(ctx context.Context, req dbplugin.NewUserRequest) (dbplugin.NewUserResponse, error) { - db.logger.Info("new user", "req", req) - // Grab the lock - db.Lock() - defer db.Unlock() - - username, err := credsutil.GenerateUsername( - credsutil.DisplayName(req.UsernameConfig.DisplayName, maxKeyLength), - credsutil.RoleName(req.UsernameConfig.RoleName, maxKeyLength), - credsutil.MaxLength(maxKeyLength)) - if err != nil { - return dbplugin.NewUserResponse{}, fmt.Errorf("failed to generate username: %w", err) - } - username = strings.ToUpper(username) - password := req.Password - - statements := removeEmpty(req.Statements.Commands) - accessMode := defaultLorryUserRule - if len(statements) > 0 { - accessMode = statements[0] - } - - err = db.lorryClient.CreateUser(ctx, username, password, accessMode, "") - if err != nil { - db.logger.Info("create user failed", "error", err) - return dbplugin.NewUserResponse{}, err - } - - resp := dbplugin.NewUserResponse{ - Username: username, - } - - return resp, nil -} - -func (db *LorryDB) UpdateUser(ctx context.Context, req dbplugin.UpdateUserRequest) (dbplugin.UpdateUserResponse, error) { - if req.Password != nil { - err := errors.New("not support yet") - return dbplugin.UpdateUserResponse{}, err - } - return dbplugin.UpdateUserResponse{}, nil -} - -func (db *LorryDB) DeleteUser(ctx context.Context, req dbplugin.DeleteUserRequest) (dbplugin.DeleteUserResponse, error) { - db.Lock() - defer db.Unlock() - - err := db.lorryClient.DeleteUser(ctx, req.Username) - - if err != nil { - return dbplugin.DeleteUserResponse{}, err - } - - return dbplugin.DeleteUserResponse{}, nil -} - -func (db *LorryDB) Type() (string, error) { - return lorryTypeName, nil -} - -func (db *LorryDB) Close() error { - return nil -} - -func removeEmpty(strs []string) []string { - var newStrs []string - for _, str := range strs { - str = strings.TrimSpace(str) - if str == "" { - continue - } - newStrs = append(newStrs, str) - } - - return newStrs -} diff --git a/pkg/testutil/apps/constant.go b/pkg/testutil/apps/constant.go index 251b7ce8fc1..e27e501b25a 100644 --- a/pkg/testutil/apps/constant.go +++ b/pkg/testutil/apps/constant.go @@ -43,8 +43,7 @@ const ( ApeCloudMySQLImage = "docker.io/apecloud/apecloud-mysql-server:latest" DefaultMySQLContainerName = "mysql" - NginxImage = "nginx" - DefaultNginxContainerName = "nginx" + NginxImage = "nginx" DefaultConfigSpecName = "config-cm" DefaultConfigSpecTplRef = "env-from-config-tpl" @@ -56,16 +55,10 @@ const ( ) var ( - defaultBuiltinHandler = appsv1alpha1.MySQLBuiltinActionHandler - defaultLifecycleActionHandler = &appsv1alpha1.LifecycleActionHandler{ - BuiltinHandler: &defaultBuiltinHandler, - } - newLifecycleActionHandler = func(name string) *appsv1alpha1.LifecycleActionHandler { - return &appsv1alpha1.LifecycleActionHandler{ - CustomHandler: &appsv1alpha1.Action{ - Exec: &appsv1alpha1.ExecAction{ - Command: []string{"/bin/sh", "-c", fmt.Sprintf("echo %s", name)}, - }, + NewLifecycleAction = func(name string) *appsv1alpha1.Action { + return &appsv1alpha1.Action{ + Exec: &appsv1alpha1.ExecAction{ + Command: []string{"/bin/sh", "-c", fmt.Sprintf("echo %s", name)}, }, } } @@ -239,24 +232,21 @@ var ( ScrapeScheme: appsv1alpha1.HTTPProtocol, }, LifecycleActions: &appsv1alpha1.ComponentLifecycleActions{ - PostProvision: defaultLifecycleActionHandler, - PreTerminate: defaultLifecycleActionHandler, + PostProvision: nil, + PreTerminate: nil, RoleProbe: &appsv1alpha1.Probe{ - BuiltinHandler: defaultLifecycleActionHandler.BuiltinHandler, - Action: appsv1alpha1.Action{ - TimeoutSeconds: 5, - }, + Action: *NewLifecycleAction("role-probe"), PeriodSeconds: 1, }, Switchover: nil, - MemberJoin: defaultLifecycleActionHandler, - MemberLeave: newLifecycleActionHandler("member-leave"), - Readonly: defaultLifecycleActionHandler, - Readwrite: defaultLifecycleActionHandler, - DataDump: defaultLifecycleActionHandler, - DataLoad: defaultLifecycleActionHandler, - Reconfigure: defaultLifecycleActionHandler, - AccountProvision: newLifecycleActionHandler("account-provision"), + MemberJoin: nil, + MemberLeave: NewLifecycleAction("member-leave"), + Readonly: nil, + Readwrite: nil, + DataDump: nil, + DataLoad: nil, + Reconfigure: nil, + AccountProvision: NewLifecycleAction("account-provision"), }, }