From 42bd823b3cebb031976fd531ce960e8e0b9a465c Mon Sep 17 00:00:00 2001 From: powerfool Date: Sun, 29 Sep 2024 10:58:24 +0800 Subject: [PATCH 1/6] Fixed unexpected behaviors in webhooks (#580) --- .github/workflows/ci-tests.yml | 41 +++++++ api/v1alpha1/obcluster_types.go | 4 +- api/v1alpha1/obcluster_webhook_test.go | 11 +- api/v1alpha1/obtenantbackuppolicy_webhook.go | 72 +++++++------ api/v1alpha1/obtenantoperation_webhook.go | 27 +++-- .../obtenantoperation_webhook_test.go | 100 +++++++++++++++++- api/v1alpha1/webhook_suite_test.go | 6 +- make/deps.mk | 2 +- make/development.mk | 5 + 9 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 .github/workflows/ci-tests.yml diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 000000000..e756d0ae0 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,41 @@ +name: run tests + +on: + pull_request: + branches: + - master + - "*_release" + paths: + - '**/*.go' + push: + branches: + - master + - "*_release" + paths: + - '**/*.go' + +env: + GO_VERSION: "1.22" + +jobs: + run-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - name: go mod vendor + run: go mod vendor + + - name: install tools + run: make tools + + - name: test webhooks + run: make test-webhooks + \ No newline at end of file diff --git a/api/v1alpha1/obcluster_types.go b/api/v1alpha1/obcluster_types.go index 7c844c56e..2f29d3a73 100644 --- a/api/v1alpha1/obcluster_types.go +++ b/api/v1alpha1/obcluster_types.go @@ -88,5 +88,7 @@ func init() { } func (c *OBCluster) SupportStaticIP() bool { - return c.Annotations[oceanbaseconst.AnnotationsSupportStaticIP] == "true" + return c.Annotations[oceanbaseconst.AnnotationsSupportStaticIP] == "true" || + c.Annotations[oceanbaseconst.AnnotationsMode] == oceanbaseconst.ModeService || + c.Annotations[oceanbaseconst.AnnotationsMode] == oceanbaseconst.ModeStandalone } diff --git a/api/v1alpha1/obcluster_webhook_test.go b/api/v1alpha1/obcluster_webhook_test.go index 97b80b436..58cef98f5 100644 --- a/api/v1alpha1/obcluster_webhook_test.go +++ b/api/v1alpha1/obcluster_webhook_test.go @@ -148,7 +148,7 @@ var _ = Describe("Test OBCluster Webhook", Label("webhook"), func() { It("Validate existence of secrets", func() { By("Create normal cluster") - cluster := newOBCluster("test", 1, 1) + cluster := newOBCluster("test3", 1, 1) cluster.Spec.UserSecrets.Monitor = "" cluster.Spec.UserSecrets.ProxyRO = "" cluster.Spec.UserSecrets.Operator = "" @@ -158,17 +158,19 @@ var _ = Describe("Test OBCluster Webhook", Label("webhook"), func() { cluster2.Spec.UserSecrets.Monitor = "secret-that-does-not-exist" cluster2.Spec.UserSecrets.ProxyRO = "" cluster2.Spec.UserSecrets.Operator = "" - Expect(k8sClient.Create(ctx, cluster)).ShouldNot(Succeed()) + Expect(k8sClient.Create(ctx, cluster2)).Should(Succeed()) + cluster3 := newOBCluster("test3", 1, 1) cluster2.Spec.UserSecrets.Monitor = wrongKeySecret - Expect(k8sClient.Create(ctx, cluster)).ShouldNot(Succeed()) + Expect(k8sClient.Create(ctx, cluster3)).ShouldNot(Succeed()) Expect(k8sClient.Delete(ctx, cluster)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, cluster2)).Should(Succeed()) }) It("Validate secrets creation and fetch them", func() { By("Create normal cluster") - cluster := newOBCluster("test", 1, 1) + cluster := newOBCluster("test-create-secrets", 1, 1) cluster.Spec.UserSecrets.Monitor = "" cluster.Spec.UserSecrets.ProxyRO = "" cluster.Spec.UserSecrets.Operator = "" @@ -178,6 +180,7 @@ var _ = Describe("Test OBCluster Webhook", Label("webhook"), func() { Expect(cluster.Spec.UserSecrets.Monitor).ShouldNot(BeEmpty()) Expect(cluster.Spec.UserSecrets.ProxyRO).ShouldNot(BeEmpty()) Expect(cluster.Spec.UserSecrets.Operator).ShouldNot(BeEmpty()) + Expect(k8sClient.Delete(ctx, cluster)).Should(Succeed()) }) It("Validate single pvc with multiple storage classes", func() { diff --git a/api/v1alpha1/obtenantbackuppolicy_webhook.go b/api/v1alpha1/obtenantbackuppolicy_webhook.go index 66ac4f818..4ae6a9c27 100644 --- a/api/v1alpha1/obtenantbackuppolicy_webhook.go +++ b/api/v1alpha1/obtenantbackuppolicy_webhook.go @@ -304,44 +304,46 @@ func (r *OBTenantBackupPolicy) validateDestination(cluster *OBCluster, dest *api if !pattern.MatchString(dest.Path) { return field.Invalid(field.NewPath("spec").Child(fieldName).Child("destination"), dest.Path, "invalid backup destination path, the path format should be "+pattern.String()) } - if dest.Type != constants.BackupDestTypeNFS && dest.OSSAccessSecret == "" { - return field.Invalid(field.NewPath("spec").Child(fieldName).Child("destination"), dest.OSSAccessSecret, "OSSAccessSecret is required when backing up data to OSS, COS or S3") - } - secret := &v1.Secret{} - err := bakClt.Get(context.Background(), types.NamespacedName{ - Namespace: r.GetNamespace(), - Name: dest.OSSAccessSecret, - }, secret) - fieldPath := field.NewPath("spec").Child(fieldName).Child("destination").Child("ossAccessSecret") - if err != nil { - if apierrors.IsNotFound(err) { - return field.Invalid(fieldPath, dest.OSSAccessSecret, "Given OSSAccessSecret not found") - } - return field.InternalError(fieldPath, err) - } - // All the following types need accessId and accessKey - switch dest.Type { - case - constants.BackupDestTypeCOS, - constants.BackupDestTypeOSS, - constants.BackupDestTypeS3, - constants.BackupDestTypeS3Compatible: - if _, ok := secret.Data["accessId"]; !ok { - return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessId field not found in given OSSAccessSecret") + if dest.Type != constants.BackupDestTypeNFS { + if dest.OSSAccessSecret == "" { + return field.Invalid(field.NewPath("spec").Child(fieldName).Child("destination"), dest.OSSAccessSecret, "OSSAccessSecret is required when backing up data to OSS, COS or S3") } - if _, ok := secret.Data["accessKey"]; !ok { - return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessKey field not found in given OSSAccessSecret") + secret := &v1.Secret{} + err := bakClt.Get(context.Background(), types.NamespacedName{ + Namespace: r.GetNamespace(), + Name: dest.OSSAccessSecret, + }, secret) + fieldPath := field.NewPath("spec").Child(fieldName).Child("destination").Child("ossAccessSecret") + if err != nil { + if apierrors.IsNotFound(err) { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "Given OSSAccessSecret not found") + } + return field.InternalError(fieldPath, err) } - } - // The following types need additional fields - switch dest.Type { - case constants.BackupDestTypeCOS: - if _, ok := secret.Data["appId"]; !ok { - return field.Invalid(fieldPath, dest.OSSAccessSecret, "appId field not found in given OSSAccessSecret") + // All the following types need accessId and accessKey + switch dest.Type { + case + constants.BackupDestTypeCOS, + constants.BackupDestTypeOSS, + constants.BackupDestTypeS3, + constants.BackupDestTypeS3Compatible: + if _, ok := secret.Data["accessId"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessId field not found in given OSSAccessSecret") + } + if _, ok := secret.Data["accessKey"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessKey field not found in given OSSAccessSecret") + } } - case constants.BackupDestTypeS3: - if _, ok := secret.Data["s3Region"]; !ok { - return field.Invalid(fieldPath, dest.OSSAccessSecret, "s3Region field not found in given OSSAccessSecret") + // The following types need additional fields + switch dest.Type { + case constants.BackupDestTypeCOS: + if _, ok := secret.Data["appId"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "appId field not found in given OSSAccessSecret") + } + case constants.BackupDestTypeS3: + if _, ok := secret.Data["s3Region"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "s3Region field not found in given OSSAccessSecret") + } } } return nil diff --git a/api/v1alpha1/obtenantoperation_webhook.go b/api/v1alpha1/obtenantoperation_webhook.go index 994551f12..a7e90643c 100644 --- a/api/v1alpha1/obtenantoperation_webhook.go +++ b/api/v1alpha1/obtenantoperation_webhook.go @@ -226,15 +226,23 @@ func (r *OBTenantOperation) validateMutation() error { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("targetTenant"), r.Spec.TargetTenant, "The target tenant is not a standby")) } } + case constants.TenantOpSetUnitNumber, + constants.TenantOpSetConnectWhiteList, + constants.TenantOpAddResourcePools, + constants.TenantOpModifyResourcePools, + constants.TenantOpDeleteResourcePools: + return r.validateNewOperations() default: - if r.Spec.TargetTenant == nil { - allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("targetTenant"), "name of targetTenant is required")) - } - } - if len(allErrs) != 0 { - return allErrs.ToAggregate() + allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("type"), string(r.Spec.Type)+" type of operation is not supported")) } + return allErrs.ToAggregate() +} +func (r *OBTenantOperation) validateNewOperations() error { + if r.Spec.TargetTenant == nil { + return field.Required(field.NewPath("spec").Child("targetTenant"), "name of targetTenant is required") + } + allErrs := field.ErrorList{} obtenant := &OBTenant{} err := clt.Get(context.Background(), types.NamespacedName{Name: *r.Spec.TargetTenant, Namespace: r.Namespace}, obtenant) if err != nil { @@ -274,9 +282,11 @@ func (r *OBTenantOperation) validateMutation() error { } else { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("targetTenant"), r.Spec.TargetTenant, "The target tenant's cluster "+obtenant.Spec.ClusterName+" does not exist")) } + break } if obcluster.Spec.Topology == nil { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("targetTenant"), r.Spec.TargetTenant, "The target tenant's cluster "+obtenant.Spec.ClusterName+" does not have a topology")) + break } pools := make(map[string]any) for _, pool := range obtenant.Spec.Pools { @@ -287,6 +297,9 @@ func (r *OBTenantOperation) validateMutation() error { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("addResourcePools"), r.Spec.AddResourcePools, "The resource pool already exists")) } } + if len(allErrs) != 0 { + return allErrs.ToAggregate() + } zonesInOBCluster := make(map[string]any, len(obcluster.Spec.Topology)) for _, zone := range obcluster.Spec.Topology { zonesInOBCluster[zone.Zone] = struct{}{} @@ -313,6 +326,7 @@ func (r *OBTenantOperation) validateMutation() error { case constants.TenantOpDeleteResourcePools: if len(r.Spec.DeleteResourcePools) == 0 { allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("deleteResourcePools"), "deleteResourcePools is required")) + break } pools := make(map[string]any) for _, pool := range obtenant.Spec.Pools { @@ -324,7 +338,6 @@ func (r *OBTenantOperation) validateMutation() error { } } default: - allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("type"), string(r.Spec.Type)+" type of operation is not supported")) } return allErrs.ToAggregate() } diff --git a/api/v1alpha1/obtenantoperation_webhook_test.go b/api/v1alpha1/obtenantoperation_webhook_test.go index dc31104ac..58d7f2336 100644 --- a/api/v1alpha1/obtenantoperation_webhook_test.go +++ b/api/v1alpha1/obtenantoperation_webhook_test.go @@ -13,10 +13,11 @@ See the Mulan PSL v2 for more details. package v1alpha1 import ( - apiconsts "github.com/oceanbase/ob-operator/api/constants" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + + apiconsts "github.com/oceanbase/ob-operator/api/constants" ) var _ = Describe("Test OBTenantOperation Webhook", Label("webhook"), Serial, func() { @@ -25,7 +26,7 @@ var _ = Describe("Test OBTenantOperation Webhook", Label("webhook"), Serial, fun tenantStandby := "test-tenant-for-operation2" It("Create cluster and tenants", func() { - c := newOBCluster(clusterName, 1, 1) + c := newOBCluster(clusterName, 3, 1) t := newOBTenant(tenantPrimary, clusterName) t2 := newOBTenant(tenantStandby, clusterName) t2.Spec.TenantRole = apiconsts.TenantRoleStandby @@ -35,6 +36,10 @@ var _ = Describe("Test OBTenantOperation Webhook", Label("webhook"), Serial, fun Expect(k8sClient.Create(ctx, c)).Should(Succeed()) Expect(k8sClient.Create(ctx, t)).Should(Succeed()) Expect(k8sClient.Create(ctx, t2)).Should(Succeed()) + + t.Status.TenantRole = apiconsts.TenantRolePrimary + t.Status.Pools = []ResourcePoolStatus{} + Expect(k8sClient.Status().Update(ctx, t)).Should(Succeed()) }) It("Check operation types", func() { @@ -119,6 +124,9 @@ var _ = Describe("Test OBTenantOperation Webhook", Label("webhook"), Serial, fun notexist := "tenant-not-exist" op.Spec.TargetTenant = ¬exist Expect(k8sClient.Create(ctx, op)).ShouldNot(Succeed()) + + op.Spec.TargetTenant = &tenantPrimary + Expect(k8sClient.Create(ctx, op)).Should(Succeed()) }) It("Check operation replay log", func() { @@ -144,4 +152,90 @@ var _ = Describe("Test OBTenantOperation Webhook", Label("webhook"), Serial, fun } Expect(k8sClient.Create(ctx, op)).ShouldNot(Succeed()) }) + + It("Check adding resource pools", func() { + op := newTenantOperation(tenantPrimary) + op.Spec.Type = apiconsts.TenantOpAddResourcePools + op.Spec.AddResourcePools = []ResourcePoolSpec{{ + Zone: "zone1", + Type: &LocalityType{ + Name: "Full", + Replica: 1, + IsActive: true, + }, + UnitConfig: &UnitConfig{ + MaxCPU: resource.MustParse("1"), + MemorySize: resource.MustParse("5Gi"), + MinCPU: resource.MustParse("1"), + MaxIops: 1024, + MinIops: 1024, + IopsWeight: 2, + LogDiskSize: resource.MustParse("12Gi"), + }, + }} + + notexist := "tenant-not-exist" + op.Spec.TargetTenant = ¬exist + + Expect(k8sClient.Create(ctx, op)).ShouldNot(Succeed()) + + op.Spec.TargetTenant = &tenantPrimary + Expect(k8sClient.Create(ctx, op)).ShouldNot(Succeed()) + + op.Spec.Force = true + Expect(k8sClient.Create(ctx, op)).Should(Succeed()) + + // Delete resource pool + opDel := newTenantOperation(tenantPrimary) + opDel.Spec.Type = apiconsts.TenantOpDeleteResourcePools + opDel.Spec.DeleteResourcePools = []string{"zone0"} + opDel.Spec.TargetTenant = &tenantPrimary + Expect(k8sClient.Create(ctx, opDel)).ShouldNot(Succeed()) + opDel.Spec.Force = true + Expect(k8sClient.Create(ctx, opDel)).Should(Succeed()) + }) + + It("Check modifying resource pools", func() { + op := newTenantOperation(tenantPrimary) + op.Spec.Type = apiconsts.TenantOpModifyResourcePools + op.Spec.ModifyResourcePools = []ResourcePoolSpec{{ + Zone: "zone0", + Type: &LocalityType{ + Name: "Full", + Replica: 1, + IsActive: true, + }, + UnitConfig: &UnitConfig{ + MaxCPU: resource.MustParse("6"), + MemorySize: resource.MustParse("6Gi"), + MinCPU: resource.MustParse("2"), + MaxIops: 1024, + MinIops: 1024, + IopsWeight: 2, + LogDiskSize: resource.MustParse("12Gi"), + }, + }} + + op.Spec.TargetTenant = &tenantPrimary + Expect(k8sClient.Create(ctx, op)).ShouldNot(Succeed()) + + op.Spec.Force = true + Expect(k8sClient.Create(ctx, op)).Should(Succeed()) + }) + + It("Check setting connection white list", func() { + op := newTenantOperation(tenantPrimary) + op.Spec.Type = apiconsts.TenantOpSetConnectWhiteList + op.Spec.ConnectWhiteList = "%,127.0.0.1" + op.Spec.Force = true + Expect(k8sClient.Create(ctx, op)).Should(Succeed()) + }) + + It("Check setting unit number", func() { + op := newTenantOperation(tenantPrimary) + op.Spec.Type = apiconsts.TenantOpSetUnitNumber + op.Spec.UnitNumber = 2 + op.Spec.Force = true + Expect(k8sClient.Create(ctx, op)).Should(Succeed()) + }) }) diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 9926ecb2a..5f103f02f 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -210,6 +210,8 @@ var _ = AfterSuite(func() { By("Clean auxiliary resources") cancel() By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) + if testEnv != nil { + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) + } }) diff --git a/make/deps.mk b/make/deps.mk index ee9c53846..a6cedb1e4 100644 --- a/make/deps.mk +++ b/make/deps.mk @@ -39,7 +39,7 @@ install-delve: ## Install delve, a debugger for the Go programming language. Mor go install github.com/go-delve/delve/cmd/dlv@master .PHONY: tools -tools: kustomize controller-gen envtest install-delve ## Download all tools +tools: kustomize controller-gen envtest ## Download all tools .PHONY: init-generator init-generator: ## Install generator tools diff --git a/make/development.mk b/make/development.mk index 7f6b00e3b..e66f58d29 100644 --- a/make/development.mk +++ b/make/development.mk @@ -35,6 +35,11 @@ test-all: manifests generate fmt vet envtest ## Run all tests including long-run go run github.com/onsi/ginkgo/v2/ginkgo -r --covermode=atomic --coverprofile=cover.profile --cpuprofile=cpu.profile --memprofile=mem.profile --cover \ --output-dir=testreports --keep-going --json-report=report.json --label-filter='$(CASE_LABEL_FILTERS)' --skip-package './distribution' +.PHONY: test-webhooks +test-webhooks: ## Test the webhooks + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ + go run github.com/onsi/ginkgo/v2/ginkgo ./api/... + REPORT_PORT ?= 8480 .PHONY: unit-coverage From eb898f38ecdd627f6d855c268618d93695c6e37b Mon Sep 17 00:00:00 2001 From: powerfool Date: Mon, 14 Oct 2024 11:36:45 +0800 Subject: [PATCH 2/6] Bump versions to 2.3.0 and update changelog (#583) --- Makefile | 2 +- README-CN.md | 8 ++++---- README.md | 8 ++++---- charts/ob-operator/Chart.yaml | 4 ++-- charts/ob-operator/templates/operator.yaml | 2 +- config/manager/kustomization.yaml | 2 +- deploy/operator.yaml | 2 +- docsite/docs/developer/deploy-locally.md | 4 ++-- docsite/docs/developer/deploy.md | 6 +++--- .../manual/200.quick-start-of-ob-operator.md | 4 ++-- docsite/docs/manual/300.deploy-ob-operator.md | 2 +- .../docs/manual/400.ob-operator-upgrade.md | 2 +- .../100.create-tenant.md | 2 +- .../300.delete-tenant-of-ob-operator.md | 2 +- .../docs/manual/900.appendix/100.example.md | 20 +++++++++---------- .../current/developer/deploy-locally.md | 4 ++-- .../current/developer/deploy.md | 6 +++--- .../manual/200.quick-start-of-ob-operator.md | 4 ++-- .../current/manual/300.deploy-ob-operator.md | 2 +- .../current/manual/400.ob-operator-upgrade.md | 2 +- .../100.create-tenant.md | 2 +- .../300.delete-tenant-of-ob-operator.md | 2 +- .../manual/900.appendix/100.example.md | 20 +++++++++---------- .../changelog.md | 18 +++++++++++++++++ .../docusaurus-plugin-content-pages/index.mdx | 8 ++++---- docsite/src/pages/changelog.md | 18 +++++++++++++++++ docsite/src/pages/index.mdx | 8 ++++---- example/openstack/README.md | 4 ++-- 28 files changed, 102 insertions(+), 66 deletions(-) diff --git a/Makefile b/Makefile index 3fe1412cb..7689a202b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ include make/* -VERSION ?= 2.2.2 +VERSION ?= 2.3.0 # Image URL to use all building/pushing image targets IMG ?= quay.io/oceanbase/ob-operator:${VERSION} # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. diff --git a/README-CN.md b/README-CN.md index 139b5a56d..6c6793d9e 100644 --- a/README-CN.md +++ b/README-CN.md @@ -15,7 +15,7 @@ ob-operator 是满足 Kubernetes Operator 扩展范式的自动化工具,可 ob-operator 依赖 [cert-manager](https://cert-manager.io/docs/), cert-manager 的安装可以参考对应的[安装文档](https://cert-manager.io/docs/installation/),如果您无法访问官方制品托管在 `quay.io` 镜像站的镜像,可通过下面的指令安装我们转托在 `docker.io` 中的制品: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` 本例子中的 OceanBase 集群存储依赖 [local-path-provisioner](https://github.com/rancher/local-path-provisioner) 提供, 需要提前进行安装并确保其存储目的地有足够大的磁盘空间。如果您计划在生产环境部署,推荐使用其他的存储解决方案。我们在[存储兼容性](#存储兼容性)一节提供了我们测试过的存储兼容性结果。 @@ -29,7 +29,7 @@ kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_r - 稳定版本 ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - 开发版本 @@ -45,7 +45,7 @@ Helm Chart 将 ob-operator 部署的命名空间进行了参数化,可在安 ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ helm repo update -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` #### 使用 terraform @@ -97,7 +97,7 @@ kubectl create secret generic root-password --from-literal=password='root_passwo 通过以下命令即可在 K8s 集群中部署 OceanBase: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` 一般初始化集群需要 2 分钟左右的时间,执行以下命令,查询集群状态,当集群状态变成 running 之后表示集群创建和初始化成功: diff --git a/README.md b/README.md index 9c30bd067..c10a30585 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ob-operator relies on [cert-manager](https://cert-manager.io/docs/) for certific If you have trouble accessing `quay.io` image registry, our mirrored cert-manager manifests can be applied through following command: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` Storage of OceanBase cluster in this example relies on [local-path-provisioner](https://github.com/rancher/local-path-provisioner), which should be installed beforehand. You should confirm that there is enough disk space in storage destination of local-path-provisioner. If you decide to deploy OceanBase cluster in production environment, it is recommended to use other storage solutions. We have provided a compatible table for storage solutions that we tested in section [Storage Compatibility](#storage-compatibility). @@ -30,7 +30,7 @@ You can deploy ob-operator in a Kubernetes cluster by executing the following co - Stable ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - Development @@ -46,7 +46,7 @@ Helm Chart parameterizes the namespace in which ob-operator is deployed, allowin ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ helm repo update -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` #### Using terraform @@ -98,7 +98,7 @@ kubectl create secret generic root-password --from-literal=password='root_passwo You can deploy OceanBase in a Kubernetes cluster by executing the following command: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` It generally takes around 2 minutes to bootstrap a cluster. Execute the following command to check the status of the cluster. Once the cluster status changes to "running," it indicates that the cluster has been successfully created and bootstrapped: diff --git a/charts/ob-operator/Chart.yaml b/charts/ob-operator/Chart.yaml index 487ff0502..6797e9e8a 100644 --- a/charts/ob-operator/Chart.yaml +++ b/charts/ob-operator/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -appVersion: 2.2.2 +appVersion: 2.3.0 description: A Helm chart for OB-Operator name: ob-operator type: application -version: 2.2.2 +version: 2.3.0 diff --git a/charts/ob-operator/templates/operator.yaml b/charts/ob-operator/templates/operator.yaml index 05bfdf6fb..f1c0bb0c5 100644 --- a/charts/ob-operator/templates/operator.yaml +++ b/charts/ob-operator/templates/operator.yaml @@ -21377,7 +21377,7 @@ spec: - --log-verbosity=0 command: - /manager - image: quay.io/oceanbase/ob-operator:2.2.2 + image: quay.io/oceanbase/ob-operator:2.3.0 livenessProbe: httpGet: path: /healthz diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 496d2a7dc..8646b404a 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: quay.io/oceanbase/ob-operator - newTag: 2.2.2 + newTag: 2.3.0 diff --git a/deploy/operator.yaml b/deploy/operator.yaml index b08bed649..cce6e8a44 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -21390,7 +21390,7 @@ spec: - --log-verbosity=0 command: - /manager - image: quay.io/oceanbase/ob-operator:2.2.2 + image: quay.io/oceanbase/ob-operator:2.3.0 livenessProbe: httpGet: path: /healthz diff --git a/docsite/docs/developer/deploy-locally.md b/docsite/docs/developer/deploy-locally.md index 53e73f0dc..8ca4f89e6 100644 --- a/docsite/docs/developer/deploy-locally.md +++ b/docsite/docs/developer/deploy-locally.md @@ -40,14 +40,14 @@ Tips: Perform `minikube dashboard` to open kubernetes dashboard, everything in t ob-operator depends on `cert-manager` to enable TLS functionalities, so we should install it first. ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` ### 4. Install ob-operator For robustness, default memory limit of ob-operator container is set to `1Gi` which is too large for us developing locally. We recommend fetching the manifests to local and configure it. wget tool could be useful here, while opening the URL and copying the contents to local file is more straight. -https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml Search the pattern `/manager`, find the target container, configure the memory limit to `400Mi` and cpu limit to `400m`. diff --git a/docsite/docs/developer/deploy.md b/docsite/docs/developer/deploy.md index 3fc5545d1..75d175dbc 100644 --- a/docsite/docs/developer/deploy.md +++ b/docsite/docs/developer/deploy.md @@ -12,20 +12,20 @@ ob-operator supports deployment using Helm. Before deploying ob-operator with th ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` Parameters: * namespace: Namespace, can be customized. It is recommended to use "oceanbase-system" as the namespace. -* version: ob-operator version number. It is recommended to use the latest version `2.2.2`. +* version: ob-operator version number. It is recommended to use the latest version `2.3.0`. ## 2.2 Deploying with Configuration Files * Stable ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` * Development ```shell diff --git a/docsite/docs/manual/200.quick-start-of-ob-operator.md b/docsite/docs/manual/200.quick-start-of-ob-operator.md index 4ba78fe29..ed2462e89 100644 --- a/docsite/docs/manual/200.quick-start-of-ob-operator.md +++ b/docsite/docs/manual/200.quick-start-of-ob-operator.md @@ -21,7 +21,7 @@ Run the following command to deploy ob-operator in the Kubernetes cluster: - Deploy the stable version of ob-operator ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - Deploy the developing version of ob-operator @@ -61,7 +61,7 @@ Perform the following steps to deploy an OceanBase cluster in the Kubernetes clu Run the following command to deploy an OceanBase cluster in the Kubernetes cluster: ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` In general, it takes about 2 minutes to create a cluster. Run the following command to check the cluster status: diff --git a/docsite/docs/manual/300.deploy-ob-operator.md b/docsite/docs/manual/300.deploy-ob-operator.md index a5b1f5a90..dad4f7ee5 100644 --- a/docsite/docs/manual/300.deploy-ob-operator.md +++ b/docsite/docs/manual/300.deploy-ob-operator.md @@ -33,7 +33,7 @@ You can deploy ob-operator by using the configuration file for the stable or dev * Deploy the stable version of ob-operator ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` * Deploy the developing version of ob-operator diff --git a/docsite/docs/manual/400.ob-operator-upgrade.md b/docsite/docs/manual/400.ob-operator-upgrade.md index 44e79d4ee..812cb58e8 100644 --- a/docsite/docs/manual/400.ob-operator-upgrade.md +++ b/docsite/docs/manual/400.ob-operator-upgrade.md @@ -17,7 +17,7 @@ If you upgrade ob-operator by using configuration files, you only need to reappl - Deploy the stable version of ob-operator ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - Deploy the developing version of ob-operator diff --git a/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md b/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md index 37af9a02d..cd505b2dd 100644 --- a/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md +++ b/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md @@ -16,7 +16,7 @@ Before you create a tenant, make sure the following conditions are met: ## Create a tenant by using the configuration file -You can create a tenant by using the configuration file of the tenant. For more information about the configuration file, visit [GitHub](https://github.com/oceanbase/ob-operator/blob/2.2.2_release/example/tenant/tenant.yaml). +You can create a tenant by using the configuration file of the tenant. For more information about the configuration file, visit [GitHub](https://github.com/oceanbase/ob-operator/blob/2.3.0_release/example/tenant/tenant.yaml). Run the following command to create a tenant. This command creates an OceanBase Database tenant with custom resources in the current Kubernetes cluster. diff --git a/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md b/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md index 2a548c86c..a6e449c8f 100644 --- a/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md +++ b/docsite/docs/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md @@ -8,7 +8,7 @@ This topic describes how to use ob-operator to delete a tenant from a Kubernetes ## Procedure -You can delete the specified tenant resources from the cluster by using the configuration file `tenant.yaml`. For more information about the configuration file, visit [GitHub](https://github.com/oceanbase/ob-operator/blob/2.2.2_release/example/tenant/tenant.yaml). +You can delete the specified tenant resources from the cluster by using the configuration file `tenant.yaml`. For more information about the configuration file, visit [GitHub](https://github.com/oceanbase/ob-operator/blob/2.3.0_release/example/tenant/tenant.yaml). Run the following command to delete a tenant. This command deletes an OceanBase Database tenant with custom resources in the current Kubernetes cluster. diff --git a/docsite/docs/manual/900.appendix/100.example.md b/docsite/docs/manual/900.appendix/100.example.md index 251953ebf..a2e1df7bb 100644 --- a/docsite/docs/manual/900.appendix/100.example.md +++ b/docsite/docs/manual/900.appendix/100.example.md @@ -27,7 +27,7 @@ In this example, the following components are deployed: Create a namespace: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/namespace.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/namespace.yaml ``` View the created namespace: @@ -46,7 +46,7 @@ oceanbase Active 98s Create secrets for the cluster and tenants: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/secret.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/secret.yaml ``` View the created secrets: @@ -73,7 +73,7 @@ ob-configserver allows you to register, store, and query metadata of the RootSer Run the following command to deploy ob-configserver and create the corresponding service: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/configserver.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/configserver.yaml ``` Check the pod status: @@ -101,7 +101,7 @@ When you deploy an OceanBase cluster, add environment variables and set the syst Deploy the OceanBase cluster: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/obcluster.yaml ``` Run the following command to query the status of the OceanBase cluster until the status becomes `running`: @@ -121,7 +121,7 @@ You can start ODP by using ob-configserver or specifying the RS list. To maximiz Run the following command to deploy ODP and create the ODP service: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/obproxy.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/obproxy.yaml ``` When you query the pod status of ODP, you can see two ODP pods. @@ -165,7 +165,7 @@ You can create a dedicated tenant for each type of business for better resource Run the following command to create a tenant: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/tenant.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/tenant.yaml ``` Run the following command to query the status of the tenant until the status becomes `running`: @@ -201,7 +201,7 @@ create database dev; Run the following command to deploy the application: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/oceanbase-todo.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/oceanbase-todo.yaml ``` After the deployment process is completed, run the following command to view the application status: @@ -254,7 +254,7 @@ When you deploy the OceanBase cluster, an OBAgent sidecar container is created i Run the following command to deploy Prometheus: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/prometheus.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/prometheus.yaml ``` Run the following command to view the deployment status: @@ -276,7 +276,7 @@ Grafana displays the metrics of OceanBase Database by using Prometheus as a data Run the following command to deploy Grafana: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/grafana.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/grafana.yaml ``` Run the following command to view the deployment status: @@ -302,4 +302,4 @@ This topic describes how to deploy OceanBase Database and related components suc ## Note -You can find all configuration files used in this topic in the [webapp](https://github.com/oceanbase/ob-operator/tree/2.2.2_release/example/webapp) directory. +You can find all configuration files used in this topic in the [webapp](https://github.com/oceanbase/ob-operator/tree/2.3.0_release/example/webapp) directory. diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy-locally.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy-locally.md index 53e73f0dc..8ca4f89e6 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy-locally.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy-locally.md @@ -40,14 +40,14 @@ Tips: Perform `minikube dashboard` to open kubernetes dashboard, everything in t ob-operator depends on `cert-manager` to enable TLS functionalities, so we should install it first. ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` ### 4. Install ob-operator For robustness, default memory limit of ob-operator container is set to `1Gi` which is too large for us developing locally. We recommend fetching the manifests to local and configure it. wget tool could be useful here, while opening the URL and copying the contents to local file is more straight. -https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml Search the pattern `/manager`, find the target container, configure the memory limit to `400Mi` and cpu limit to `400m`. diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy.md index e1a9e9215..908a16fdf 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer/deploy.md @@ -10,20 +10,20 @@ ob-operator 支持通过 Helm 进行部署,在使用 Helm 命令部署 ob-oper ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` 参数说明: * namespace:命名空间,可自定义,一般建议使用 oceanbase-system。 -* version:ob-operator 版本号,建议使用最新的版本 `2.2.2`。 +* version:ob-operator 版本号,建议使用最新的版本 `2.3.0`。 ## 2.2 使用配置文件部署 * Stable ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` * Development ```shell diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/200.quick-start-of-ob-operator.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/200.quick-start-of-ob-operator.md index 2610061ed..f9c2cc787 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/200.quick-start-of-ob-operator.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/200.quick-start-of-ob-operator.md @@ -21,7 +21,7 @@ sidebar_position: 2 - 稳定版本 ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - 开发版本 @@ -61,7 +61,7 @@ oceanbase-controller-manager-86cfc8f7bf-4hfnj 2/2 Running 0 1m 使用以下命令在 Kubernetes 集群上部署 OceanBase 集群: ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` 集群创建通常需要约 2 分钟。执行以下命令检查集群状态: diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/300.deploy-ob-operator.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/300.deploy-ob-operator.md index cddb53e61..335848484 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/300.deploy-ob-operator.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/300.deploy-ob-operator.md @@ -32,7 +32,7 @@ helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system -- * 稳定版本 ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` * 开发版本 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/400.ob-operator-upgrade.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/400.ob-operator-upgrade.md index 33fc19f55..149f41743 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/400.ob-operator-upgrade.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/400.ob-operator-upgrade.md @@ -17,7 +17,7 @@ sidebar_position: 4 - 稳定版本 ```shell - kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml + kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - 开发版本 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md index 3824da662..391c89a8f 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/100.create-tenant.md @@ -16,7 +16,7 @@ sidebar_position: 2 ## 使用配置文件创建租户 -通过应用租户配置文件创建租户。配置文件内容可参考 [GitHub](https://github.com/oceanbase/ob-operator/blob/2.2.2_release/example/tenant/tenant.yaml) 。 +通过应用租户配置文件创建租户。配置文件内容可参考 [GitHub](https://github.com/oceanbase/ob-operator/blob/2.3.0_release/example/tenant/tenant.yaml) 。 创建租户的命令如下,该命令会在当前 Kubernetes 集群中创建一个 OBTenant 租户的资源。 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md index 1f760516d..a5dc07fc6 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/200.tenant-management-of-ob-operator/300.delete-tenant-of-ob-operator.md @@ -8,7 +8,7 @@ sidebar_position: 4 ## 具体操作 -通过配置文件 tenant.yaml 在集群中删除指定的租户资源。配置文件可参考 [GitHub](https://github.com/oceanbase/ob-operator/blob/2.2.2_release/example/tenant/tenant.yaml)。 +通过配置文件 tenant.yaml 在集群中删除指定的租户资源。配置文件可参考 [GitHub](https://github.com/oceanbase/ob-operator/blob/2.3.0_release/example/tenant/tenant.yaml)。 删除租户的命令如下,该命令会在当前 Kubernetes 集群中删除对应租户的 OBTenant 资源。 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/900.appendix/100.example.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/900.appendix/100.example.md index 48a3cd9ec..2c7ec56f9 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/900.appendix/100.example.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/900.appendix/100.example.md @@ -26,7 +26,7 @@ sidebar_position: 1 创建 namespace。 ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/namespace.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/namespace.yaml ``` 使用以下命令查看创建的 namespace: @@ -45,7 +45,7 @@ oceanbase Active 98s 创建集群和租户的 secret: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/secret.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/secret.yaml ``` 通过以下命令查看创建的 secret: @@ -72,7 +72,7 @@ ob-configserver 是提供 OceanBase rootservice 信息注册和查询的服务 使用如下命令部署 ob-configserver 以及创建对应的 service: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/configserver.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/configserver.yaml ``` 检查 pod 状态: @@ -100,7 +100,7 @@ svc-ob-configserver NodePort 10.96.3.39 8080:30080/TCP 98s 使用如下命令部署 OceanBase 集群: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/obcluster.yaml ``` 轮询使用如下命令检查 obcluster 状态,直到集群变成 running 状态。 @@ -120,7 +120,7 @@ ObProxy 支持使用 ob-configserver 或者直接指定 rs_list 的形式启动 使用如下命令部署 ObProxy 以及创建 service: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/obproxy.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/obproxy.yaml ``` 查看 ObProxy 的 pod 状态,会有两个 obproxy 的 pod。 @@ -164,7 +164,7 @@ mysql -h${obproxy-service-address} -P2883 -uroot@sys#metadb -p 使用如下命令创建租户: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/tenant.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/tenant.yaml ``` 创建后轮询租户的资源状态, 当变成 running 时表示租户以及创建完成了 @@ -200,7 +200,7 @@ create database dev; 使用如下命令部署应用: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/oceanbase-todo.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/oceanbase-todo.yaml ``` 部署成功之后,可以通过如下命令进行查看部署的状态: @@ -253,7 +253,7 @@ $ curl http://10.43.39.231:20031 使用如下命令部署 prometheus: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/prometheus.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/prometheus.yaml ``` 使用如下命令查看部署状态: @@ -275,7 +275,7 @@ grafana 可以使用 prometheus 作为数据源,进行 OceanBase 指标的展 使用如下命令部署 grafana: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/webapp/grafana.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/webapp/grafana.yaml ``` 使用如下命令查看部署状态: @@ -301,4 +301,4 @@ svc-grafana NodePort 10.96.2.145 3000:30030/TCP 2m ## 说明 -本文中的配置文件均可在 [webapp 配置文件](https://github.com/oceanbase/ob-operator/tree/2.2.2_release/example/webapp) 目录中找到。 +本文中的配置文件均可在 [webapp 配置文件](https://github.com/oceanbase/ob-operator/tree/2.3.0_release/example/webapp) 目录中找到。 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/changelog.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/changelog.md index 6a48d2297..1a28cc91a 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/changelog.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/changelog.md @@ -1,5 +1,23 @@ # 变更日志 +## 2.3.0 (发布于 2024.10.14) + +### 新增特性 + +1. 支持跨 K8s 集群调度 OceanBase 集群 +2. 支持设置腾讯云 COS、AWS s3 以及 s3 兼容的对象存储服务作为数据备份的介质 +3. 支持删除特定的 OBServer +4. 支持根据场景设置优化 OceanBase 集群的系统参数和变量 +5. 支持将大部分 K8s 内置的 Pod 字段设置到 OBServer 中 + +### 缺陷修复 + +1. 修复 2-2-2 集群滚动替换 OBServer 时可能出现卡住的问题 + +### 功能优化 + +1. 补充了几种新的 `OBTenantOperation` 类型用于执行常见操作,如创建或删除资源池、设置 Unit Number 等 + ## 2.2.2 (发布于 2024.06.18) ### 新增特性 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/index.mdx b/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/index.mdx index 797dae269..23e1d7a88 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/index.mdx +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-pages/index.mdx @@ -19,7 +19,7 @@ import Link from '@docusaurus/Link' ob-operator 依赖 [cert-manager](https://cert-manager.io/docs/), cert-manager 的安装可以参考对应的[安装文档](https://cert-manager.io/docs/installation/),如果您无法访问官方制品托管在 `quay.io` 镜像站的镜像,可通过下面的指令安装我们转托在 `docker.io` 中的制品: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` 本例子中的 OceanBase 集群存储依赖 [local-path-provisioner](https://github.com/rancher/local-path-provisioner) 提供, 需要提前进行安装并确保其存储目的地有足够大的磁盘空间。如果您计划在生产环境部署,推荐使用其他的存储解决方案。我们在[存储兼容性](#存储兼容性)一节提供了我们测试过的存储兼容性结果。 @@ -33,7 +33,7 @@ kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_r - 稳定版本 ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - 开发版本 @@ -49,7 +49,7 @@ Helm Chart 将 ob-operator 部署的命名空间进行了参数化,可在安 ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ helm repo update -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` #### 使用 terraform @@ -101,7 +101,7 @@ kubectl create secret generic root-password --from-literal=password='root_passwo 通过以下命令即可在 K8s 集群中部署 OceanBase: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` 一般初始化集群需要 2 分钟左右的时间,执行以下命令,查询集群状态,当集群状态变成 running 之后表示集群创建和初始化成功: diff --git a/docsite/src/pages/changelog.md b/docsite/src/pages/changelog.md index dcb979b60..7934762db 100644 --- a/docsite/src/pages/changelog.md +++ b/docsite/src/pages/changelog.md @@ -1,5 +1,23 @@ # Changelog +## 2.3.0 (Release on 2024.10.14) + +### New Features + +1. Support for scheduling OceanBase cluster across multiple K8s clusters. +2. Support for backing up to Tencent COS, AWS s3 and s3 compatible storage. +3. Support for deleting specific OBServer. +4. Support for optimizing parameters and variables by scenario. +5. Support for setting most of native fields of Pods to OBServer. + +### Bug fixes + +1. Fixed the issue that it get stuck when a 2-2-2 cluster rolling replace its OBServer pods. + +### Optimization + +1. Supplement several new types of `OBTenantOperation` to perform common operations like creating or deleting resource pools, setting unit number and so on. + ## 2.2.2 (Release on 2024.06.18) ### New Features diff --git a/docsite/src/pages/index.mdx b/docsite/src/pages/index.mdx index 780f3df93..4c92b9933 100644 --- a/docsite/src/pages/index.mdx +++ b/docsite/src/pages/index.mdx @@ -20,7 +20,7 @@ ob-operator relies on [cert-manager](https://cert-manager.io/docs/) for certific If you have trouble accessing `quay.io` image registry, our mirrored cert-manager manifests can be applied through following command: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` Storage of OceanBase cluster in this example relies on [local-path-provisioner](https://github.com/rancher/local-path-provisioner), which should be installed beforehand. You should confirm that there is enough disk space in storage destination of local-path-provisioner.If you decide to deploy OceanBase cluster in production environment, it is recommended to use other storage solutions. We have provided a compatible table for storage solutions that we tested in section [Storage Compatibility](#storage-compatibility). @@ -34,7 +34,7 @@ You can deploy ob-operator in a Kubernetes cluster by executing the following co - Stable ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/operator.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/operator.yaml ``` - Development @@ -50,7 +50,7 @@ Helm Chart parameterizes the namespace in which ob-operator is deployed, allowin ```shell helm repo add ob-operator https://oceanbase.github.io/ob-operator/ helm repo update -helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.2.2 +helm install ob-operator ob-operator/ob-operator --namespace=oceanbase-system --create-namespace --version=2.3.0 ``` #### Using terraform @@ -102,7 +102,7 @@ kubectl create secret generic root-password --from-literal=password='root_passwo You can deploy OceanBase in a Kubernetes cluster by executing the following command: ```shell -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/example/quickstart/obcluster.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/example/quickstart/obcluster.yaml ``` It generally takes around 2 minutes to bootstrap a cluster. Execute the following command to check the status of the cluster. Once the cluster status changes to "running," it indicates that the cluster has been successfully created and bootstrapped: diff --git a/example/openstack/README.md b/example/openstack/README.md index 31581d060..644e98b5e 100644 --- a/example/openstack/README.md +++ b/example/openstack/README.md @@ -11,13 +11,13 @@ This folder contains configuration files to deploy OceanBase and OpenStack on Ku 1. Deploy cert-manager Deploy the cert-manager using the following command. Ensure all pods are running before proceeding to the next step: ``` -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` 2. deploy ob-operator Deploy the ob-operator using the command below. Wait until all pods are running: ``` -kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.2.2_release/deploy/cert-manager.yaml +kubectl apply -f https://raw.githubusercontent.com/oceanbase/ob-operator/2.3.0_release/deploy/cert-manager.yaml ``` 3. create secret Create secret using the following command From 75da18c9bdd34e2d8786b7685ea67982ae334d88 Mon Sep 17 00:00:00 2001 From: powerfool Date: Mon, 14 Oct 2024 20:58:30 +0800 Subject: [PATCH 3/6] Supplement restoring tenant from new types of backup destinations (#586) --- api/v1alpha1/obtenant_webhook.go | 122 ++++++++++++--------- build/Dockerfile.dashboard | 4 +- build/Dockerfile.obhelper | 2 +- build/Dockerfile.operator | 2 +- internal/resource/obtenantrestore/utils.go | 49 ++++----- 5 files changed, 99 insertions(+), 80 deletions(-) diff --git a/api/v1alpha1/obtenant_webhook.go b/api/v1alpha1/obtenant_webhook.go index 6412f60eb..6358e0f3a 100644 --- a/api/v1alpha1/obtenant_webhook.go +++ b/api/v1alpha1/obtenant_webhook.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "context" + "errors" "fmt" "regexp" "strings" @@ -291,55 +292,13 @@ func (r *OBTenant) validateMutation() error { if res.ArchiveSource == nil && res.BakDataSource == nil && res.SourceUri == "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore"), res, "Restore must have a source option, but both archiveSource, bakDataSource and sourceUri are nil now")) - } - - if res.ArchiveSource != nil && res.ArchiveSource.Type == constants.BackupDestTypeOSS { - if res.ArchiveSource.OSSAccessSecret == "" { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("archiveSource").Child("ossAccessSecret"), res.ArchiveSource.OSSAccessSecret, "Tenant restoring from OSS type backup data must have a OSSAccessSecret")) - } else { - secret := &v1.Secret{} - err := tenantClt.Get(context.Background(), types.NamespacedName{ - Namespace: r.GetNamespace(), - Name: res.ArchiveSource.OSSAccessSecret, - }, secret) - if err != nil { - if apierrors.IsNotFound(err) { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("archiveSource").Child("ossAccessSecret"), res.ArchiveSource.OSSAccessSecret, "Given OSSAccessSecret not found")) - } - allErrs = append(allErrs, field.InternalError(field.NewPath("spec").Child("source").Child("restore").Child("archiveSource").Child("ossAccessSecret"), err)) - } else { - if _, ok := secret.Data["accessId"]; !ok { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("archiveSource").Child("ossAccessSecret"), res.ArchiveSource.OSSAccessSecret, "accessId field not found in given OSSAccessSecret")) - } - if _, ok := secret.Data["accessKey"]; !ok { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("archiveSource").Child("ossAccessSecret"), res.ArchiveSource.OSSAccessSecret, "accessKey field not found in given OSSAccessSecret")) - } - } - } - } - - if res.BakDataSource != nil && res.BakDataSource.Type == constants.BackupDestTypeOSS { - if res.BakDataSource.OSSAccessSecret == "" { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("bakDataSource").Child("ossAccessSecret"), res.BakDataSource.OSSAccessSecret, "Tenant restoring from OSS type backup data must have a OSSAccessSecret")) - } else { - secret := &v1.Secret{} - err := tenantClt.Get(context.Background(), types.NamespacedName{ - Namespace: r.GetNamespace(), - Name: res.BakDataSource.OSSAccessSecret, - }, secret) - if err != nil { - if apierrors.IsNotFound(err) { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("bakDataSource").Child("ossAccessSecret"), res.BakDataSource.OSSAccessSecret, "Given OSSAccessSecret not found")) - } - allErrs = append(allErrs, field.InternalError(field.NewPath("spec").Child("source").Child("restore").Child("bakDataSource").Child("ossAccessSecret"), err)) - } else { - if _, ok := secret.Data["accessId"]; !ok { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("bakDataSource").Child("ossAccessSecret"), res.BakDataSource.OSSAccessSecret, "accessId field not found in given OSSAccessSecret")) - } - if _, ok := secret.Data["accessKey"]; !ok { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("source").Child("restore").Child("bakDataSource").Child("ossAccessSecret"), res.BakDataSource.OSSAccessSecret, "accessKey field not found in given OSSAccessSecret")) - } - } + } else { + destErrs := errors.Join( + validateBackupDestination(cluster, res.ArchiveSource, "spec", "source", "restore", "archiveSource"), + validateBackupDestination(cluster, res.BakDataSource, "spec", "source", "restore", "bakDataSource"), + ) + if destErrs != nil { + return destErrs } } } @@ -355,3 +314,68 @@ func (r *OBTenant) ValidateDelete() (admission.Warnings, error) { // TODO(user): fill in your validation logic upon object deletion. return nil, nil } + +func validateBackupDestination(cluster *OBCluster, dest *apitypes.BackupDestination, paths ...string) error { + var errorPath *field.Path + if len(paths) == 0 { + errorPath = field.NewPath("spec").Child("destination") + } else { + errorPath = field.NewPath("spec").Child(paths[0]) + for _, p := range paths[1:] { + errorPath = errorPath.Child(p) + } + } + if dest.Type == constants.BackupDestTypeNFS && cluster.Spec.BackupVolume == nil { + return field.Invalid(errorPath, cluster.Spec.BackupVolume, "backupVolume of obcluster is required when backing up data to NFS") + } + pattern, ok := constants.DestPathPatternMapping[dest.Type] + if !ok { + return field.Invalid(errorPath.Child("destination").Child("type"), dest.Type, "invalid backup destination type") + } + if !pattern.MatchString(dest.Path) { + return field.Invalid(errorPath.Child("destination").Child("path"), dest.Path, "invalid backup destination path, the path format should be "+pattern.String()) + } + if dest.Type != constants.BackupDestTypeNFS { + if dest.OSSAccessSecret == "" { + return field.Invalid(errorPath.Child("destination"), dest.OSSAccessSecret, "OSSAccessSecret is required when backing up data to OSS, COS or S3") + } + secret := &v1.Secret{} + err := bakClt.Get(context.Background(), types.NamespacedName{ + Namespace: cluster.GetNamespace(), + Name: dest.OSSAccessSecret, + }, secret) + fieldPath := errorPath.Child("destination").Child("ossAccessSecret") + if err != nil { + if apierrors.IsNotFound(err) { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "Given OSSAccessSecret not found") + } + return field.InternalError(fieldPath, err) + } + // All the following types need accessId and accessKey + switch dest.Type { + case + constants.BackupDestTypeCOS, + constants.BackupDestTypeOSS, + constants.BackupDestTypeS3, + constants.BackupDestTypeS3Compatible: + if _, ok := secret.Data["accessId"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessId field not found in given OSSAccessSecret") + } + if _, ok := secret.Data["accessKey"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "accessKey field not found in given OSSAccessSecret") + } + } + // The following types need additional fields + switch dest.Type { + case constants.BackupDestTypeCOS: + if _, ok := secret.Data["appId"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "appId field not found in given OSSAccessSecret") + } + case constants.BackupDestTypeS3: + if _, ok := secret.Data["s3Region"]; !ok { + return field.Invalid(fieldPath, dest.OSSAccessSecret, "s3Region field not found in given OSSAccessSecret") + } + } + } + return nil +} diff --git a/build/Dockerfile.dashboard b/build/Dockerfile.dashboard index 164a2306c..af511e73e 100644 --- a/build/Dockerfile.dashboard +++ b/build/Dockerfile.dashboard @@ -1,11 +1,11 @@ -FROM node:18-alpine as builder-fe +FROM node:18-alpine AS builder-fe WORKDIR /workspace COPY ./ui . ENV NODE_OPTIONS=--max_old_space_size=5120 RUN yarn RUN yarn build -FROM golang:1.22 as builder-be +FROM golang:1.22 AS builder-be ARG GOPROXY=https://goproxy.io,direct ARG GOSUMDB=sum.golang.org ARG COMMIT_HASH=unknown diff --git a/build/Dockerfile.obhelper b/build/Dockerfile.obhelper index bbb4fb8ab..4eff551e5 100644 --- a/build/Dockerfile.obhelper +++ b/build/Dockerfile.obhelper @@ -1,4 +1,4 @@ -FROM golang:1.22 as builder +FROM golang:1.22 AS builder ARG GOPROXY=https://goproxy.io,direct WORKDIR /workspace COPY . . diff --git a/build/Dockerfile.operator b/build/Dockerfile.operator index 74a0c09ed..3442bf649 100644 --- a/build/Dockerfile.operator +++ b/build/Dockerfile.operator @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.22 as builder +FROM golang:1.22 AS builder ARG GOPROXY ARG GOSUMDB diff --git a/internal/resource/obtenantrestore/utils.go b/internal/resource/obtenantrestore/utils.go index 55f7dbaee..ac2f3abfb 100644 --- a/internal/resource/obtenantrestore/utils.go +++ b/internal/resource/obtenantrestore/utils.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/oceanbase/ob-operator/api/constants" + apitypes "github.com/oceanbase/ob-operator/api/types" v1alpha1 "github.com/oceanbase/ob-operator/api/v1alpha1" oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" resourceutils "github.com/oceanbase/ob-operator/internal/resource/utils" @@ -49,25 +50,8 @@ func (m *ObTenantRestoreManager) getSourceUri() (string, error) { return source.SourceUri, nil } var bakPath, archivePath string - if source.BakDataSource != nil && source.BakDataSource.Type == constants.BackupDestTypeOSS { - accessId, accessKey, err := m.readAccessCredentials(source.BakDataSource.OSSAccessSecret) - if err != nil { - return "", err - } - bakPath = strings.Join([]string{source.BakDataSource.Path, "access_id=" + accessId, "access_key=" + accessKey}, "&") - } else { - bakPath = "file://" + path.Join(oceanbaseconst.BackupPath, source.BakDataSource.Path) - } - - if source.ArchiveSource != nil && source.ArchiveSource.Type == constants.BackupDestTypeOSS { - accessId, accessKey, err := m.readAccessCredentials(source.ArchiveSource.OSSAccessSecret) - if err != nil { - return "", err - } - archivePath = strings.Join([]string{source.ArchiveSource.Path, "access_id=" + accessId, "access_key=" + accessKey}, "&") - } else { - archivePath = "file://" + path.Join(oceanbaseconst.BackupPath, source.ArchiveSource.Path) - } + bakPath = m.getDestPath(source.BakDataSource) + archivePath = m.getDestPath(source.ArchiveSource) if bakPath == "" || archivePath == "" { return "", errors.New("Unexpected error: both bakPath and archivePath must be set") @@ -76,16 +60,27 @@ func (m *ObTenantRestoreManager) getSourceUri() (string, error) { return strings.Join([]string{bakPath, archivePath}, ","), nil } -func (m *ObTenantRestoreManager) readAccessCredentials(secretName string) (accessId, accessKey string, err error) { +func (m *ObTenantRestoreManager) getDestPath(dest *apitypes.BackupDestination) string { + if dest.Type == constants.BackupDestTypeNFS || resourceutils.IsZero(dest.Type) { + return "file://" + path.Join(oceanbaseconst.BackupPath, dest.Path) + } + if dest.OSSAccessSecret == "" { + return "" + } secret := &v1.Secret{} - err = m.Client.Get(m.Ctx, types.NamespacedName{ - Namespace: m.Resource.Namespace, - Name: secretName, + err := m.Client.Get(m.Ctx, types.NamespacedName{ + Namespace: m.Resource.GetNamespace(), + Name: dest.OSSAccessSecret, }, secret) if err != nil { - return "", "", err + m.PrintErrEvent(err) + return "" + } + destPath := strings.Join([]string{dest.Path, "access_id=" + string(secret.Data["accessId"]), "access_key=" + string(secret.Data["accessKey"])}, "&") + if dest.Type == constants.BackupDestTypeCOS { + destPath += ("&appid=" + string(secret.Data["appId"])) + } else if dest.Type == constants.BackupDestTypeS3 { + destPath += ("&s3_region=" + string(secret.Data["s3Region"])) } - accessId = string(secret.Data["accessId"]) - accessKey = string(secret.Data["accessKey"]) - return accessId, accessKey, nil + return destPath } From 1cb4588aa9b41961cc50c46db9302fd82726de38 Mon Sep 17 00:00:00 2001 From: powerfool Date: Tue, 15 Oct 2024 21:03:36 +0800 Subject: [PATCH 4/6] Fixed unexpected selection of PVCs (#588) --- internal/resource/observer/observer_task.go | 29 ++++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/internal/resource/observer/observer_task.go b/internal/resource/observer/observer_task.go index e44e55d75..189cd004d 100644 --- a/internal/resource/observer/observer_task.go +++ b/internal/resource/observer/observer_task.go @@ -114,8 +114,10 @@ func CreateOBServerPod(m *OBServerManager) tasktypes.TaskError { annotations := m.generateStaticIpAnnotation() ownerReferenceList = append(ownerReferenceList, ownerReference) observerPodSpec := m.createOBPodSpec(obcluster) - originLabels := m.OBServer.Labels - originLabels[oceanbaseconst.LabelOBServerUID] = string(m.OBServer.UID) + podLabels := m.OBServer.Labels + podLabels[oceanbaseconst.LabelRefUID] = string(m.OBServer.UID) + podLabels[oceanbaseconst.LabelOBServerUID] = string(m.OBServer.UID) // For compatibility with old version + podLabels[oceanbaseconst.LabelRefOBServer] = string(m.OBServer.Name) podFields := m.OBServer.Spec.OBServerTemplate.PodFields if podFields != nil { @@ -127,8 +129,8 @@ func CreateOBServerPod(m *OBServerManager) tasktypes.TaskError { observerPodSpec.Subdomain = varsReplacer.Replace(*podFields.Subdomain) } for k := range podFields.Labels { - if _, exist := originLabels[k]; !exist { - originLabels[k] = varsReplacer.Replace(podFields.Labels[k]) + if _, exist := podLabels[k]; !exist { + podLabels[k] = varsReplacer.Replace(podFields.Labels[k]) } } for k := range podFields.Annotations { @@ -144,7 +146,7 @@ func CreateOBServerPod(m *OBServerManager) tasktypes.TaskError { Name: m.OBServer.Name, Namespace: m.OBServer.Namespace, OwnerReferences: ownerReferenceList, - Labels: originLabels, + Labels: podLabels, Annotations: annotations, }, Spec: observerPodSpec, @@ -171,6 +173,10 @@ func CreateOBServerPVC(m *OBServerManager) tasktypes.TaskError { ownerReferenceList = append(ownerReferenceList, ownerReference) } singlePvcAnnoVal, singlePvcExist := resourceutils.GetAnnotationField(m.OBServer, oceanbaseconst.AnnotationsSinglePVC) + pvcLabels := m.OBServer.Labels + pvcLabels[oceanbaseconst.LabelRefUID] = string(m.OBServer.UID) + pvcLabels[oceanbaseconst.LabelRefOBServer] = string(m.OBServer.Name) + if singlePvcExist && singlePvcAnnoVal == "true" { sumQuantity := resource.Quantity{} sumQuantity.Add(m.OBServer.Spec.OBServerTemplate.Storage.DataStorage.Size) @@ -185,7 +191,7 @@ func CreateOBServerPVC(m *OBServerManager) tasktypes.TaskError { Name: m.OBServer.Name, Namespace: m.OBServer.Namespace, OwnerReferences: ownerReferenceList, - Labels: m.OBServer.Labels, + Labels: pvcLabels, }, Spec: m.generatePVCSpec(storageSpec), } @@ -198,7 +204,7 @@ func CreateOBServerPVC(m *OBServerManager) tasktypes.TaskError { Name: fmt.Sprintf("%s-%s", m.OBServer.Name, oceanbaseconst.DataVolumeSuffix), Namespace: m.OBServer.Namespace, OwnerReferences: ownerReferenceList, - Labels: m.OBServer.Labels, + Labels: pvcLabels, } pvc := &corev1.PersistentVolumeClaim{ ObjectMeta: objectMeta, @@ -213,7 +219,7 @@ func CreateOBServerPVC(m *OBServerManager) tasktypes.TaskError { Name: fmt.Sprintf("%s-%s", m.OBServer.Name, oceanbaseconst.ClogVolumeSuffix), Namespace: m.OBServer.Namespace, OwnerReferences: ownerReferenceList, - Labels: m.OBServer.Labels, + Labels: pvcLabels, } pvc = &corev1.PersistentVolumeClaim{ ObjectMeta: objectMeta, @@ -228,7 +234,7 @@ func CreateOBServerPVC(m *OBServerManager) tasktypes.TaskError { Name: fmt.Sprintf("%s-%s", m.OBServer.Name, oceanbaseconst.LogVolumeSuffix), Namespace: m.OBServer.Namespace, OwnerReferences: ownerReferenceList, - Labels: m.OBServer.Labels, + Labels: pvcLabels, } pvc = &corev1.PersistentVolumeClaim{ ObjectMeta: objectMeta, @@ -516,11 +522,14 @@ func CreateOBServerSvc(m *OBServerManager) tasktypes.TaskError { mode, modeAnnoExist := resourceutils.GetAnnotationField(m.OBServer, oceanbaseconst.AnnotationsMode) if modeAnnoExist && mode == oceanbaseconst.ModeService { m.Logger.Info("Create observer service") + svcLabels := m.OBServer.Labels + svcLabels[oceanbaseconst.LabelRefUID] = string(m.OBServer.UID) + svcLabels[oceanbaseconst.LabelRefOBServer] = string(m.OBServer.Name) svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: m.OBServer.Name, Namespace: m.OBServer.Namespace, - Labels: m.OBServer.Labels, + Labels: svcLabels, OwnerReferences: []metav1.OwnerReference{{ APIVersion: m.OBServer.APIVersion, Kind: m.OBServer.Kind, From 1e90ef1b164a6460cacc0fb8fad42d95d87ffe24 Mon Sep 17 00:00:00 2001 From: powerfool Date: Wed, 16 Oct 2024 11:54:48 +0800 Subject: [PATCH 5/6] Fixed wrong uid field in listing pvcs (#589) --- internal/resource/observer/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/resource/observer/utils.go b/internal/resource/observer/utils.go index 4347d04d6..2d9d824eb 100644 --- a/internal/resource/observer/utils.go +++ b/internal/resource/observer/utils.go @@ -141,7 +141,7 @@ func (m *OBServerManager) setRecoveryStatus() { func (m *OBServerManager) getPVCs() (*corev1.PersistentVolumeClaimList, error) { pvcs := &corev1.PersistentVolumeClaimList{} - err := m.K8sResClient.List(m.Ctx, pvcs, client.InNamespace(m.OBServer.Namespace), client.MatchingLabels{oceanbaseconst.LabelRefUID: m.OBServer.Labels[oceanbaseconst.LabelRefUID]}) + err := m.K8sResClient.List(m.Ctx, pvcs, client.InNamespace(m.OBServer.Namespace), client.MatchingLabels{oceanbaseconst.LabelRefUID: string(m.OBServer.UID)}) if err != nil { return nil, errors.Wrap(err, "list pvc") } @@ -667,7 +667,7 @@ func (m *OBServerManager) cleanWorkerK8sResource() error { pvc := &corev1.PersistentVolumeClaim{} if err := m.K8sResClient.DeleteAllOf(m.Ctx, pvc, client.InNamespace(m.OBServer.Namespace), - client.MatchingLabels{oceanbaseconst.LabelRefUID: m.OBServer.Labels[oceanbaseconst.LabelRefUID]}, + client.MatchingLabels{oceanbaseconst.LabelRefUID: string(m.OBServer.UID)}, ); err != nil { errs = stderrs.Join(errs, errors.Wrap(err, "Failed to delete pvc")) } From d3942818365a2ffb8f6e975c42f0685442035105 Mon Sep 17 00:00:00 2001 From: Chris Sun <85611200+chris-sun-star@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:28:23 +0800 Subject: [PATCH 6/6] add document for inter-cluster deployment (#591) * add document for inter-cluster deployment * fix(docs): fixed wrong doc link and doc title --------- Co-authored-by: yuyi --- .../100.high-availability-intro.md | 1 + .../700.inter-k8s-cluster-management.md | 95 ++++++++++++++++++ .../100.high-availability-intro.md | 1 + .../700.inter-k8s-cluster-management.md | 95 ++++++++++++++++++ .../img/inter-k8s-cluster-architecture.jpg | Bin 0 -> 108478 bytes 5 files changed, 192 insertions(+) create mode 100644 docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md create mode 100644 docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md create mode 100644 docsite/static/img/inter-k8s-cluster-architecture.jpg diff --git a/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md b/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md index 4351cc0ae..a1f8a2276 100644 --- a/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md +++ b/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md @@ -9,3 +9,4 @@ ob-operator ensures the high availability of data by using the following feature * Node fault recovery. The distributed architecture of OceanBase Database allows you to restore the service when a minority of nodes fail. By relying on certain network plugins, you can even restore the service from majority nodes failure. For more information, see [Recover from node failure](300.disaster-recovery-of-ob-operator.md). * Backup and restore of tenant data. The backup and restore feature of OceanBase Database allows you to back up tenant data to different storage media to ensure data safety. For more information, see [Back up a tenant](400.tenant-backup-of-ob-operator.md). * Primary and standby tenants. OceanBase Database allows you to create a standby tenant for the primary tenant. When a fault occurs to the primary tenant, you can quickly switch your business to the standby tenant to reduce the business interruption. For more information, see [Physical standby database](600.standby-tenant-of-ob-operator.md). +* Inter K8s cluster management. OceanBase can be deployed across multiple K8s cluster, this is a huge improment of high-availability and also gives the user more confident to operator the K8s cluster running OceanBase workloads. see [Inter K8s cluster management](700.inter-k8s-cluster-management.md). diff --git a/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md b/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md new file mode 100644 index 000000000..e204108d0 --- /dev/null +++ b/docsite/docs/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 6 +--- + +# Inter K8s cluster management + +:::tip +This feature is available in ob-operator version 2.3.0 and later. +Prerequisite: Pod and service connectivity must be established across all K8s clusters involved. +::: + +Deploying workloads across multiple K8s clusters enhances scalability, reliability, and security. By deploying zones (obzones) across different K8s clusters, you can fully leverage OceanBase's high-availability design. This approach provides disaster tolerance at the K8s cluster level, making operations more resilient. + +## Architecture +![inter-k8s-cluster-architecture](/img/inter-k8s-cluster-architecture.jpg) + +As shown in the architecture diagram, K8s clusters play different roles. The cluster running the ob-operator is referred to as the master cluster, while the other clusters are called worker clusters. +Worker clusters are registered by creating a custom resource of type K8sCluster in the master cluster. The ob-operator accesses these worker clusters using credentials stored in these resources. While OceanBase workloads run as native K8s resources in the worker clusters, the custom resources for OceanBase remain in the master cluster. + +## How to add `worker` K8s Cluster +To add a worker cluster, ensure the credentials used for access have permissions to `get`, `list`, `watch`, `create`, `update`, `patch` and `delete` resources of type `pod`, `service`, `pvc`, `job` and `namespace`. Follow the example below to create a K8sCluster resource by replacing the placeholder under kubeConfig with your worker cluster’s credentials, then apply it to the master cluster. + +```yaml k8s_cluster.yaml +apiVersion: k8s.oceanbase.com/v1alpha1 +kind: K8sCluster +metadata: + name: k8s-remote +spec: + name: remote + description: "This is the remote k8s cluster for testing" + kubeConfig: | + # Typically you can found it in ~/.kube/config +``` + +Verify the resource using the following command +```bash +kubectl get k8scluster +``` + +The expected output should look like this +```bash +NAME AGE CLUSTERNAME +k8s-remote 1m remote +``` + +## Create OceanBase Cluster across multiple K8s clusters +To create an OceanBase cluster across multiple K8s clusters, the only difference compared with deploy it in a single K8s cluster is to specify in which K8s cluster the obzone should be created, you may reference the following example. +```yaml multi-k8s-cluster.yaml +apiVersion: oceanbase.oceanbase.com/v1alpha1 +kind: OBCluster +metadata: + name: test + namespace: default + # annotations: + # "oceanbase.oceanbase.com/independent-pvc-lifecycle": "true" + # "oceanbase.oceanbase.com/mode": "service" +spec: + clusterName: test + clusterId: 1 + userSecrets: + root: root-password + topology: + - zone: zone1 + replica: 1 + - zone: zone2 + replica: 1 + k8sCluster: k8s-cluster-hz + - zone: zone3 + replica: 1 + k8sCluster: k8s-cluster-sh + observer: + image: oceanbase/oceanbase-cloud-native:4.2.1.7-107000162024060611 + resource: + cpu: 2 + memory: 10Gi + storage: + dataStorage: + storageClass: local-path + size: 50Gi + redoLogStorage: + storageClass: local-path + size: 50Gi + logStorage: + storageClass: local-path + size: 20Gi + parameters: + - name: system_memory + value: 1G + - name: "__min_full_resource_pool_memory" + value: "2147483648" # 2G +``` + +## Managing OceanBase Cluster in multiple K8s clusters +Managing an OceanBase cluster across multiple K8s clusters remains straightforward. Simply modify the custom resources in the master cluster, and the ob-operator will synchronize the changes with the relevant resources in the worker clusters. + diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md index 6471ae7e8..d9fa4807d 100644 --- a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/100.high-availability-intro.md @@ -9,3 +9,4 @@ ob-operator 利用 OceanBase 的若干特性来保证数据的高可用 * [节点故障恢复](300.disaster-recovery-of-ob-operator.md),基于 OceanBase 分布式的特性,可以从少数派节点故障的情况恢复,利用特定的网络插件甚至能实现全部节点故障的恢复。 * [租户数据备份恢复](400.tenant-backup-of-ob-operator.md),利用 OceanBase 的备份恢复能力,可以将租户的数据备份到其他存储介质,为数据提供更安全的保障。 * [主备租户](600.standby-tenant-of-ob-operator.md),利用 OceanBase 的主备租户能力,可以建立两个租户的主备关系,在故障发生时可以很快切换,能保证业务受到的影响更小。 +* [多 K8s 集群部署](700.inter-k8s-cluster-management.md),支持将一个 OceanBase 集群部署在多个 K8s 集群中, 可以显著的提高 OceanBase 的高可用能力,也给了用户更多信心来运维运行 OceanBase 负载的 K8s 集群。 diff --git a/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md new file mode 100644 index 000000000..d91942269 --- /dev/null +++ b/docsite/i18n/zh-Hans/docusaurus-plugin-content-docs/current/manual/500.ob-operator-user-guide/300.high-availability/700.inter-k8s-cluster-management.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 6 +--- + +# 多 K8s 集群部署 + +:::tip +此功能适用于 ob-operator 2.3.0 及更高版本。 +前提条件: 需要在所有 K8s 集群之间保证 Pod 和服务的互通。 +::: + +在多个 K8s 集群上部署工作负载可以增强系统的扩展性、可靠性和安全性。通过将不同的 obzone 部署到不同的 K8s 集群,可以充分发挥 OceanBase 高可用架构的优势,实现集群级别的容灾,并使集群和工作负载的管理更加简单高效。 + +## 整体架构 +![inter-k8s-cluster-architecture](/img/inter-k8s-cluster-architecture.jpg) + +如架构图所示,K8s 集群具有不同角色。我们将部署了 ob-operator 的集群称为主集群(master),其他集群称为工作集群(worker)。 + +通过在主集群中创建类型为 K8sCluster 的自定义资源,可以将工作集群注册进来。ob-operator 使用存储在自定义资源中的凭证访问这些工作集群。OceanBase 的工作负载在工作集群中以原生 K8s 资源运行,而 OceanBase 的自定义资源仍保存在主集群中。 + +## 添加工作 K8s 集群 +要将 K8s 集群添加为工作集群,请确保凭证具有以下权限:`get`, `list`, `watch`, `create`, `update`, `patch` 和 `delete` 以下资源,`pod`, `service`, `pvc`, `job` 和 `namespace`。参考以下示例,将 kubeConfig 下的占位符替换为工作集群的凭证,并将其应用于主集群。 +```yaml k8s_cluster.yaml +apiVersion: k8s.oceanbase.com/v1alpha1 +kind: K8sCluster +metadata: + name: k8s-remote +spec: + name: remote + description: "This is the remote k8s cluster for testing" + kubeConfig: | + # Typically you can found it in ~/.kube/config +``` + +使用以下命令检查资源 +```bash +kubectl get k8scluster +``` + +预期输出如下 +```bash +NAME AGE CLUSTERNAME +k8s-remote 1m remote +``` + +## 创建多个 K8s 集群中运行的 OceanBase 集群 + +要在多个 K8s 集群中创建 OceanBase 集群,与单 K8s 集群中创建集群唯一的区别是为运行在工作集群中的 obzone 指定运行的 K8s 集群。请参考以下示例配置 + +```yaml multi-k8s-cluster.yaml +apiVersion: oceanbase.oceanbase.com/v1alpha1 +kind: OBCluster +metadata: + name: test + namespace: default + # annotations: + # "oceanbase.oceanbase.com/independent-pvc-lifecycle": "true" + # "oceanbase.oceanbase.com/mode": "service" +spec: + clusterName: test + clusterId: 1 + userSecrets: + root: root-password + topology: + - zone: zone1 + replica: 1 + - zone: zone2 + replica: 1 + k8sCluster: k8s-cluster-hz + - zone: zone3 + replica: 1 + k8sCluster: k8s-cluster-sh + observer: + image: oceanbase/oceanbase-cloud-native:4.2.1.7-107000162024060611 + resource: + cpu: 2 + memory: 10Gi + storage: + dataStorage: + storageClass: local-path + size: 50Gi + redoLogStorage: + storageClass: local-path + size: 50Gi + logStorage: + storageClass: local-path + size: 20Gi + parameters: + - name: system_memory + value: 1G + - name: "__min_full_resource_pool_memory" + value: "2147483648" # 2G +``` +## 管理多 K8s 集群中运行的 OceanBase 集群 +OceanBase 的管理方式和单个 K8s 集群中运行没有其他区别,只需要修改主集群中的自定义资源,ob-operator 会负责将相应更改同步到各个工作集群中。 diff --git a/docsite/static/img/inter-k8s-cluster-architecture.jpg b/docsite/static/img/inter-k8s-cluster-architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7abc31b8da63e0433eb41bedce8a7d63fc241c5 GIT binary patch literal 108478 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 literal 0 HcmV?d00001