From 2bda6fdca357c185452dd7e20fa07abbc5dfb05e Mon Sep 17 00:00:00 2001 From: zyy17 Date: Sat, 21 Sep 2024 00:11:10 +0800 Subject: [PATCH] refactor: add `Validate()` and `Check()` Signed-off-by: zyy17 --- apis/v1alpha1/validate.go | 318 ++++++++++++++++++ controllers/greptimedbcluster/controller.go | 126 +------ .../greptimedbstandalone/controller.go | 103 +----- 3 files changed, 330 insertions(+), 217 deletions(-) create mode 100644 apis/v1alpha1/validate.go diff --git a/apis/v1alpha1/validate.go b/apis/v1alpha1/validate.go new file mode 100644 index 00000000..049e2b84 --- /dev/null +++ b/apis/v1alpha1/validate.go @@ -0,0 +1,318 @@ +// Copyright 2024 Greptime Team +// +// 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 ( + "context" + "fmt" + + "github.com/pelletier/go-toml" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Validate checks the GreptimeDBCluster and returns an error if it is invalid. +func (in *GreptimeDBCluster) Validate() error { + if in == nil { + return nil + } + + if err := in.validateFrontend(); err != nil { + return err + } + + if err := in.validateMeta(); err != nil { + return err + } + + if err := in.validateDatanode(); err != nil { + return err + } + + if in.GetFlownode() != nil { + if err := in.validateFlownode(); err != nil { + return err + } + } + + if in.GetWALProvider() != nil { + if err := validateWALProvider(in.GetWALProvider()); err != nil { + return err + } + } + + if in.GetObjectStorageProvider() != nil { + if err := valiateStorageProvider(in.GetObjectStorageProvider()); err != nil { + return err + } + } + + return nil +} + +// Check checks the GreptimeDBCluster with other resources and returns an error if it is invalid. +func (in *GreptimeDBCluster) Check(ctx context.Context, client client.Client) error { + // Check if the TLS secret exists and contains the required keys. + if secretName := in.GetFrontend().GetTLS().GetSecretName(); secretName != "" { + if err := checkTLSSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + // Check if the PodMonitor CRD exists. + if in.GetPrometheusMonitor().IsEnablePrometheusMonitor() { + if err := checkPodMonitorExists(ctx, client); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetS3Storage().GetSecretName(); secretName != "" { + if err := checkS3CredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetOSSStorage().GetSecretName(); secretName != "" { + if err := checkOSSCredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetGCSStorage().GetSecretName(); secretName != "" { + if err := checkGCSCredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + return nil +} + +func (in *GreptimeDBCluster) validateFrontend() error { + if err := validateTomlConfig(in.GetFrontend().GetConfig()); err != nil { + return fmt.Errorf("invalid frontend toml config: '%v'", err) + } + return nil +} + +func (in *GreptimeDBCluster) validateMeta() error { + if err := validateTomlConfig(in.GetMeta().GetConfig()); err != nil { + return fmt.Errorf("invalid meta toml config: '%v'", err) + } + + if in.GetMeta().IsEnableRegionFailover() { + if in.GetWALProvider().GetKafkaWAL() == nil { + return fmt.Errorf("meta enable region failover requires kafka WAL") + } + } + + return nil +} + +func (in *GreptimeDBCluster) validateDatanode() error { + if err := validateTomlConfig(in.GetDatanode().GetConfig()); err != nil { + return fmt.Errorf("invalid datanode toml config: '%v'", err) + } + return nil +} + +func (in *GreptimeDBCluster) validateFlownode() error { + if err := validateTomlConfig(in.GetFlownode().GetConfig()); err != nil { + return fmt.Errorf("invalid flownode toml config: '%v'", err) + } + return nil +} + +// Validate checks the GreptimeDBStandalone and returns an error if it is invalid. +func (in *GreptimeDBStandalone) Validate() error { + if in == nil { + return nil + } + + if err := validateTomlConfig(in.GetConfig()); err != nil { + return fmt.Errorf("invalid toml config: '%v'", err) + } + + if wal := in.GetWALProvider(); wal != nil { + if err := validateWALProvider(wal); err != nil { + return err + } + } + + if ocp := in.GetObjectStorageProvider(); ocp != nil { + if err := valiateStorageProvider(ocp); err != nil { + return err + } + } + + return nil +} + +// Check checks the GreptimeDBStandalone with other resources and returns an error if it is invalid. +func (in *GreptimeDBStandalone) Check(ctx context.Context, client client.Client) error { + // Check if the TLS secret exists and contains the required keys. + if secretName := in.GetTLS().GetSecretName(); secretName != "" { + if err := checkTLSSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + // Check if the PodMonitor CRD exists. + if in.GetPrometheusMonitor().IsEnablePrometheusMonitor() { + if err := checkPodMonitorExists(ctx, client); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetS3Storage().GetSecretName(); secretName != "" { + if err := checkS3CredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetOSSStorage().GetSecretName(); secretName != "" { + if err := checkOSSCredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + if secretName := in.GetObjectStorageProvider().GetGCSStorage().GetSecretName(); secretName != "" { + if err := checkGCSCredentialsSecret(ctx, client, in.GetNamespace(), secretName); err != nil { + return err + } + } + + return nil +} + +func validateTomlConfig(input string) error { + if len(input) > 0 { + data := make(map[string]interface{}) + err := toml.Unmarshal([]byte(input), &data) + if err != nil { + return err + } + } + return nil +} + +func validateWALProvider(input *WALProviderSpec) error { + if input == nil { + return nil + } + + if input.RaftEngineWAL != nil && input.KafkaWAL != nil { + return fmt.Errorf("only one of 'raftEngine' or 'kafka' can be set") + } + + if input.RaftEngineWAL != nil { + if err := validateFileStorage(input.RaftEngineWAL.FileStorage); err != nil { + return err + } + } + + return nil +} + +func valiateStorageProvider(input *ObjectStorageProviderSpec) error { + if input == nil { + return nil + } + + if input.getSetObjectStorageCount() > 1 { + return fmt.Errorf("only one storage provider can be set") + } + + if fs := input.GetCacheFileStorage(); fs != nil { + if err := validateFileStorage(fs); err != nil { + return err + } + } + + return nil +} + +func validateFileStorage(input *FileStorage) error { + if input == nil { + return nil + } + + if input.Name == "" { + return fmt.Errorf("name is required in file storage") + } + + if input.MountPath == "" { + return fmt.Errorf("mountPath is required in file storage") + } + + if input.StorageSize == "" { + return fmt.Errorf("storageSize is required in file storage") + } + + return nil +} + +// checkTLSSecret checks if the secret exists and contains the required keys. +func checkTLSSecret(ctx context.Context, client client.Client, namespace, name string) error { + return checkSecretData(ctx, client, namespace, name, []string{TLSCrtSecretKey, TLSKeySecretKey}) +} + +func checkGCSCredentialsSecret(ctx context.Context, client client.Client, namespace, name string) error { + return checkSecretData(ctx, client, namespace, name, []string{ServiceAccountKey}) +} + +func checkOSSCredentialsSecret(ctx context.Context, client client.Client, namespace, name string) error { + return checkSecretData(ctx, client, namespace, name, []string{AccessKeyIDSecretKey, SecretAccessKeySecretKey}) +} + +func checkS3CredentialsSecret(ctx context.Context, client client.Client, namespace, name string) error { + return checkSecretData(ctx, client, namespace, name, []string{AccessKeyIDSecretKey, SecretAccessKeySecretKey}) +} + +// checkPodMonitorExists checks if the PodMonitor CRD exists. +func checkPodMonitorExists(ctx context.Context, client client.Client) error { + const ( + kind = "podmonitors" + group = "monitoring.coreos.com" + ) + + var crd apiextensionsv1.CustomResourceDefinition + if err := client.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s.%s", kind, group)}, &crd); err != nil { + return err + } + + return nil +} + +// checkSecretData checks if the secret exists and contains the required keys. +func checkSecretData(ctx context.Context, client client.Client, namespace, name string, keys []string) error { + var secret corev1.Secret + if err := client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil { + return err + } + + if secret.Data == nil { + return fmt.Errorf("the data of secret '%s/%s' is empty", namespace, name) + } + + for _, key := range keys { + if _, ok := secret.Data[key]; !ok { + return fmt.Errorf("secret '%s/%s' does not have key '%s'", namespace, name, key) + } + } + + return nil +} diff --git a/controllers/greptimedbcluster/controller.go b/controllers/greptimedbcluster/controller.go index 6186c83f..cc548856 100644 --- a/controllers/greptimedbcluster/controller.go +++ b/controllers/greptimedbcluster/controller.go @@ -18,17 +18,12 @@ import ( "context" "errors" "fmt" - "strings" "time" - "github.com/pelletier/go-toml" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" @@ -133,7 +128,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if err = r.validate(ctx, cluster); err != nil { + if err = cluster.Validate(); err != nil { + r.Recorder.Event(cluster, corev1.EventTypeWarning, "InvalidCluster", fmt.Sprintf("Invalid cluster: %v", err)) + return ctrl.Result{}, err + } + + if err = cluster.Check(ctx, r.Client); err != nil { r.Recorder.Event(cluster, corev1.EventTypeWarning, "InvalidCluster", fmt.Sprintf("Invalid cluster: %v", err)) return ctrl.Result{}, err } @@ -264,120 +264,6 @@ func (r *Reconciler) updateClusterStatus(ctx context.Context, cluster *v1alpha1. return deployers.UpdateStatus(ctx, cluster, r.Client) } -func (r *Reconciler) validate(ctx context.Context, cluster *v1alpha1.GreptimeDBCluster) error { - if cluster.Spec.Meta == nil && cluster.Spec.Datanode == nil && cluster.Spec.Frontend == nil && cluster.Spec.Flownode == nil { - return fmt.Errorf("no components spec in cluster") - } - - if cluster.Spec.Frontend != nil && cluster.Spec.Frontend.TLS != nil { - if len(cluster.Spec.Frontend.TLS.SecretName) > 0 { - tlsSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: cluster.Spec.Frontend.TLS.SecretName}, tlsSecret) - if err != nil { - return fmt.Errorf("get tls secret '%s' failed, error: '%v'", cluster.Spec.Frontend.TLS.SecretName, err) - } - - if _, ok := tlsSecret.Data[v1alpha1.TLSCrtSecretKey]; !ok { - return fmt.Errorf("tls secret '%s' does not contain key '%s'", cluster.Spec.Frontend.TLS.SecretName, v1alpha1.TLSCrtSecretKey) - } - - if _, ok := tlsSecret.Data[v1alpha1.TLSKeySecretKey]; !ok { - return fmt.Errorf("tls secret '%s' does not contain key '%s'", cluster.Spec.Frontend.TLS.SecretName, v1alpha1.TLSKeySecretKey) - } - } - } - - // To detect if the CRD of podmonitor is installed. - if cluster.Spec.PrometheusMonitor != nil { - if cluster.Spec.PrometheusMonitor.Enabled { - // CheckPodMonitorCRDInstall is used to check if the CRD of podmonitor is installed, it is not used to create the podmonitor. - err := r.checkPodMonitorCRDInstall(ctx, metav1.GroupKind{ - Group: "monitoring.coreos.com", - Kind: "PodMonitor", - }) - if err != nil { - if k8serrors.IsNotFound(err) { - return fmt.Errorf("the crd podmonitors.monitoring.coreos.com is not installed") - } else { - return fmt.Errorf("check crd of podmonitors.monitoring.coreos.com is installed error: %v", err) - } - } - } - } - - if cluster.Spec.Meta != nil { - if err := r.validateTomlConfig(cluster.Spec.Meta.Config); err != nil { - return fmt.Errorf("invalid meta toml config: %v", err) - } - if cluster.GetMeta().IsEnableRegionFailover() { - if cluster.GetWALProvider().GetKafkaWAL() == nil { - return fmt.Errorf("meta enable region failover requires kafka WAL") - } - } - } - - if cluster.Spec.Datanode != nil { - if err := r.validateTomlConfig(cluster.Spec.Datanode.Config); err != nil { - return fmt.Errorf("invalid datanode toml config: %v", err) - } - } - - if cluster.Spec.Frontend != nil { - if err := r.validateTomlConfig(cluster.Spec.Frontend.Config); err != nil { - return fmt.Errorf("invalid frontend toml config: %v", err) - } - } - - if cluster.Spec.Flownode != nil { - if err := r.validateTomlConfig(cluster.Spec.Flownode.Config); err != nil { - return fmt.Errorf("invalid flownode toml config: %v", err) - } - } - - if cluster.Spec.ObjectStorageProvider != nil { - checkProviders := func() bool { - providers := []bool{ - cluster.Spec.ObjectStorageProvider.S3 != nil, - cluster.Spec.ObjectStorageProvider.OSS != nil, - cluster.Spec.ObjectStorageProvider.GCS != nil, - } - providerCount := 0 - for _, p := range providers { - if p { - providerCount++ - } - } - return providerCount > 1 - }() - if checkProviders { - return fmt.Errorf("only one object storage provider can be specified") - } - } - - return nil -} - -func (r *Reconciler) validateTomlConfig(input string) error { - if len(input) > 0 { - data := make(map[string]interface{}) - err := toml.Unmarshal([]byte(input), &data) - if err != nil { - return err - } - } - return nil -} - -func (r *Reconciler) checkPodMonitorCRDInstall(ctx context.Context, groupKind metav1.GroupKind) error { - var crd apiextensionsv1.CustomResourceDefinition - nameNamespace := types.NamespacedName{Name: fmt.Sprintf("%ss.%s", strings.ToLower(groupKind.Kind), groupKind.Group)} - err := r.Get(ctx, nameNamespace, &crd) - if err != nil { - return err - } - return nil -} - func (r *Reconciler) recordNormalEventByPhase(cluster *v1alpha1.GreptimeDBCluster) { switch cluster.Status.ClusterPhase { case v1alpha1.PhaseStarting: diff --git a/controllers/greptimedbstandalone/controller.go b/controllers/greptimedbstandalone/controller.go index 686bb80e..d64dc565 100644 --- a/controllers/greptimedbstandalone/controller.go +++ b/controllers/greptimedbstandalone/controller.go @@ -18,17 +18,12 @@ import ( "context" "errors" "fmt" - "strings" "time" - "github.com/pelletier/go-toml" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" @@ -113,7 +108,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - if err = r.validate(ctx, standalone); err != nil { + if err = standalone.Validate(); err != nil { + r.Recorder.Event(standalone, corev1.EventTypeWarning, "InvalidStandalone", fmt.Sprintf("Invalid standalone: %v", err)) + return ctrl.Result{}, err + } + + if err = standalone.Check(ctx, r.Client); err != nil { r.Recorder.Event(standalone, corev1.EventTypeWarning, "InvalidStandalone", fmt.Sprintf("Invalid standalone: %v", err)) return ctrl.Result{}, err } @@ -240,97 +240,6 @@ func (r *Reconciler) setStandaloneStatus(ctx context.Context, standalone *v1alph return UpdateStatus(ctx, standalone, r.Client) } -func (r *Reconciler) validate(ctx context.Context, standalone *v1alpha1.GreptimeDBStandalone) error { - if standalone.Spec.Base == nil { - return fmt.Errorf("no components spec in standalone") - } - - if standalone.Spec.TLS != nil { - if len(standalone.Spec.TLS.SecretName) > 0 { - tlsSecret := &corev1.Secret{} - err := r.Get(ctx, types.NamespacedName{Namespace: standalone.Namespace, Name: standalone.Spec.TLS.SecretName}, tlsSecret) - if err != nil { - return fmt.Errorf("get tls secret '%s' failed, error: '%v'", standalone.Spec.TLS.SecretName, err) - } - - if _, ok := tlsSecret.Data[v1alpha1.TLSCrtSecretKey]; !ok { - return fmt.Errorf("tls secret '%s' does not contain key '%s'", standalone.Spec.TLS.SecretName, v1alpha1.TLSCrtSecretKey) - } - - if _, ok := tlsSecret.Data[v1alpha1.TLSKeySecretKey]; !ok { - return fmt.Errorf("tls secret '%s' does not contain key '%s'", standalone.Spec.TLS.SecretName, v1alpha1.TLSKeySecretKey) - } - } - } - - // To detect if the CRD of podmonitor is installed. - if standalone.Spec.PrometheusMonitor != nil { - if standalone.Spec.PrometheusMonitor.Enabled { - // CheckPodMonitorCRDInstall is used to check if the CRD of podmonitor is installed, it is not used to create the podmonitor. - err := r.checkPodMonitorCRDInstall(ctx, metav1.GroupKind{ - Group: "monitoring.coreos.com", - Kind: "PodMonitor", - }) - if err != nil { - if k8serrors.IsNotFound(err) { - return fmt.Errorf("the crd podmonitors.monitoring.coreos.com is not installed") - } else { - return fmt.Errorf("check crd of podmonitors.monitoring.coreos.com is installed error: %v", err) - } - } - } - } - - if len(standalone.Spec.Config) > 0 { - if err := r.validateTomlConfig(standalone.Spec.Config); err != nil { - return fmt.Errorf("invalid meta toml config: %v", err) - } - } - - if standalone.Spec.ObjectStorageProvider != nil { - checkProviders := func() bool { - providers := []bool{ - standalone.Spec.ObjectStorageProvider.S3 != nil, - standalone.Spec.ObjectStorageProvider.OSS != nil, - standalone.Spec.ObjectStorageProvider.GCS != nil, - } - providerCount := 0 - for _, p := range providers { - if p { - providerCount++ - } - } - return providerCount > 1 - }() - if checkProviders { - return fmt.Errorf("only one object storage provider can be specified") - } - } - - return nil -} - -func (r *Reconciler) validateTomlConfig(input string) error { - if len(input) > 0 { - data := make(map[string]interface{}) - err := toml.Unmarshal([]byte(input), &data) - if err != nil { - return err - } - } - return nil -} - -func (r *Reconciler) checkPodMonitorCRDInstall(ctx context.Context, groupKind metav1.GroupKind) error { - var crd apiextensionsv1.CustomResourceDefinition - nameNamespace := types.NamespacedName{Name: fmt.Sprintf("%ss.%s", strings.ToLower(groupKind.Kind), groupKind.Group)} - err := r.Get(ctx, nameNamespace, &crd) - if err != nil { - return err - } - return nil -} - func (r *Reconciler) recordNormalEventByPhase(standalone *v1alpha1.GreptimeDBStandalone) { switch standalone.Status.StandalonePhase { case v1alpha1.PhaseStarting: