diff --git a/internal/resourcepolicies/resource_policies_test.go b/internal/resourcepolicies/resource_policies_test.go index 72253eaf9..e8fa6ad11 100644 --- a/internal/resourcepolicies/resource_policies_test.go +++ b/internal/resourcepolicies/resource_policies_test.go @@ -212,6 +212,18 @@ volumePolicies: pvcAccessModes: ReadWriteOnce action: type: skip +`, + wantErr: true, + }, + { + name: "error format of pvcAccessModes (list with non-string)", + yamlData: `version: v1 +volumePolicies: + - conditions: + pvcAccessModes: + - 123 + action: + type: skip `, wantErr: true, }, @@ -410,14 +422,20 @@ func TestGetResourceMatchedAction(t *testing.T) { } func TestGetResourcePoliciesFromConfig(t *testing.T) { - // Create a test ConfigMap - cm := &corev1api.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-configmap", - Namespace: "test-namespace", - }, - Data: map[string]string{ - "test-data": `version: v1 + testCases := []struct { + name string + cm *corev1api.ConfigMap + expectedErr string + }{ + { + name: "valid configmap", + cm: &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v1 volumePolicies: - conditions: capacity: '0,10Gi' @@ -438,68 +456,102 @@ volumePolicies: action: type: skip `, + }, + }, + expectedErr: "", + }, + { + name: "nil configmap", + cm: nil, + expectedErr: "could not parse config from nil configmap", + }, + { + name: "empty data configmap", + cm: &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{}, + }, + expectedErr: "illegal resource policies test-namespace/test-configmap configmap", + }, + { + name: "multiple data configmap", + cm: &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "data1": "value1", + "data2": "value2", + }, + }, + expectedErr: "illegal resource policies test-namespace/test-configmap configmap", + }, + { + name: "invalid yaml data", + cm: &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v1 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: skip + invalid-key: value +`, + }, + }, + expectedErr: "failed to decode yaml data into resource policies", + }, + { + name: "build policy error", + cm: &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v1 +volumePolicies: + - conditions: + capacity: 'invalid-capacity' + csi: + driver: disks.csi.driver + action: + type: skip +`, + }, + }, + expectedErr: "wrong format of Capacity invalid-capacity", }, } - // Call the function and check for errors - resPolicies, err := getResourcePoliciesFromConfig(cm) - require.NoError(t, err) - - // Check that the returned resourcePolicies object contains the expected data - assert.Equal(t, "v1", resPolicies.version) - - assert.Len(t, resPolicies.volumePolicies, 3) - - policies := ResourcePolicies{ - Version: "v1", - VolumePolicies: []VolumePolicy{ - { - Conditions: map[string]any{ - "capacity": "0,10Gi", - "csi": map[string]any{ - "driver": "disks.csi.driver", - }, - }, - Action: Action{ - Type: Skip, - }, - }, - { - Conditions: map[string]any{ - "csi": map[string]any{ - "driver": "files.csi.driver", - "volumeAttributes": map[string]string{"protocol": "nfs"}, - }, - }, - Action: Action{ - Type: Skip, - }, - }, - { - Conditions: map[string]any{ - "pvcLabels": map[string]string{ - "environment": "production", - }, - }, - Action: Action{ - Type: Skip, - }, - }, - }, + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resPolicies, err := getResourcePoliciesFromConfig(tc.cm) + if tc.expectedErr == "" { + require.NoError(t, err) + assert.Equal(t, "v1", resPolicies.version) + assert.Len(t, resPolicies.volumePolicies, 3) + } else { + require.ErrorContains(t, err, tc.expectedErr) + assert.Nil(t, resPolicies) + } + }) } - - p := &Policies{} - err = p.BuildPolicy(&policies) - if err != nil { - t.Fatalf("failed to build policy: %v", err) - } - - assert.Equal(t, p, resPolicies) } -func TestGetResourcePoliciesFromRestore(t *testing.T) { - // Create a test ConfigMap - cm := &corev1api.ConfigMap{ +func TestGetResourcePoliciesFromBackup(t *testing.T) { + validCM := &corev1api.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "test-configmap", Namespace: "test-namespace", @@ -517,27 +569,353 @@ volumePolicies: }, } - // Create a fake client - client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cm).Build() - logger := logrus.New() - - restore := velerov1api.Restore{ + invalidActionCM := &corev1api.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-action-configmap", Namespace: "test-namespace", - Name: "test-restore", }, - Spec: velerov1api.RestoreSpec{ - ResourcePolicy: &corev1api.TypedLocalObjectReference{ - Kind: ConfigmapRefType, - Name: "test-configmap", - }, + Data: map[string]string{ + "test-data": `version: v1 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: invalid-action +`, }, } - resPolicies, err := GetResourcePoliciesFromRestore(context.Background(), &restore, client, logger) - require.NoError(t, err) - assert.Equal(t, "v1", resPolicies.version) - assert.Len(t, resPolicies.volumePolicies, 1) + invalidVersionCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-version-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v2 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: skip +`, + }, + } + + emptyCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-configmap", + Namespace: "test-namespace", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(validCM, invalidActionCM, invalidVersionCM, emptyCM).Build() + logger := logrus.New() + + testCases := []struct { + name string + backup velerov1api.Backup + expectedErr string + }{ + { + name: "valid configmap", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "test-configmap", + }, + }, + }, + expectedErr: "", + }, + { + name: "invalid kind", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: "Secret", + Name: "test-configmap", + }, + }, + }, + expectedErr: "", + }, + { + name: "configmap not found", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "non-existent-configmap", + }, + }, + }, + expectedErr: "fail to get ResourcePolicies test-namespace/non-existent-configmap ConfigMap", + }, + { + name: "invalid action configmap", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "invalid-action-configmap", + }, + }, + }, + expectedErr: "fail to validate ResourcePolicies in ConfigMap test-namespace/test-backup", + }, + { + name: "invalid version configmap", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "invalid-version-configmap", + }, + }, + }, + expectedErr: "fail to validate ResourcePolicies in ConfigMap test-namespace/test-backup", + }, + { + name: "empty configmap", + backup: velerov1api.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-backup", + }, + Spec: velerov1api.BackupSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "empty-configmap", + }, + }, + }, + expectedErr: "fail to read the ResourcePolicies from ConfigMap test-namespace/test-backup", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resPolicies, err := GetResourcePoliciesFromBackup(tc.backup, client, logger) + if tc.expectedErr == "" { + require.NoError(t, err) + if tc.backup.Spec.ResourcePolicy != nil && tc.backup.Spec.ResourcePolicy.Kind == ConfigmapRefType { + assert.NotNil(t, resPolicies) + } else { + assert.Nil(t, resPolicies) + } + } else { + require.ErrorContains(t, err, tc.expectedErr) + assert.Nil(t, resPolicies) + } + }) + } +} + +func TestGetResourcePoliciesFromRestore(t *testing.T) { + validCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v1 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: skip +`, + }, + } + + invalidActionCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-action-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v1 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: invalid-action +`, + }, + } + + invalidVersionCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-version-configmap", + Namespace: "test-namespace", + }, + Data: map[string]string{ + "test-data": `version: v2 +volumePolicies: + - conditions: + capacity: '0,10Gi' + csi: + driver: disks.csi.driver + action: + type: skip +`, + }, + } + + emptyCM := &corev1api.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-configmap", + Namespace: "test-namespace", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(validCM, invalidActionCM, invalidVersionCM, emptyCM).Build() + logger := logrus.New() + + testCases := []struct { + name string + restore *velerov1api.Restore + expectedErr string + }{ + { + name: "valid configmap", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "test-configmap", + }, + }, + }, + expectedErr: "", + }, + { + name: "invalid kind", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: "Secret", + Name: "test-configmap", + }, + }, + }, + expectedErr: "invalid ResourcePolicy kind \"Secret\", only \"configmap\" is supported", + }, + { + name: "configmap not found", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "non-existent-configmap", + }, + }, + }, + expectedErr: "fail to get ResourcePolicies test-namespace/non-existent-configmap ConfigMap", + }, + { + name: "invalid action configmap", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "invalid-action-configmap", + }, + }, + }, + expectedErr: "fail to validate ResourcePolicies in ConfigMap test-namespace/invalid-action-configmap", + }, + { + name: "invalid version configmap", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "invalid-version-configmap", + }, + }, + }, + expectedErr: "fail to validate ResourcePolicies in ConfigMap test-namespace/invalid-version-configmap", + }, + { + name: "empty configmap", + restore: &velerov1api.Restore{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-restore", + }, + Spec: velerov1api.RestoreSpec{ + ResourcePolicy: &corev1api.TypedLocalObjectReference{ + Kind: ConfigmapRefType, + Name: "empty-configmap", + }, + }, + }, + expectedErr: "fail to read the ResourcePolicies from ConfigMap test-namespace/empty-configmap", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resPolicies, err := GetResourcePoliciesFromRestore(context.Background(), tc.restore, client, logger) + if tc.expectedErr == "" { + require.NoError(t, err) + assert.NotNil(t, resPolicies) + } else { + require.ErrorContains(t, err, tc.expectedErr) + assert.Nil(t, resPolicies) + } + }) + } } func TestGetMatchAction(t *testing.T) { @@ -1834,6 +2212,17 @@ namespacedFilterPolicies: wantErr: true, errMsg: "invalid glob pattern", }, + { + name: "invalid - bad glob pattern in excludedNames", + yamlData: `version: v1 +namespacedFilterPolicies: +- namespaces: ["test"] + resourceFilters: + - kinds: ["Pod"] + excludedNames: ["[invalid"]`, + wantErr: true, + errMsg: "invalid glob pattern", + }, { name: "invalid - duplicate namespace pattern", yamlData: `version: v1 @@ -1847,6 +2236,16 @@ namespacedFilterPolicies: wantErr: true, errMsg: "duplicate namespace pattern", }, + { + name: "invalid - bad namespace pattern", + yamlData: `version: v1 +namespacedFilterPolicies: +- namespaces: ["prod**uction"] + resourceFilters: + - kinds: ["Pod"]`, + wantErr: true, + errMsg: "wildcard pattern contains consecutive asterisks", + }, } for _, tc := range testCases { @@ -1903,6 +2302,50 @@ namespacedFilterPolicies: assert.Equal(t, map[string]string{"app": "web"}, rf.LabelSelector) } +func TestClusterScopedFilterPoliciesAccessor(t *testing.T) { + yamlData := `version: v1 +clusterScopedFilterPolicy: + resourceFilters: + - kinds: ["ClusterRole"] + names: ["my-app-*"]` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + err = policies.BuildPolicy(resPolicies) + require.NoError(t, err) + + csfPolicy := policies.GetClusterScopedFilterPolicy() + require.NotNil(t, csfPolicy) + assert.Len(t, csfPolicy.ResourceFilters, 1) + + rf := csfPolicy.ResourceFilters[0] + assert.Equal(t, []string{"ClusterRole"}, rf.Kinds) + assert.Equal(t, []string{"my-app-*"}, rf.Names) +} + +func TestIncludeExcludePolicyAccessor(t *testing.T) { + yamlData := `version: v1 +includeExcludePolicy: + includedClusterScopedResources: + - ClusterRole + excludedClusterScopedResources: + - ClusterRoleBinding` + + resPolicies, err := unmarshalResourcePolicies(&yamlData) + require.NoError(t, err) + + policies := &Policies{} + err = policies.BuildPolicy(resPolicies) + require.NoError(t, err) + + iePolicy := policies.GetIncludeExcludePolicy() + require.NotNil(t, iePolicy) + assert.Equal(t, []string{"ClusterRole"}, iePolicy.IncludedClusterScopedResources) + assert.Equal(t, []string{"ClusterRoleBinding"}, iePolicy.ExcludedClusterScopedResources) +} + func TestFirstMatchSemantics(t *testing.T) { yamlData := `version: v1 namespacedFilterPolicies: