diff --git a/apis/v1alpha1/defaulting_test.go b/apis/v1alpha1/defaulting_test.go index aae96d47..6a5df34a 100644 --- a/apis/v1alpha1/defaulting_test.go +++ b/apis/v1alpha1/defaulting_test.go @@ -26,7 +26,7 @@ import ( func TestClusterSetDefaults(t *testing.T) { const ( - testDir = "testdata/greptimedbcluster" + testDir = "testdata/defaulting/greptimedbcluster" inputFileName = "input.yaml" expectFileName = "expect.yaml" ) @@ -86,7 +86,7 @@ func TestClusterSetDefaults(t *testing.T) { func TestStandaloneSetDefaults(t *testing.T) { const ( - testDir = "testdata/greptimedbstandalone" + testDir = "testdata/defaulting/greptimedbstandalone" inputFileName = "input.yaml" expectFileName = "expect.yaml" ) diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test00/expect.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test00/expect.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test00/expect.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test00/expect.yaml diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test00/input.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test00/input.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test00/input.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test00/input.yaml diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test01/expect.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test01/expect.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test01/expect.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test01/expect.yaml diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test01/input.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test01/input.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test01/input.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test01/input.yaml diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test02/expect.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test02/expect.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test02/expect.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test02/expect.yaml diff --git a/apis/v1alpha1/testdata/greptimedbcluster/test02/input.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbcluster/test02/input.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbcluster/test02/input.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbcluster/test02/input.yaml diff --git a/apis/v1alpha1/testdata/greptimedbstandalone/test00/expect.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbstandalone/test00/expect.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbstandalone/test00/expect.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbstandalone/test00/expect.yaml diff --git a/apis/v1alpha1/testdata/greptimedbstandalone/test00/input.yaml b/apis/v1alpha1/testdata/defaulting/greptimedbstandalone/test00/input.yaml similarity index 100% rename from apis/v1alpha1/testdata/greptimedbstandalone/test00/input.yaml rename to apis/v1alpha1/testdata/defaulting/greptimedbstandalone/test00/input.yaml diff --git a/apis/v1alpha1/testdata/validation/greptimedbcluster/test00/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbcluster/test00/input.yaml new file mode 100644 index 00000000..86ebd650 --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbcluster/test00/input.yaml @@ -0,0 +1,17 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBCluster +metadata: + name: test00 + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest + frontend: + replicas: 1 + meta: + etcdEndpoints: + - etcd.etcd-cluster.svc.cluster.local:2379 + replicas: 1 + datanode: + replicas: 3 diff --git a/apis/v1alpha1/testdata/validation/greptimedbcluster/test01/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbcluster/test01/input.yaml new file mode 100644 index 00000000..cdedf3a4 --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbcluster/test01/input.yaml @@ -0,0 +1,20 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBCluster +metadata: + name: test01-error + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest + frontend: + replicas: 1 + # This is an error because the config is not a valid toml config. + config: | + It's not a valid toml config---- + meta: + etcdEndpoints: + - etcd.etcd-cluster.svc.cluster.local:2379 + replicas: 1 + datanode: + replicas: 3 diff --git a/apis/v1alpha1/testdata/validation/greptimedbcluster/test02/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbcluster/test02/input.yaml new file mode 100644 index 00000000..bc4ec247 --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbcluster/test02/input.yaml @@ -0,0 +1,23 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBCluster +metadata: + name: test02-error + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest + frontend: + replicas: 1 + meta: + etcdEndpoints: + - etcd.etcd-cluster.svc.cluster.local:2379 + replicas: 1 + datanode: + replicas: 3 + # This is an error because multiple objectStorage configs are not allowed. + objectStorage: + s3: + bucket: "greptimedb" + oss: + bucket: "greptimedb" diff --git a/apis/v1alpha1/testdata/validation/greptimedbcluster/test03/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbcluster/test03/input.yaml new file mode 100644 index 00000000..bbe22cce --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbcluster/test03/input.yaml @@ -0,0 +1,28 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBCluster +metadata: + name: test03-error + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest + frontend: + replicas: 1 + meta: + etcdEndpoints: + - etcd.etcd-cluster.svc.cluster.local:2379 + replicas: 1 + datanode: + replicas: 3 + wal: + # This is an error because multiple wal configs are not allowed. + raftEngine: + fs: + storageClassName: io2 # Use io2 storage class for WAL for better performance. + name: wal + storageSize: 5Gi + mountPath: /wal + kafka: + brokerEndpoints: + - "kafka-bootstrap.kafka.svc.cluster.local:9092" diff --git a/apis/v1alpha1/testdata/validation/greptimedbstandalone/test00/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbstandalone/test00/input.yaml new file mode 100644 index 00000000..ff47d584 --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbstandalone/test00/input.yaml @@ -0,0 +1,9 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBStandalone +metadata: + name: test00 + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest diff --git a/apis/v1alpha1/testdata/validation/greptimedbstandalone/test01/input.yaml b/apis/v1alpha1/testdata/validation/greptimedbstandalone/test01/input.yaml new file mode 100644 index 00000000..b58c768e --- /dev/null +++ b/apis/v1alpha1/testdata/validation/greptimedbstandalone/test01/input.yaml @@ -0,0 +1,12 @@ +apiVersion: greptime.io/v1alpha1 +kind: GreptimeDBStandalone +metadata: + name: test00-error + namespace: default +spec: + base: + main: + image: greptime/greptimedb:latest + # This is an error because the config is not a valid toml config. + config: | + It's not a valid toml config---- diff --git a/apis/v1alpha1/validate.go b/apis/v1alpha1/validate.go new file mode 100644 index 00000000..c3231a55 --- /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 wal := in.GetWALProvider(); wal != nil { + if err := validateWALProvider(wal); err != nil { + return err + } + } + + if osp := in.GetObjectStorageProvider(); osp != nil { + if err := validateObjectStorageProvider(osp); 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 standalone toml config: '%v'", err) + } + + if wal := in.GetWALProvider(); wal != nil { + if err := validateWALProvider(wal); err != nil { + return err + } + } + + if osp := in.GetObjectStorageProvider(); osp != nil { + if err := validateObjectStorageProvider(osp); 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.GetRaftEngineWAL() != nil && input.GetKafkaWAL() != nil { + return fmt.Errorf("only one of 'raftEngine' or 'kafka' can be set") + } + + if fs := input.GetRaftEngineWAL().GetFileStorage(); fs != nil { + if err := validateFileStorage(fs); err != nil { + return err + } + } + + return nil +} + +func validateObjectStorageProvider(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.GetName() == "" { + return fmt.Errorf("name is required in file storage") + } + + if input.GetMountPath() == "" { + return fmt.Errorf("mountPath is required in file storage") + } + + if input.GetSize() == "" { + 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/apis/v1alpha1/validate_test.go b/apis/v1alpha1/validate_test.go new file mode 100644 index 00000000..16d6bf2d --- /dev/null +++ b/apis/v1alpha1/validate_test.go @@ -0,0 +1,93 @@ +// 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 ( + "os" + "path/filepath" + "strings" + "testing" + + "sigs.k8s.io/yaml" +) + +func TestClusterValidation(t *testing.T) { + const ( + testDir = "testdata/validation/greptimedbcluster" + inputFileName = "input.yaml" + ) + + entries, err := os.ReadDir(testDir) + if err != nil { + t.Fatal(err) + } + + for _, entry := range entries { + if entry.IsDir() { + inputFile := filepath.Join(testDir, entry.Name(), inputFileName) + inputData, err := os.ReadFile(inputFile) + if err != nil { + t.Errorf("failed to read %s: %v", inputFile, err) + } + + var input GreptimeDBCluster + + if err := yaml.Unmarshal(inputData, &input); err != nil { + t.Fatalf("failed to unmarshal %s: %v", inputFile, err) + } + + if err := input.Validate(); err != nil && !expectError(input.GetName()) { + t.Errorf("expected no error, but got %v", err) + } + } + } +} + +func TestStandaloneValidation(t *testing.T) { + const ( + testDir = "testdata/validation/greptimedbstandalone" + inputFileName = "input.yaml" + ) + + entries, err := os.ReadDir(testDir) + if err != nil { + t.Fatal(err) + } + + for _, entry := range entries { + if entry.IsDir() { + inputFile := filepath.Join(testDir, entry.Name(), inputFileName) + inputData, err := os.ReadFile(inputFile) + if err != nil { + t.Errorf("failed to read %s: %v", inputFile, err) + } + + var input GreptimeDBStandalone + + if err := yaml.Unmarshal(inputData, &input); err != nil { + t.Fatalf("failed to unmarshal %s: %v", inputFile, err) + } + + if err := input.Validate(); err != nil && !expectError(input.GetName()) { + t.Errorf("expected no error, but got %v", err) + } + } + } +} + +// If the resource name contains the word "error", we expect an error in the validation. +func expectError(name string) bool { + return strings.Contains(name, "error") +} diff --git a/controllers/greptimedbcluster/controller.go b/controllers/greptimedbcluster/controller.go index 8783f763..9ab13af3 100644 --- a/controllers/greptimedbcluster/controller.go +++ b/controllers/greptimedbcluster/controller.go @@ -18,18 +18,13 @@ import ( "context" "errors" "fmt" - "strings" "time" "github.com/google/go-cmp/cmp" - "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" @@ -134,7 +129,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 } @@ -267,120 +267,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 ba38eb85..799e589d 100644 --- a/controllers/greptimedbstandalone/controller.go +++ b/controllers/greptimedbstandalone/controller.go @@ -18,18 +18,13 @@ import ( "context" "errors" "fmt" - "strings" "time" "github.com/google/go-cmp/cmp" - "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" @@ -114,7 +109,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 } @@ -244,97 +244,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: