Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge changes in 2.3.0_release #585

Merged
merged 6 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 4 additions & 4 deletions README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) 提供, 需要提前进行安装并确保其存储目的地有足够大的磁盘空间。如果您计划在生产环境部署,推荐使用其他的存储解决方案。我们在[存储兼容性](#存储兼容性)一节提供了我们测试过的存储兼容性结果。
Expand All @@ -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
```

- 开发版本
Expand All @@ -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
Expand Down Expand Up @@ -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 之后表示集群创建和初始化成功:
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion api/v1alpha1/obcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
11 changes: 7 additions & 4 deletions api/v1alpha1/obcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -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 = ""
Expand All @@ -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() {
Expand Down
122 changes: 73 additions & 49 deletions api/v1alpha1/obtenant_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1alpha1

import (
"context"
"errors"
"fmt"
"regexp"
"strings"
Expand Down Expand Up @@ -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
}
}
}
Expand All @@ -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
}
72 changes: 37 additions & 35 deletions api/v1alpha1/obtenantbackuppolicy_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading