diff --git a/.dockerignore b/.dockerignore index 243f81a50..5e506ce09 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ !**/*.go !**/*.mod !**/*.sum +distribution/ \ No newline at end of file diff --git a/PROJECT b/PROJECT index 811d9e754..b7db8c4d3 100644 --- a/PROJECT +++ b/PROJECT @@ -69,15 +69,6 @@ resources: defaulting: true validation: true webhookVersion: v1 -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: oceanbase.com - group: oceanbase - kind: OBClusterBackup - path: github.com/oceanbase/ob-operator/api/v1alpha1 - version: v1alpha1 - api: crdVersion: v1 namespaced: true @@ -93,7 +84,7 @@ resources: controller: true domain: oceanbase.com group: oceanbase - kind: OBClusterRestore + kind: OBTenantRestore path: github.com/oceanbase/ob-operator/api/v1alpha1 version: v1alpha1 - api: @@ -102,16 +93,20 @@ resources: controller: true domain: oceanbase.com group: oceanbase - kind: OBTenantRestore + kind: OBTenantBackupPolicy path: github.com/oceanbase/ob-operator/api/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true controller: true domain: oceanbase.com group: oceanbase - kind: OBTenantBackupPolicy + kind: OBTenantOperation path: github.com/oceanbase/ob-operator/api/v1alpha1 version: v1alpha1 webhooks: @@ -124,7 +119,7 @@ resources: controller: true domain: oceanbase.com group: oceanbase - kind: OBTenantOperation + kind: OBResourceRescue path: github.com/oceanbase/ob-operator/api/v1alpha1 version: v1alpha1 webhooks: diff --git a/api/v1alpha1/obresourcerescue_types.go b/api/v1alpha1/obresourcerescue_types.go new file mode 100644 index 000000000..5af8bab3f --- /dev/null +++ b/api/v1alpha1/obresourcerescue_types.go @@ -0,0 +1,71 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// OBResourceRescueSpec defines the desired state of OBResourceRescue +type OBResourceRescueSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + TargetKind string `json:"targetKind"` + TargetResName string `json:"targetResName"` + Type string `json:"type"` + TargetGV string `json:"targetGV,omitempty"` + Namespace string `json:"namespace,omitempty"` + TargetStatus string `json:"targetStatus,omitempty"` +} + +// OBResourceRescueStatus defines the observed state of OBResourceRescue +type OBResourceRescueStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + Status string `json:"status"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// OBResourceRescue is the Schema for the obresourcerescues API +type OBResourceRescue struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OBResourceRescueSpec `json:"spec,omitempty"` + Status OBResourceRescueStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OBResourceRescueList contains a list of OBResourceRescue +type OBResourceRescueList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OBResourceRescue `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OBResourceRescue{}, &OBResourceRescueList{}) +} diff --git a/api/v1alpha1/obresourcerescue_webhook.go b/api/v1alpha1/obresourcerescue_webhook.go new file mode 100644 index 000000000..c4495cbf4 --- /dev/null +++ b/api/v1alpha1/obresourcerescue_webhook.go @@ -0,0 +1,117 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var obresourcerescuelog = logf.Log.WithName("obresourcerescue-resource") + +var rescueTypeMapping = map[string]struct{}{ + "delete": {}, + "reset": {}, + "retry": {}, + "skip": {}, +} + +func (r *OBResourceRescue) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-oceanbase-oceanbase-com-v1alpha1-obresourcerescue,mutating=true,failurePolicy=fail,sideEffects=None,groups=oceanbase.oceanbase.com,resources=obresourcerescues,verbs=create;update,versions=v1alpha1,name=mobresourcerescue.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &OBResourceRescue{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *OBResourceRescue) Default() { + r.Spec.Type = strings.ToLower(r.Spec.Type) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-oceanbase-oceanbase-com-v1alpha1-obresourcerescue,mutating=false,failurePolicy=fail,sideEffects=None,groups=oceanbase.oceanbase.com,resources=obresourcerescues,verbs=create;update,versions=v1alpha1,name=vobresourcerescue.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &OBResourceRescue{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *OBResourceRescue) ValidateCreate() (admission.Warnings, error) { + return r.validateMutation() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *OBResourceRescue) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + oldRes := old.(*OBResourceRescue) + if r.Status.Status == "Successful" || r.Status.Status == "" { + if r.Spec.Type != oldRes.Spec.Type { + return nil, field.Invalid(field.NewPath("spec", "type"), r.Spec.Type, "type cannot be changed") + } + if r.Spec.TargetKind != oldRes.Spec.TargetKind { + return nil, field.Invalid(field.NewPath("spec", "targetKind"), r.Spec.TargetKind, "targetKind cannot be changed") + } + if r.Spec.TargetResName != oldRes.Spec.TargetResName { + return nil, field.Invalid(field.NewPath("spec", "targetResName"), r.Spec.TargetResName, "targetResName cannot be changed") + } + if r.Spec.TargetGV != oldRes.Spec.TargetGV { + return nil, field.Invalid(field.NewPath("spec", "targetGV"), r.Spec.TargetGV, "targetGV cannot be changed") + } + if r.Spec.Namespace != oldRes.Spec.Namespace { + return nil, field.Invalid(field.NewPath("spec", "namespace"), r.Spec.Namespace, "namespace cannot be changed") + } + if r.Spec.TargetStatus != oldRes.Spec.TargetStatus { + return nil, field.Invalid(field.NewPath("spec", "targetStatus"), r.Spec.TargetStatus, "targetStatus cannot be changed") + } + } + return r.validateMutation() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *OBResourceRescue) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +func (r *OBResourceRescue) validateMutation() (admission.Warnings, error) { + var errList field.ErrorList + var warnings []string + + if r.Spec.TargetKind == "" { + errList = append(errList, field.Required(field.NewPath("spec", "targetKind"), "targetKind is required")) + } + if r.Spec.TargetResName == "" { + errList = append(errList, field.Required(field.NewPath("spec", "targetResName"), "targetResName is required")) + } + if r.Spec.Type == "" { + errList = append(errList, field.Required(field.NewPath("spec", "type"), "type is required")) + } else if _, exist := rescueTypeMapping[r.Spec.Type]; !exist { + errList = append(errList, field.Invalid(field.NewPath("spec", "type"), r.Spec.Type, "unsupported rescue type")) + } else if r.Spec.Type == "reset" && r.Spec.TargetStatus == "" { + errList = append(errList, field.Required(field.NewPath("spec", "targetStatus"), "targetStatus is required when type is reset")) + } + + return warnings, errList.ToAggregate() +} diff --git a/api/v1alpha1/obresourcerescue_webhook_test.go b/api/v1alpha1/obresourcerescue_webhook_test.go new file mode 100644 index 000000000..3f9be7cf0 --- /dev/null +++ b/api/v1alpha1/obresourcerescue_webhook_test.go @@ -0,0 +1,123 @@ +/* +Copyright (c) 2023 OceanBase +ob-operator is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +*/ + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" +) + +var _ = Describe("OBResourceRescueWebhook", func() { + It("Validate create", func() { + rescue := newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + }) + + It("Validate wrong types", func() { + rescue := newOBResourceRescue() + rescue.Spec.Type = "wrong" + Expect(k8sClient.Create(ctx, rescue)).ShouldNot(Succeed()) + }) + + It("Validate update", func() { + rescue := newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.Type = "reset" + rescue.Spec.TargetStatus = "running" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + }) + + It("Validate target status field when type is reset", func() { + rescue := newOBResourceRescue() + rescue.Spec.Type = "reset" + Expect(k8sClient.Create(ctx, rescue)).ShouldNot(Succeed()) + rescue.Spec.TargetStatus = "Running" + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + }) + + It("Validate empty kind, resName, and type", func() { + rescue := newOBResourceRescue() + rescue.Spec.TargetKind = "" + Expect(k8sClient.Create(ctx, rescue)).ShouldNot(Succeed()) + rescue.Spec.TargetKind = "OBCluster" + rescue.Spec.TargetResName = "" + Expect(k8sClient.Create(ctx, rescue)).ShouldNot(Succeed()) + rescue.Spec.TargetResName = "test" + rescue.Spec.Type = "" + Expect(k8sClient.Create(ctx, rescue)).ShouldNot(Succeed()) + rescue.Spec.Type = "delete" + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + }) + + It("Validate forbidding to update a resource", func() { + rescue := newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.Type = "reset" + rescue.Spec.TargetStatus = "working" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + + rescue = newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.TargetKind = "OBTenant" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + + rescue = newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.TargetResName = "test2" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + + rescue = newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.Namespace = "test232" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + + rescue = newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.TargetGV = "oceanbase.oceanbase.com/v2" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + + rescue = newOBResourceRescue() + Expect(k8sClient.Create(ctx, rescue)).Should(Succeed()) + rescue.Spec.TargetStatus = "failed" + Expect(k8sClient.Update(ctx, rescue)).ShouldNot(Succeed()) + Expect(k8sClient.Delete(ctx, rescue)).Should(Succeed()) + }) +}) + +func newOBResourceRescue() *OBResourceRescue { + return &OBResourceRescue{ + ObjectMeta: metav1.ObjectMeta{ + Name: rand.String(10), + Namespace: defaultNamespace, + }, + Spec: OBResourceRescueSpec{ + TargetKind: "OBCluster", + TargetResName: "test", + Type: "delete", + }, + Status: OBResourceRescueStatus{ + Status: "Successful", + }, + } +} diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 6dab63f74..3a1e80942 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -124,6 +124,9 @@ var _ = BeforeSuite(func() { err = (&OBCluster{}).SetupWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) + err = (&OBResourceRescue{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:webhook go func() { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 352d23f9b..248b0bb9c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -344,6 +344,95 @@ func (in *OBParameterStatus) DeepCopy() *OBParameterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OBResourceRescue) DeepCopyInto(out *OBResourceRescue) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OBResourceRescue. +func (in *OBResourceRescue) DeepCopy() *OBResourceRescue { + if in == nil { + return nil + } + out := new(OBResourceRescue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OBResourceRescue) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OBResourceRescueList) DeepCopyInto(out *OBResourceRescueList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OBResourceRescue, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OBResourceRescueList. +func (in *OBResourceRescueList) DeepCopy() *OBResourceRescueList { + if in == nil { + return nil + } + out := new(OBResourceRescueList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OBResourceRescueList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OBResourceRescueSpec) DeepCopyInto(out *OBResourceRescueSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OBResourceRescueSpec. +func (in *OBResourceRescueSpec) DeepCopy() *OBResourceRescueSpec { + if in == nil { + return nil + } + out := new(OBResourceRescueSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OBResourceRescueStatus) DeepCopyInto(out *OBResourceRescueStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OBResourceRescueStatus. +func (in *OBResourceRescueStatus) DeepCopy() *OBResourceRescueStatus { + if in == nil { + return nil + } + out := new(OBResourceRescueStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OBServer) DeepCopyInto(out *OBServer) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 1fa17916a..c5aa9a6c9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -189,6 +189,10 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "OBTenantOperation") os.Exit(1) } + if err = (controller.NewOBResourceRescueReconciler(mgr)).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OBResourceRescue") + os.Exit(1) + } if os.Getenv("DISABLE_WEBHOOKS") != "true" { if err = (&v1alpha1.OBTenantBackupPolicy{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "OBTenantBackupPolicy") @@ -206,6 +210,10 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "OBCluster") os.Exit(1) } + if err = (&v1alpha1.OBResourceRescue{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "OBResourceRescue") + os.Exit(1) + } } //+kubebuilder:scaffold:builder diff --git a/config/crd/bases/oceanbase.oceanbase.com_obresourcerescues.yaml b/config/crd/bases/oceanbase.oceanbase.com_obresourcerescues.yaml new file mode 100644 index 000000000..b0b14617d --- /dev/null +++ b/config/crd/bases/oceanbase.oceanbase.com_obresourcerescues.yaml @@ -0,0 +1,76 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: obresourcerescues.oceanbase.oceanbase.com +spec: + group: oceanbase.oceanbase.com + names: + kind: OBResourceRescue + listKind: OBResourceRescueList + plural: obresourcerescues + singular: obresourcerescue + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: OBResourceRescue is the Schema for the obresourcerescues API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OBResourceRescueSpec defines the desired state of OBResourceRescue + properties: + namespace: + type: string + targetGV: + type: string + targetKind: + type: string + targetResName: + type: string + targetStatus: + type: string + type: + type: string + required: + - targetKind + - targetResName + - type + type: object + status: + description: OBResourceRescueStatus defines the observed state of OBResourceRescue + properties: + status: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + type: string + required: + - status + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1c7e7e2da..edb1ce99e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,12 +7,11 @@ resources: - bases/oceanbase.oceanbase.com_observers.yaml - bases/oceanbase.oceanbase.com_obparameters.yaml - bases/oceanbase.oceanbase.com_obtenants.yaml -- bases/oceanbase.oceanbase.com_obclusterbackups.yaml - bases/oceanbase.oceanbase.com_obtenantbackups.yaml -- bases/oceanbase.oceanbase.com_obclusterrestores.yaml - bases/oceanbase.oceanbase.com_obtenantrestores.yaml - bases/oceanbase.oceanbase.com_obtenantbackuppolicies.yaml - bases/oceanbase.oceanbase.com_obtenantoperations.yaml +- bases/oceanbase.oceanbase.com_obresourcerescues.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -23,12 +22,11 @@ patchesStrategicMerge: # - patches/webhook_in_observers.yaml # - patches/webhook_in_obparameters.yaml - patches/webhook_in_obtenants.yaml -# - patches/webhook_in_obclusterbackups.yaml # - patches/webhook_in_obtenantbackups.yaml -# - patches/webhook_in_obclusterrestores.yaml # - patches/webhook_in_obtenantrestores.yaml - patches/webhook_in_obtenantbackuppolicies.yaml - patches/webhook_in_obtenantoperations.yaml +- patches/webhook_in_obresourcerescues.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -38,12 +36,11 @@ patchesStrategicMerge: # - patches/cainjection_in_observers.yaml # - patches/cainjection_in_obparameters.yaml - patches/cainjection_in_obtenants.yaml -# - patches/cainjection_in_obclusterbackups.yaml # - patches/cainjection_in_obtenantbackups.yaml -# - patches/cainjection_in_obclusterrestores.yaml # - patches/cainjection_in_obtenantrestores.yaml - patches/cainjection_in_obtenantbackuppolicies.yaml - patches/cainjection_in_obtenantoperations.yaml +- patches/cainjection_in_obresourcerescues.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_obresourcerescues.yaml b/config/crd/patches/cainjection_in_obresourcerescues.yaml new file mode 100644 index 000000000..7b22c74cf --- /dev/null +++ b/config/crd/patches/cainjection_in_obresourcerescues.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: obresourcerescues.oceanbase.oceanbase.com diff --git a/config/crd/patches/webhook_in_obresourcerescues.yaml b/config/crd/patches/webhook_in_obresourcerescues.yaml new file mode 100644 index 000000000..e702436e2 --- /dev/null +++ b/config/crd/patches/webhook_in_obresourcerescues.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: obresourcerescues.oceanbase.oceanbase.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/obresourcerescue_editor_role.yaml b/config/rbac/obresourcerescue_editor_role.yaml new file mode 100644 index 000000000..d8218d04c --- /dev/null +++ b/config/rbac/obresourcerescue_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit obresourcerescues. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: obresourcerescue-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ob-operator + app.kubernetes.io/part-of: ob-operator + app.kubernetes.io/managed-by: kustomize + name: obresourcerescue-editor-role +rules: +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues/status + verbs: + - get diff --git a/config/rbac/obresourcerescue_viewer_role.yaml b/config/rbac/obresourcerescue_viewer_role.yaml new file mode 100644 index 000000000..039831d89 --- /dev/null +++ b/config/rbac/obresourcerescue_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view obresourcerescues. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: obresourcerescue-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: ob-operator + app.kubernetes.io/part-of: ob-operator + app.kubernetes.io/managed-by: kustomize + name: obresourcerescue-viewer-role +rules: +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues + verbs: + - get + - list + - watch +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b59362ec8..f4ea259fa 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -223,6 +223,32 @@ rules: - get - patch - update +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues/finalizers + verbs: + - update +- apiGroups: + - oceanbase.oceanbase.com + resources: + - obresourcerescues/status + verbs: + - get + - patch + - update - apiGroups: - oceanbase.oceanbase.com resources: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 95223b999..63b07653a 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -24,6 +24,26 @@ webhooks: resources: - obclusters sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-oceanbase-oceanbase-com-v1alpha1-obresourcerescue + failurePolicy: Fail + name: mobresourcerescue.kb.io + rules: + - apiGroups: + - oceanbase.oceanbase.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - obresourcerescues + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -110,6 +130,26 @@ webhooks: resources: - obclusters sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-oceanbase-oceanbase-com-v1alpha1-obresourcerescue + failurePolicy: Fail + name: vobresourcerescue.kb.io + rules: + - apiGroups: + - oceanbase.oceanbase.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - obresourcerescues + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/internal/controller/config/const.go b/internal/controller/config/const.go index c8c275305..d77a0fb32 100644 --- a/internal/controller/config/const.go +++ b/internal/controller/config/const.go @@ -18,10 +18,9 @@ const ( OBServerControllerName = "observer-controller" OBParameterControllerName = "obparameter-controller" OBTenantControllerName = "obtenant-controller" - OBClusterBackupControllerName = "obclusterbackup-controller" OBTenantBackupControllerName = "obtenantbackup-controller" - OBClusterRestoreControllerName = "obclusterrestore-controller" OBTenantRestoreControllerName = "obtenantrestore-controller" OBTenantBackupPolicyControllerName = "obtenantbackuppolicy-controller" OBTenantOperationControllerName = "obtenantoperation-controller" + OBResourceRescueControllerName = "obresourcerescue-controller" ) diff --git a/internal/controller/obresourcerescue_controller.go b/internal/controller/obresourcerescue_controller.go new file mode 100644 index 000000000..62243365c --- /dev/null +++ b/internal/controller/obresourcerescue_controller.go @@ -0,0 +1,193 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "errors" + + kubeerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/util/retry" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/oceanbase/ob-operator/api/v1alpha1" + ctlconfig "github.com/oceanbase/ob-operator/internal/controller/config" + "github.com/oceanbase/ob-operator/internal/telemetry" + taskstatus "github.com/oceanbase/ob-operator/pkg/task/const/status" +) + +// OBResourceRescueReconciler reconciles a OBResourceRescue object +type OBResourceRescueReconciler struct { + client.Client + Dynamic dynamic.Interface + Scheme *runtime.Scheme + Recorder telemetry.Recorder +} + +//+kubebuilder:rbac:groups=oceanbase.oceanbase.com,resources=obresourcerescues,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=oceanbase.oceanbase.com,resources=obresourcerescues/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=oceanbase.oceanbase.com,resources=obresourcerescues/finalizers,verbs=update + +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile +func (r *OBResourceRescueReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + rescue := v1alpha1.OBResourceRescue{} + if err := r.Client.Get(ctx, req.NamespacedName, &rescue); err != nil { + if kubeerrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if rescue.Status.Status == "Successful" { + return ctrl.Result{}, nil + } + + switch rescue.Spec.TargetKind { + case "OBCluster", "OBParameter", "OBServer", "OBZone", "OBTenant", "OBTenantBackupPolicy", "OBTenantBackup", "OBTenantRestore", "OBTenantOperation": + gvStr := v1alpha1.GroupVersion.String() + if rescue.Spec.TargetGV != "" { + gvStr = rescue.Spec.TargetGV + } + + gvk := schema.FromAPIVersionAndKind(gvStr, rescue.Spec.TargetKind) + mapping, err := r.Client.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + logger.Error(err, "failed to get REST mapping") + return ctrl.Result{}, err + } + + uns, err := r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).Get(ctx, rescue.Spec.TargetResName, metav1.GetOptions{}) + if err != nil { + logger.Error(err, "failed to get the target resource") + return ctrl.Result{}, err + } + + switch rescue.Spec.Type { + case "delete": + uns.SetFinalizers(nil) + _, err := r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).Update(ctx, uns, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update finalizers of the target resource") + return ctrl.Result{}, err + } + if uns.GetDeletionTimestamp() == nil { + err = r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).Delete(ctx, rescue.Spec.TargetResName, metav1.DeleteOptions{}) + if err != nil { + logger.Error(err, "failed to delete the target resource") + return ctrl.Result{}, err + } + } + case "reset": + err := errors.Join( + unstructured.SetNestedField(uns.Object, nil, "status", "operationContext"), + unstructured.SetNestedField(uns.Object, rescue.Spec.TargetStatus, "status", "status"), + ) + if err != nil { + logger.Error(err, "failed to reset fields of the target resource") + return ctrl.Result{}, err + } + _, err = r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).UpdateStatus(ctx, uns, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update status of the target resource") + return ctrl.Result{}, err + } + case "retry": + // operationContext.TaskStatus = taskstatus.Pending + // operationContext.FailureRule.RetryCount = 0 + err := errors.Join( + unstructured.SetNestedField(uns.Object, "Pending", "status", "operationContext", "taskStatus"), + unstructured.SetNestedField(uns.Object, 0, "status", "operationContext", "failureRule", "retryCount"), + ) + if err != nil { + logger.Error(err, "failed to set operationContext fields of the target resource") + return ctrl.Result{}, err + } + _, err = r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).UpdateStatus(ctx, uns, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update status of the target resource") + return ctrl.Result{}, err + } + case "skip": + // operationContext.TaskStatus = taskstatus.Successful + // When coordinator finds that the task status is `successful`, it will go on the following steps. + err = unstructured.SetNestedField(uns.Object, taskstatus.Successful, "status", "operationContext", "taskStatus") + if err != nil { + logger.Error(err, "failed to reset fields of the target resource") + return ctrl.Result{}, err + } + _, err = r.Dynamic.Resource(mapping.Resource).Namespace(rescue.GetNamespace()).UpdateStatus(ctx, uns, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "failed to update status of the target resource") + return ctrl.Result{}, err + } + } + + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := r.Client.Get(ctx, req.NamespacedName, &rescue); err != nil { + if kubeerrors.IsNotFound(err) { + return nil + } + return err + } + rescue.Status.Status = "Successful" + return r.Client.Status().Update(ctx, &rescue) + }) + + if err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OBResourceRescueReconciler) SetupWithManager(mgr ctrl.Manager) error { + kubeconfig, err := config.GetConfig() + if err != nil { + return err + } + + r.Dynamic, err = dynamic.NewForConfig(kubeconfig) + if err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + WithEventFilter(preds). + For(&v1alpha1.OBResourceRescue{}). + Complete(r) +} + +func NewOBResourceRescueReconciler(mgr ctrl.Manager) *OBResourceRescueReconciler { + return &OBResourceRescueReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: telemetry.NewRecorder(context.Background(), mgr.GetEventRecorderFor(ctlconfig.OBResourceRescueControllerName)), + } +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go new file mode 100644 index 000000000..f8de60098 --- /dev/null +++ b/internal/controller/suite_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + oceanbasev1alpha1 "github.com/oceanbase/ob-operator/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = oceanbasev1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +})