diff --git a/.secrets.baseline b/.secrets.baseline index fdc5c9870..09fc45748 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -208,7 +208,7 @@ "filename": "apis/clusters/v1beta1/cassandra_webhook.go", "hashed_secret": "e0a46b27231f798fe22dc4d5d82b5feeb5dcf085", "is_verified": false, - "line_number": 260 + "line_number": 266 } ], "apis/clusters/v1beta1/kafka_types.go": [ @@ -365,7 +365,7 @@ "filename": "apis/clusters/v1beta1/redis_webhook.go", "hashed_secret": "bc1c5ae5fd4a238d86261f422e62c489de408c22", "is_verified": false, - "line_number": 345 + "line_number": 351 } ], "apis/clusters/v1beta1/zookeeper_types.go": [ @@ -408,21 +408,21 @@ "filename": "apis/kafkamanagement/v1beta1/usercertificate_webhook.go", "hashed_secret": "3747c0c1bc4416dc2334f5aff52f3c9df602d92d", "is_verified": false, - "line_number": 45 + "line_number": 52 }, { "type": "Secret Keyword", "filename": "apis/kafkamanagement/v1beta1/usercertificate_webhook.go", "hashed_secret": "11495ec6584371b5d9982b538de7b47957781c13", "is_verified": false, - "line_number": 49 + "line_number": 56 }, { "type": "Secret Keyword", "filename": "apis/kafkamanagement/v1beta1/usercertificate_webhook.go", "hashed_secret": "7eb7eabdf6b5b4f62b12c2b706192d408f95a3c0", "is_verified": false, - "line_number": 62 + "line_number": 69 } ], "apis/kafkamanagement/v1beta1/zz_generated.deepcopy.go": [ @@ -1120,6 +1120,22 @@ "line_number": 186 } ], + "pkg/utils/user_creds_from_secret_test.go": [ + { + "type": "Secret Keyword", + "filename": "pkg/utils/user_creds_from_secret_test.go", + "hashed_secret": "46b77150f07f905116b6be9b7d29ea4b6c2daac8", + "is_verified": false, + "line_number": 34 + }, + { + "type": "Secret Keyword", + "filename": "pkg/utils/user_creds_from_secret_test.go", + "hashed_secret": "a27a9f290fd2247aaa3cc515001183f17c77a96d", + "is_verified": false, + "line_number": 84 + } + ], "scripts/cloud-init-secret.yaml": [ { "type": "Base64 High Entropy String", @@ -1130,5 +1146,5 @@ } ] }, - "generated_at": "2024-02-08T08:15:55Z" + "generated_at": "2024-02-09T14:24:18Z" } diff --git a/Makefile b/Makefile index 5f9893459..2b79f6a9a 100644 --- a/Makefile +++ b/Makefile @@ -82,8 +82,12 @@ test-users: test-webhooks: KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./apis/clusters/v1beta1 -coverprofile cover.out +.PHONY: test-utils +test-utils: + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./pkg/utils/... -coverprofile cover.out + .PHONY: test - test: manifests generate fmt vet docker-build-server-stub run-server-stub envtest test-clusters test-clusterresources test-kafkamanagement test-users stop-server-stub + test: manifests generate fmt vet docker-build-server-stub run-server-stub test-utils envtest test-clusters test-clusterresources test-kafkamanagement test-users stop-server-stub .PHONY: goimports goimports: diff --git a/PROJECT b/PROJECT index 865a17cd8..782b452e1 100644 --- a/PROJECT +++ b/PROJECT @@ -289,6 +289,7 @@ resources: version: v1beta1 webhooks: defaulting: true + validation: true webhookVersion: v1 - api: crdVersion: v1 @@ -299,6 +300,10 @@ resources: kind: Topic path: github.com/instaclustr/operator/apis/kafkamanagement/v1beta1 version: v1beta1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -308,9 +313,6 @@ resources: kind: PostgreSQLUser path: github.com/instaclustr/operator/apis/clusterresources/v1beta1 version: v1beta1 - webhooks: - validation: true - webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/apis/clusterresources/v1beta1/awsencryptionkey_webhook.go b/apis/clusterresources/v1beta1/awsencryptionkey_webhook.go index 991155195..939e6f67d 100644 --- a/apis/clusterresources/v1beta1/awsencryptionkey_webhook.go +++ b/apis/clusterresources/v1beta1/awsencryptionkey_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var awsencryptionkeylog = logf.Log.WithName("awsencryptionkey-resource") @@ -60,6 +61,11 @@ var _ webhook.Validator = &AWSEncryptionKey{} func (aws *AWSEncryptionKey) ValidateCreate() error { awsencryptionkeylog.Info("validate create", "name", aws.Name) + err := zerofieldvalidator.ValidateRequiredFields(aws.Spec) + if err != nil { + return err + } + aliasMatched, err := regexp.Match(models.EncryptionKeyAliasRegExp, []byte(aws.Spec.Alias)) if !aliasMatched || err != nil { return fmt.Errorf("AWS Encryption key alias must fit the pattern: %s, %v", models.EncryptionKeyAliasRegExp, err) diff --git a/apis/clusterresources/v1beta1/awsendpointserviceprincipal_webhook.go b/apis/clusterresources/v1beta1/awsendpointserviceprincipal_webhook.go index beb80939c..e1b68a915 100644 --- a/apis/clusterresources/v1beta1/awsendpointserviceprincipal_webhook.go +++ b/apis/clusterresources/v1beta1/awsendpointserviceprincipal_webhook.go @@ -24,6 +24,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -45,6 +47,11 @@ var principalArnPattern, _ = regexp.Compile(`^arn:aws:iam::[0-9]{12}:(root$|user func (r *AWSEndpointServicePrincipal) ValidateCreate() error { awsendpointserviceprincipallog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if (r.Spec.ClusterDataCenterID == "" && r.Spec.ClusterRef == nil) || (r.Spec.ClusterDataCenterID != "" && r.Spec.ClusterRef != nil) { return fmt.Errorf("only one of the following fields should be specified: dataCentreId, clusterRef") diff --git a/apis/clusterresources/v1beta1/awssecuritygroupfirewallrule_webhook.go b/apis/clusterresources/v1beta1/awssecuritygroupfirewallrule_webhook.go index 50626e7b7..5c92ec683 100644 --- a/apis/clusterresources/v1beta1/awssecuritygroupfirewallrule_webhook.go +++ b/apis/clusterresources/v1beta1/awssecuritygroupfirewallrule_webhook.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -60,6 +61,11 @@ var _ webhook.Validator = &AWSSecurityGroupFirewallRule{} func (r *AWSSecurityGroupFirewallRule) ValidateCreate() error { awssecuritygroupfirewallrulelog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if !validation.Contains(r.Spec.Type, models.BundleTypes) { return fmt.Errorf("type %s is unavailable, available types: %v", r.Spec.Type, models.BundleTypes) diff --git a/apis/clusterresources/v1beta1/awsvpcpeering_webhook.go b/apis/clusterresources/v1beta1/awsvpcpeering_webhook.go index 017fa5aff..bbf389718 100644 --- a/apis/clusterresources/v1beta1/awsvpcpeering_webhook.go +++ b/apis/clusterresources/v1beta1/awsvpcpeering_webhook.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var awsvpcpeeringlog = logf.Log.WithName("awsvpcpeering-resource") @@ -59,6 +60,11 @@ var _ webhook.Validator = &AWSVPCPeering{} func (r *AWSVPCPeering) ValidateCreate() error { awsvpcpeeringlog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if r.Spec.PeerAWSAccountID == "" { return fmt.Errorf("peer AWS Account ID is empty") } @@ -76,7 +82,7 @@ func (r *AWSVPCPeering) ValidateCreate() error { return fmt.Errorf("peer Subnets list is empty") } - err := r.Spec.Validate(models.AWSRegions) + err = r.Spec.Validate(models.AWSRegions) if err != nil { return err } diff --git a/apis/clusterresources/v1beta1/azurevnetpeering_webhook.go b/apis/clusterresources/v1beta1/azurevnetpeering_webhook.go index fd2ed5940..2bee7ecdd 100644 --- a/apis/clusterresources/v1beta1/azurevnetpeering_webhook.go +++ b/apis/clusterresources/v1beta1/azurevnetpeering_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var azurevnetpeeringlog = logf.Log.WithName("azurevnetpeering-resource") @@ -60,6 +61,11 @@ var _ webhook.Validator = &AzureVNetPeering{} func (r *AzureVNetPeering) ValidateCreate() error { azurevnetpeeringlog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if r.Spec.PeerResourceGroup == "" { return fmt.Errorf("peer Resource Group is empty") } @@ -81,7 +87,7 @@ func (r *AzureVNetPeering) ValidateCreate() error { return fmt.Errorf("peer Subnets list is empty") } - err := r.Spec.Validate() + err = r.Spec.Validate() if err != nil { return err } diff --git a/apis/clusterresources/v1beta1/cassandrauser_webhook.go b/apis/clusterresources/v1beta1/cassandrauser_webhook.go index 3d97865c3..bcb837aaf 100644 --- a/apis/clusterresources/v1beta1/cassandrauser_webhook.go +++ b/apis/clusterresources/v1beta1/cassandrauser_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -43,6 +44,11 @@ var _ webhook.Validator = &CassandraUser{} func (u *CassandraUser) ValidateCreate() error { cassandrauserlog.Info("validate create", "name", u.Name) + err := zerofieldvalidator.ValidateRequiredFields(u.Spec) + if err != nil { + return err + } + if u.Spec.SecretRef.Name == "" || u.Spec.SecretRef.Namespace == "" { return models.ErrEmptySecretRef } diff --git a/apis/clusterresources/v1beta1/clusterbackup_webhook.go b/apis/clusterresources/v1beta1/clusterbackup_webhook.go index 5f0e5ce26..833b4deba 100644 --- a/apis/clusterresources/v1beta1/clusterbackup_webhook.go +++ b/apis/clusterresources/v1beta1/clusterbackup_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -54,6 +55,11 @@ var _ webhook.Validator = &ClusterBackup{} func (r *ClusterBackup) ValidateCreate() error { clusterbackuplog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + _, ok := models.ClusterKindsMap[r.Spec.ClusterRef.ClusterKind] if !ok { return models.ErrUnsupportedBackupClusterKind diff --git a/apis/clusterresources/v1beta1/clusternetworkfirewallrule_webhook.go b/apis/clusterresources/v1beta1/clusternetworkfirewallrule_webhook.go index 88af77d3b..8b0d03097 100644 --- a/apis/clusterresources/v1beta1/clusternetworkfirewallrule_webhook.go +++ b/apis/clusterresources/v1beta1/clusternetworkfirewallrule_webhook.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -60,6 +61,11 @@ var _ webhook.Validator = &ClusterNetworkFirewallRule{} func (fr *ClusterNetworkFirewallRule) ValidateCreate() error { clusternetworkfirewallrulelog.Info("validate create", "name", fr.Name) + err := zerofieldvalidator.ValidateRequiredFields(fr.Spec) + if err != nil { + return err + } + if !validation.Contains(fr.Spec.Type, models.BundleTypes) { return fmt.Errorf("type %s is unavailable, available types: %v", fr.Spec.Type, models.BundleTypes) diff --git a/apis/clusterresources/v1beta1/exclusionwindow_webhook.go b/apis/clusterresources/v1beta1/exclusionwindow_webhook.go index ae3ef4d91..b5068141a 100644 --- a/apis/clusterresources/v1beta1/exclusionwindow_webhook.go +++ b/apis/clusterresources/v1beta1/exclusionwindow_webhook.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -55,6 +56,11 @@ var _ webhook.Validator = &ExclusionWindow{} func (r *ExclusionWindow) ValidateCreate() error { exclusionwindowlog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if (r.Spec.ClusterID == "" && r.Spec.ClusterRef == nil) || (r.Spec.ClusterID != "" && r.Spec.ClusterRef != nil) { return fmt.Errorf("only one of the following fields should be specified: clusterId, clusterRef") diff --git a/apis/clusterresources/v1beta1/gcpvpcpeering_webhook.go b/apis/clusterresources/v1beta1/gcpvpcpeering_webhook.go index 3ec5a6688..a179dc712 100644 --- a/apis/clusterresources/v1beta1/gcpvpcpeering_webhook.go +++ b/apis/clusterresources/v1beta1/gcpvpcpeering_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var gcpvpcpeeringlog = logf.Log.WithName("gcpvpcpeering-resource") @@ -60,6 +61,11 @@ var _ webhook.Validator = &GCPVPCPeering{} func (r *GCPVPCPeering) ValidateCreate() error { gcpvpcpeeringlog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if r.Spec.PeerVPCNetworkName == "" { return fmt.Errorf("peer VPC Network Name is empty") } @@ -77,7 +83,7 @@ func (r *GCPVPCPeering) ValidateCreate() error { return fmt.Errorf("peer Subnets list is empty") } - err := r.Spec.Validate() + err = r.Spec.Validate() if err != nil { return err } diff --git a/apis/clusterresources/v1beta1/maintenanceevents_webhook.go b/apis/clusterresources/v1beta1/maintenanceevents_webhook.go index feae7ba15..4601f9ef3 100644 --- a/apis/clusterresources/v1beta1/maintenanceevents_webhook.go +++ b/apis/clusterresources/v1beta1/maintenanceevents_webhook.go @@ -24,6 +24,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -44,6 +45,11 @@ var _ webhook.Validator = &MaintenanceEvents{} func (r *MaintenanceEvents) ValidateCreate() error { maintenanceeventslog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if err := r.ValidateMaintenanceEventsReschedules(); err != nil { return fmt.Errorf("maintenance events reschedules validation failed: %v", err) } diff --git a/apis/clusterresources/v1beta1/nodereload_webhook.go b/apis/clusterresources/v1beta1/nodereload_webhook.go index 45cb7637e..056e89f67 100644 --- a/apis/clusterresources/v1beta1/nodereload_webhook.go +++ b/apis/clusterresources/v1beta1/nodereload_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var nodereloadlog = logf.Log.WithName("nodereload-resource") @@ -45,6 +46,11 @@ var _ webhook.Validator = &NodeReload{} func (nr *NodeReload) ValidateCreate() error { nodereloadlog.Info("validate create", "name", nr.Name) + err := zerofieldvalidator.ValidateRequiredFields(nr.Spec) + if err != nil { + return err + } + if nr.Spec.Nodes == nil { return fmt.Errorf("nodes list is empty") } diff --git a/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go index 3313f6419..a026c52ba 100644 --- a/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go +++ b/apis/clusterresources/v1beta1/opensearchegressrules_webhook.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -49,6 +50,12 @@ var _ webhook.Validator = &OpenSearchEgressRules{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *OpenSearchEgressRules) ValidateCreate() error { opensearchegressruleslog.Info("validate create", "name", r.Name) + + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + matched, err := regexp.MatchString(models.OpenSearchBindingIDPattern, r.Spec.OpenSearchBindingID) if err != nil { return fmt.Errorf("can`t match openSearchBindingId to pattern: %s, error: %w", models.OpenSearchBindingIDPattern, err) diff --git a/apis/clusterresources/v1beta1/opensearchuser_webhook.go b/apis/clusterresources/v1beta1/opensearchuser_webhook.go index 85b02f353..f78d4ab75 100644 --- a/apis/clusterresources/v1beta1/opensearchuser_webhook.go +++ b/apis/clusterresources/v1beta1/opensearchuser_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -42,6 +43,11 @@ var _ webhook.Validator = &OpenSearchUser{} func (u *OpenSearchUser) ValidateCreate() error { opensearchuserlog.Info("validate create", "name", u.Name) + err := zerofieldvalidator.ValidateRequiredFields(u.Spec) + if err != nil { + return err + } + if u.Spec.SecretRef.Name == "" || u.Spec.SecretRef.Namespace == "" { return models.ErrEmptySecretRef } diff --git a/apis/clusterresources/v1beta1/redisuser_webhook.go b/apis/clusterresources/v1beta1/redisuser_webhook.go index 5cd16493f..f10900e02 100644 --- a/apis/clusterresources/v1beta1/redisuser_webhook.go +++ b/apis/clusterresources/v1beta1/redisuser_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -43,6 +44,11 @@ var _ webhook.Validator = &RedisUser{} func (r *RedisUser) ValidateCreate() error { redisuserlog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if r.Spec.SecretRef.Name == "" || r.Spec.SecretRef.Namespace == "" { return models.ErrEmptySecretRef } diff --git a/apis/clusters/v1beta1/cadence_webhook.go b/apis/clusters/v1beta1/cadence_webhook.go index f51a1078b..d4f914e3d 100644 --- a/apis/clusters/v1beta1/cadence_webhook.go +++ b/apis/clusters/v1beta1/cadence_webhook.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -84,7 +85,12 @@ func (cv *cadenceValidator) ValidateCreate(ctx context.Context, obj runtime.Obje cadencelog.Info("validate create", "name", c.Name) - err := c.Spec.Cluster.ValidateCreation() + err := zerofieldvalidator.ValidateRequiredFields(c.Spec) + if err != nil { + return err + } + + err = c.Spec.Cluster.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/cassandra_webhook.go b/apis/clusters/v1beta1/cassandra_webhook.go index 1cd98551b..5d0c4cae8 100644 --- a/apis/clusters/v1beta1/cassandra_webhook.go +++ b/apis/clusters/v1beta1/cassandra_webhook.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -79,6 +80,11 @@ func (cv *cassandraValidator) ValidateCreate(ctx context.Context, obj runtime.Ob cassandralog.Info("validate create", "name", c.Name) + err := zerofieldvalidator.ValidateRequiredFields(c.Spec) + if err != nil { + return err + } + if c.Spec.RestoreFrom != nil { if c.Spec.RestoreFrom.ClusterID == "" { return fmt.Errorf("restore clusterID field is empty") @@ -87,7 +93,7 @@ func (cv *cassandraValidator) ValidateCreate(ctx context.Context, obj runtime.Ob } } - err := c.Spec.GenericClusterSpec.ValidateCreation() + err = c.Spec.GenericClusterSpec.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/kafka_webhook.go b/apis/clusters/v1beta1/kafka_webhook.go index 3be0652f6..87ebadb2f 100644 --- a/apis/clusters/v1beta1/kafka_webhook.go +++ b/apis/clusters/v1beta1/kafka_webhook.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/utils/slices" "github.com/instaclustr/operator/pkg/validation" ) @@ -80,7 +81,12 @@ func (kv *kafkaValidator) ValidateCreate(ctx context.Context, obj runtime.Object kafkalog.Info("validate create", "name", k.Name) - err := k.Spec.GenericClusterSpec.ValidateCreation() + err := zerofieldvalidator.ValidateRequiredFields(k.Spec) + if err != nil { + return err + } + + err = k.Spec.GenericClusterSpec.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/kafkaconnect_webhook.go b/apis/clusters/v1beta1/kafkaconnect_webhook.go index 0f550f945..f4f757774 100644 --- a/apis/clusters/v1beta1/kafkaconnect_webhook.go +++ b/apis/clusters/v1beta1/kafkaconnect_webhook.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -84,7 +85,12 @@ func (kcv *kafkaConnectValidator) ValidateCreate(ctx context.Context, obj runtim kafkaconnectlog.Info("validate create", "name", kc.Name) - err := kc.Spec.Cluster.ValidateCreation() + err := zerofieldvalidator.ValidateRequiredFields(kc.Spec) + if err != nil { + return err + } + + err = kc.Spec.Cluster.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/opensearch_webhook.go b/apis/clusters/v1beta1/opensearch_webhook.go index fc3c69c3c..ff077d02e 100644 --- a/apis/clusters/v1beta1/opensearch_webhook.go +++ b/apis/clusters/v1beta1/opensearch_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -87,6 +88,11 @@ func (osv *openSearchValidator) ValidateCreate(ctx context.Context, obj runtime. opensearchlog.Info("validate create", "name", os.Name) + err := zerofieldvalidator.ValidateRequiredFields(os.Spec) + if err != nil { + return err + } + if os.Spec.RestoreFrom != nil { if os.Spec.RestoreFrom.ClusterID == "" { return fmt.Errorf("restore clusterID field is empty") @@ -95,7 +101,7 @@ func (osv *openSearchValidator) ValidateCreate(ctx context.Context, obj runtime. } } - err := os.Spec.ValidateCreation() + err = os.Spec.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/postgresql_webhook.go b/apis/clusters/v1beta1/postgresql_webhook.go index 0fdab47ac..3112d6d1d 100644 --- a/apis/clusters/v1beta1/postgresql_webhook.go +++ b/apis/clusters/v1beta1/postgresql_webhook.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -83,6 +84,11 @@ func (pgv *pgValidator) ValidateCreate(ctx context.Context, obj runtime.Object) postgresqllog.Info("validate create", "name", pg.Name) + err := zerofieldvalidator.ValidateRequiredFields(pg.Spec) + if err != nil { + return err + } + if pg.Spec.PgRestoreFrom != nil { if pg.Spec.PgRestoreFrom.ClusterID == "" { return fmt.Errorf("restore clusterID field is empty") @@ -91,7 +97,7 @@ func (pgv *pgValidator) ValidateCreate(ctx context.Context, obj runtime.Object) } } - err := pg.Spec.Cluster.ValidateCreation() + err = pg.Spec.Cluster.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/redis_webhook.go b/apis/clusters/v1beta1/redis_webhook.go index fa9aec8a5..4bb7ebd79 100644 --- a/apis/clusters/v1beta1/redis_webhook.go +++ b/apis/clusters/v1beta1/redis_webhook.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -92,6 +93,11 @@ func (rv *redisValidator) ValidateCreate(ctx context.Context, obj runtime.Object redislog.Info("validate create", "name", r.Name) + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + if r.Spec.RestoreFrom != nil { if r.Spec.RestoreFrom.ClusterID == "" { return fmt.Errorf("restore clusterID field is empty") @@ -100,7 +106,7 @@ func (rv *redisValidator) ValidateCreate(ctx context.Context, obj runtime.Object } } - err := r.Spec.Cluster.ValidateCreation() + err = r.Spec.Cluster.ValidateCreation() if err != nil { return err } diff --git a/apis/clusters/v1beta1/zookeeper_webhook.go b/apis/clusters/v1beta1/zookeeper_webhook.go index f2c64712b..0314ee458 100644 --- a/apis/clusters/v1beta1/zookeeper_webhook.go +++ b/apis/clusters/v1beta1/zookeeper_webhook.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" "github.com/instaclustr/operator/pkg/validation" ) @@ -80,7 +81,12 @@ func (zv *zookeeperValidator) ValidateCreate(ctx context.Context, obj runtime.Ob zookeeperlog.Info("validate create", "name", z.Name) - err := z.Spec.Cluster.ValidateCreation() + err := zerofieldvalidator.ValidateRequiredFields(z.Spec) + if err != nil { + return err + } + + err = z.Spec.Cluster.ValidateCreation() if err != nil { return err } diff --git a/apis/kafkamanagement/v1beta1/kafkaacl_webhook.go b/apis/kafkamanagement/v1beta1/kafkaacl_webhook.go index 46636ae35..e8e3cc796 100644 --- a/apis/kafkamanagement/v1beta1/kafkaacl_webhook.go +++ b/apis/kafkamanagement/v1beta1/kafkaacl_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) var kafkaacllog = logf.Log.WithName("kafkaacl-resource") @@ -57,7 +58,12 @@ var _ webhook.Validator = &KafkaACL{} func (kacl *KafkaACL) ValidateCreate() error { kafkaacllog.Info("validate create", "name", kacl.Name) - err := kacl.validateCreate() + err := zerofieldvalidator.ValidateRequiredFields(kacl.Spec) + if err != nil { + return err + } + + err = kacl.validateCreate() if err != nil { return err } diff --git a/apis/kafkamanagement/v1beta1/kafkauser_webhook.go b/apis/kafkamanagement/v1beta1/kafkauser_webhook.go index f1bde2420..d8dd15204 100644 --- a/apis/kafkamanagement/v1beta1/kafkauser_webhook.go +++ b/apis/kafkamanagement/v1beta1/kafkauser_webhook.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -58,6 +59,11 @@ var _ webhook.Validator = &KafkaUser{} func (ku *KafkaUser) ValidateCreate() error { kafkauserlog.Info("validate create", "name", ku.Name) + err := zerofieldvalidator.ValidateRequiredFields(ku.Spec) + if err != nil { + return err + } + return nil } diff --git a/apis/kafkamanagement/v1beta1/mirror_webhook.go b/apis/kafkamanagement/v1beta1/mirror_webhook.go index a2567c1c5..f13255f2d 100644 --- a/apis/kafkamanagement/v1beta1/mirror_webhook.go +++ b/apis/kafkamanagement/v1beta1/mirror_webhook.go @@ -17,11 +17,13 @@ limitations under the License. package v1beta1 import ( + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -47,3 +49,33 @@ func (r *Mirror) Default() { }) } } + +//+kubebuilder:webhook:path=/validate-kafkamanagement-instaclustr-com-v1beta1-mirror,mutating=false,failurePolicy=fail,sideEffects=None,groups=kafkamanagement.instaclustr.com,resources=mirrors,verbs=create;update,versions=v1beta1,name=vmirror.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Mirror{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Mirror) ValidateCreate() error { + mirrorlog.Info("validate create", "name", r.Name) + + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Mirror) ValidateUpdate(old runtime.Object) error { + mirrorlog.Info("validate update", "name", r.Name) + + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Mirror) ValidateDelete() error { + mirrorlog.Info("validate delete", "name", r.Name) + + return nil +} diff --git a/apis/kafkamanagement/v1beta1/topic_webhook.go b/apis/kafkamanagement/v1beta1/topic_webhook.go index 434f48c29..d17a946da 100644 --- a/apis/kafkamanagement/v1beta1/topic_webhook.go +++ b/apis/kafkamanagement/v1beta1/topic_webhook.go @@ -17,11 +17,13 @@ limitations under the License. package v1beta1 import ( + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/instaclustr/operator/pkg/models" + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -47,3 +49,35 @@ func (r *Topic) Default() { }) } } + +//+kubebuilder:webhook:path=/validate-kafkamanagement-instaclustr-com-v1beta1-topic,mutating=false,failurePolicy=fail,sideEffects=None,groups=kafkamanagement.instaclustr.com,resources=topics,verbs=create;update,versions=v1beta1,name=vtopic.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Topic{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Topic) ValidateCreate() error { + topiclog.Info("validate create", "name", r.Name) + + err := zerofieldvalidator.ValidateRequiredFields(r.Spec) + if err != nil { + return err + } + + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Topic) ValidateUpdate(old runtime.Object) error { + topiclog.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Topic) ValidateDelete() error { + topiclog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} diff --git a/apis/kafkamanagement/v1beta1/usercertificate_webhook.go b/apis/kafkamanagement/v1beta1/usercertificate_webhook.go index e49030d3e..429b8a1d0 100644 --- a/apis/kafkamanagement/v1beta1/usercertificate_webhook.go +++ b/apis/kafkamanagement/v1beta1/usercertificate_webhook.go @@ -23,6 +23,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/instaclustr/operator/pkg/utils/zerofieldvalidator" ) // log is for logging in this package. @@ -42,6 +44,11 @@ var _ webhook.Validator = &UserCertificate{} func (cert *UserCertificate) ValidateCreate() error { usercertificatelog.Info("validate create", "name", cert.Name) + err := zerofieldvalidator.ValidateRequiredFields(cert.Spec) + if err != nil { + return err + } + if cert.Spec.SecretRef == nil && cert.Spec.CertificateRequestTemplate == nil { return errors.New("one of the following fields should be set: spec.secretRef, spec.generateCSR") } diff --git a/config/samples/clusterresources_v1beta1_awsendpointserviceprincipal.yaml b/config/samples/clusterresources_v1beta1_awsendpointserviceprincipal.yaml index 5ccf94768..fa7c260da 100644 --- a/config/samples/clusterresources_v1beta1_awsendpointserviceprincipal.yaml +++ b/config/samples/clusterresources_v1beta1_awsendpointserviceprincipal.yaml @@ -4,9 +4,9 @@ metadata: name: awsendpointserviceprincipal-sample-1 spec: clusterDataCenterId: "1a2c55c7-ab88-4914-94eb-2f618809795c" - clusterRef: - name: redis-sample - namespace: default - clusterKind: Redis +# clusterRef: +# name: redis-sample +# namespace: default +# clusterKind: Redis # endPointServiceId: "249efcdb-6538-4563-bf7a-4a2f5f90de96" principalArn: "arn:aws:iam::152668027680:role/aws-principal-test-1" \ No newline at end of file diff --git a/config/samples/clusters_v1beta1_cadence.yaml b/config/samples/clusters_v1beta1_cadence.yaml index d9ad8069e..f1c9d2aaf 100644 --- a/config/samples/clusters_v1beta1_cadence.yaml +++ b/config/samples/clusters_v1beta1_cadence.yaml @@ -11,7 +11,7 @@ kind: Cadence metadata: name: cadence-sample spec: - name: "mykyta-cadence-test" + name: "username-cadence-test" version: "1.2.2" # standardProvisioning: # - targetCassandra: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index dd48c1fe6..880072b60 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -912,6 +912,46 @@ webhooks: resources: - kafkausers sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-kafkamanagement-instaclustr-com-v1beta1-mirror + failurePolicy: Fail + name: vmirror.kb.io + rules: + - apiGroups: + - kafkamanagement.instaclustr.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - mirrors + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-kafkamanagement-instaclustr-com-v1beta1-topic + failurePolicy: Fail + name: vtopic.kb.io + rules: + - apiGroups: + - kafkamanagement.instaclustr.com + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - topics + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/pkg/utils/dcomparison/map_diff_test.go b/pkg/utils/dcomparison/map_diff_test.go index c15e0e2c2..cacc7faf5 100644 --- a/pkg/utils/dcomparison/map_diff_test.go +++ b/pkg/utils/dcomparison/map_diff_test.go @@ -9,8 +9,6 @@ import ( ) func TestCompareMaps(t *testing.T) { - t.Parallel() - type _t struct { map1 map[string]any map2 map[string]any diff --git a/pkg/utils/user_creds_from_secret_test.go b/pkg/utils/user_creds_from_secret_test.go new file mode 100644 index 000000000..cc65a2ebc --- /dev/null +++ b/pkg/utils/user_creds_from_secret_test.go @@ -0,0 +1,89 @@ +package utils + +import ( + "testing" + + k8sCore "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/instaclustr/operator/pkg/models" +) + +func TestGetUserCreds(t *testing.T) { + type args struct { + secret *k8sCore.Secret + } + tests := []struct { + name string + args args + wantUsername string + wantPassword string + wantErr bool + }{ + { + name: "success", + args: args{secret: &k8sCore.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Immutable: nil, + Data: map[string][]byte{models.Username: []byte(models.Username), models.Password: []byte(models.Password)}, + StringData: nil, + Type: "", + }}, + wantUsername: models.Username, + wantPassword: models.Password, + wantErr: false, + }, + { + name: "no username", + args: args{secret: &k8sCore.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Immutable: nil, + Data: map[string][]byte{models.Password: []byte(models.Password)}, + StringData: nil, + Type: "", + }}, + wantErr: true, + }, + { + name: "no password", + args: args{secret: &k8sCore.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Immutable: nil, + Data: map[string][]byte{models.Username: []byte(models.Username)}, + StringData: nil, + Type: "", + }}, + wantErr: true, + }, + { + name: "empty map", + args: args{secret: &k8sCore.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{}, + Immutable: nil, + Data: map[string][]byte{}, + StringData: nil, + Type: "", + }}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotUsername, gotPassword, err := GetUserCreds(tt.args.secret) + if (err != nil) != tt.wantErr { + t.Errorf("GetUserCreds() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotUsername != tt.wantUsername { + t.Errorf("GetUserCreds() gotUsername = %v, want %v", gotUsername, tt.wantUsername) + } + if gotPassword != tt.wantPassword { + t.Errorf("GetUserCreds() gotPassword = %v, want %v", gotPassword, tt.wantPassword) + } + }) + } +} diff --git a/pkg/utils/zerofieldvalidator/validator.go b/pkg/utils/zerofieldvalidator/validator.go new file mode 100644 index 000000000..1b9ad53e9 --- /dev/null +++ b/pkg/utils/zerofieldvalidator/validator.go @@ -0,0 +1,139 @@ +package zerofieldvalidator + +import ( + "fmt" + "reflect" + "strings" +) + +func ValidateRequiredFields(v any) error { + val := reflect.ValueOf(v) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return fmt.Errorf("expected struct type - got the %s", val.Kind()) + } + + err := validateValue(val) + if err != nil { + return fmt.Errorf("error have occured: %w", err) + } + + return nil +} + +func validateValue(val reflect.Value) error { + kind := val.Kind() + _ = kind + switch val.Kind() { + case reflect.Struct: + return validateStruct(val) + + case reflect.Array, reflect.Slice: + return validateArrayOrSlice(val) + + case reflect.Map: + return validateMap(val) + + case reflect.Ptr: + return validatePointer(val) + + case reflect.Bool: + return nil + + default: + if val.IsZero() { + return fmt.Errorf("required field is empty") + } + } + return nil +} + +func isStructPtr(fieldValue reflect.Value) bool { + return fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() && fieldValue.Elem().Kind() == reflect.Struct +} + +func tagContainsOmitempty(tag string) bool { + return strings.Contains(tag, "omitempty") +} + +func validateStruct(val reflect.Value) error { + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + fieldValue := val.Field(i) + + if (fieldValue.Kind() == reflect.Array || fieldValue.Kind() == reflect.Slice) && fieldValue.Len() > 0 { + err := validateValue(fieldValue) + if err != nil { + return fmt.Errorf("%s, %w", field.Name, err) + } + } + + if tag, ok := field.Tag.Lookup("json"); ok && tagContainsOmitempty(tag) { + if fieldValue.Kind() == reflect.Ptr || fieldValue.Kind() == reflect.Map { + if !fieldValue.IsZero() { + err := validateValue(fieldValue) + if err != nil { + return fmt.Errorf("%s, %w", field.Name, err) + } + } + } + continue + } + + err := validateValue(fieldValue) + if err != nil { + return fmt.Errorf("%s, %w", field.Name, err) + } + } + + return nil +} + +func validateArrayOrSlice(val reflect.Value) error { + if val.Len() < 1 { + return fmt.Errorf("len is 0") + } + for i := 0; i < val.Len(); i++ { + fieldValue := val.Index(i) + if fieldValue.Kind() == reflect.Struct || + isStructPtr(fieldValue) { + err := validateValue(val.Index(i)) + if err != nil { + return err + } + } + + } + + return nil +} + +func validateMap(val reflect.Value) error { + if val.Len() < 1 { + return fmt.Errorf("empty map") + } + iter := val.MapRange() + for iter.Next() { + v := iter.Value() + if v.Kind() == reflect.Struct || + isStructPtr(v) { + err := validateValue(v) + if err != nil { + return err + } + } + } + + return nil +} + +func validatePointer(val reflect.Value) error { + if val.IsNil() { + return fmt.Errorf("pointer to nil") + } + return validateValue(val.Elem()) +} diff --git a/pkg/utils/zerofieldvalidator/validator_test.go b/pkg/utils/zerofieldvalidator/validator_test.go new file mode 100644 index 000000000..8602b4683 --- /dev/null +++ b/pkg/utils/zerofieldvalidator/validator_test.go @@ -0,0 +1,406 @@ +package zerofieldvalidator + +import "testing" + +type TestStruct struct { + StrField string `json:"str_field"` +} + +type EndlessStruct struct { + Struct *EndlessStruct `json:"struct"` +} + +func TestValidateRequiredFields(t *testing.T) { + tests := []struct { + name string + args any + wantErr bool + }{ + { + name: "empty required string", + args: struct { + Some string `json:"some"` + }{ + Some: "", + }, + wantErr: true, + }, + { + name: "filled required string", + args: struct { + Some string `json:"some"` + }{ + Some: "some", + }, + wantErr: false, + }, + { + name: "empty optional string", + args: struct { + Some string `json:"some,omitempty"` + }{ + Some: "", + }, + wantErr: false, + }, { + name: "filled optional string", + args: struct { + Some string `json:"some,omitempty"` + }{ + Some: "some", + }, + wantErr: false, + }, + { + name: "empty required int", + args: struct { + Some int `json:"some"` + }{ + Some: 0, + }, + wantErr: true, + }, + { + name: "filled required int", + args: struct { + Some int `json:"some"` + }{ + Some: 1, + }, + wantErr: false, + }, + { + name: "empty optional int", + args: struct { + Some int `json:"some,omitempty"` + }{ + Some: 0, + }, + wantErr: false, + }, + { + name: "filled optional int", + args: struct { + Some int `json:"some,omitempty"` + }{ + Some: 1, + }, + wantErr: false, + }, + { + name: "empty required slice", + args: struct { + Some []int `json:"some"` + }{ + Some: []int{}, + }, + wantErr: true, + }, + { + name: "filled required slice", + args: struct { + Some []int `json:"some"` + }{ + Some: []int{1, 2, 3}, + }, + wantErr: false, + }, + { + name: "empty optional slice", + args: struct { + Some []int `json:"some,omitempty"` + }{ + Some: []int{}, + }, + wantErr: false, + }, + { + name: "filled optional slice", + args: struct { + Some []int `json:"some,omitempty"` + }{ + Some: []int{1, 2, 3}, + }, + wantErr: false, + }, + { + name: "empty required embedded struct", + args: struct { + TestStruct `json:",inline"` + }{ + TestStruct: TestStruct{}, + }, + wantErr: true, + }, + { + name: "filled required embedded struct", + args: struct { + TestStruct `json:",inline"` + }{ + TestStruct: TestStruct{ + StrField: "some", + }, + }, + wantErr: false, + }, + { + name: "empty optional embedded struct", + args: struct { + TestStruct `json:",inline,omitempty"` + }{ + TestStruct: TestStruct{}, + }, + wantErr: false, + }, + { + name: "filled optional embedded struct", + args: struct { + TestStruct `json:",inline"` + }{ + TestStruct: TestStruct{ + StrField: "some", + }, + }, + wantErr: false, + }, + { + name: "empty required field in filled optional embedded struct", + args: struct { + TestStruct `json:",inline"` + }{ + TestStruct: TestStruct{ + StrField: "", + }, + }, + wantErr: true, + }, + { + name: "empty required pointer to struct", + args: struct { + Struct *TestStruct `json:"struct"` + }{}, + wantErr: true, + }, + { + name: "filled required pointer to struct", + args: struct { + Struct *TestStruct `json:"struct"` + }{ + Struct: &TestStruct{ + StrField: "some", + }, + }, + wantErr: false, + }, + { + name: "empty optional pointer to struct", + args: struct { + Struct *TestStruct `json:"struct,omitempty"` + }{}, + wantErr: false, + }, + { + name: "filled optional pointer to struct", + args: struct { + Struct *TestStruct `json:"struct,omitempty"` + }{ + Struct: &TestStruct{ + StrField: "some", + }, + }, + wantErr: false, + }, + { + name: "empty required field in filled optional pointer to struct", + args: struct { + Struct *TestStruct `json:"struct,omitempty"` + }{ + Struct: &TestStruct{ + StrField: "", + }, + }, + wantErr: true, + }, + { + name: "empty required field in filled required pointer to struct", + args: struct { + Struct *TestStruct `json:"struct"` + }{ + Struct: &TestStruct{ + StrField: "", + }, + }, + wantErr: true, + }, + { + name: "empty required slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some"` + }{ + Some: []*TestStruct{}, + }, + wantErr: true, + }, + { + name: "filled required slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some"` + }{ + Some: []*TestStruct{{ + StrField: "some", + }}, + }, + wantErr: false, + }, + { + name: "empty required field in filled required slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some"` + }{ + Some: []*TestStruct{{ + StrField: "", + }}, + }, + wantErr: true, + }, + { + name: "empty required field in filled optional slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some,omitempty"` + }{ + Some: []*TestStruct{{ + StrField: "", + }}, + }, + wantErr: true, + }, + { + name: "filled required field in filled optional slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some,omitempty"` + }{ + Some: []*TestStruct{{ + StrField: "some", + }}, + }, + wantErr: false, + }, + { + name: "filled optional slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some,omitempty"` + }{ + Some: []*TestStruct{{ + StrField: "some", + }}, + }, + wantErr: false, + }, + { + name: "empty optional slice of pointers to struct", + args: struct { + Some []*TestStruct `json:"some,omitempty"` + }{ + Some: []*TestStruct{}, + }, + wantErr: false, + }, + { + name: "empty required map of string to pointers on struct", + args: struct { + Some map[string]*TestStruct `json:"some"` + }{ + Some: map[string]*TestStruct{}, + }, + wantErr: true, + }, + { + name: "filled required map of string to pointers on struct", + args: struct { + Some map[string]*TestStruct `json:"some"` + }{ + Some: map[string]*TestStruct{"some": { + StrField: "some", + }}, + }, + wantErr: false, + }, + { + name: "empty required field in filled required map of string to pointers on struct", + args: struct { + Some map[string]*TestStruct `json:"some"` + }{ + Some: map[string]*TestStruct{"some": { + StrField: "", + }}, + }, + wantErr: true, + }, + { + name: "filled required field in filled optional map of string to pointers on struct", + args: struct { + Some map[string]*TestStruct `json:"some,omitempty"` + }{ + Some: map[string]*TestStruct{"some": { + StrField: "some", + }}, + }, + wantErr: false, + }, + { + name: "empty required field in filled optional map of string to pointers on struct", + args: struct { + Some map[string]*TestStruct `json:"some,omitempty"` + }{ + Some: map[string]*TestStruct{"some": { + StrField: "", + }}, + }, + wantErr: true, + }, + { + name: "empty required map", + args: struct { + Some map[string]string `json:"some"` + }{ + Some: map[string]string{}, + }, + wantErr: true, + }, + { + name: "filled required map", + args: struct { + Some map[string]string `json:"some"` + }{ + Some: map[string]string{"some": "some"}, + }, + wantErr: false, + }, + { + name: "empty optional map", + args: struct { + Some map[string]string `json:"some,omitempty"` + }{ + Some: nil, + }, + wantErr: false, + }, + { + name: "filed optional map", + args: struct { + Some map[string]string `json:"some,omitempty"` + }{ + Some: map[string]string{"some": "some"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateRequiredFields(tt.args); (err != nil) != tt.wantErr { + t.Errorf("ValidateRequiredFields() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}