improve logical backup comparison unit test and improve container sync (#2686)
* improve logical backup comparison unit test and improve container sync * add new comparison function for volume mounts + unit test
This commit is contained in:
		
							parent
							
								
									37d6993439
								
							
						
					
					
						commit
						e71891e2bd
					
				|  | @ -597,7 +597,7 @@ func (c *Cluster) compareContainers(description string, setA, setB []v1.Containe | |||
| 		newCheck("new %s's %s (index %d) security context does not match the current one", | ||||
| 			func(a, b v1.Container) bool { return !reflect.DeepEqual(a.SecurityContext, b.SecurityContext) }), | ||||
| 		newCheck("new %s's %s (index %d) volume mounts do not match the current one", | ||||
| 			func(a, b v1.Container) bool { return !reflect.DeepEqual(a.VolumeMounts, b.VolumeMounts) }), | ||||
| 			func(a, b v1.Container) bool { return !compareVolumeMounts(a.VolumeMounts, b.VolumeMounts) }), | ||||
| 	} | ||||
| 
 | ||||
| 	if !c.OpConfig.EnableLazySpiloUpgrade { | ||||
|  | @ -738,6 +738,27 @@ func comparePorts(a, b []v1.ContainerPort) bool { | |||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func compareVolumeMounts(old, new []v1.VolumeMount) bool { | ||||
| 	if len(old) != len(new) { | ||||
| 		return false | ||||
| 	} | ||||
| 	for _, mount := range old { | ||||
| 		if !volumeMountExists(mount, new) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func volumeMountExists(mount v1.VolumeMount, mounts []v1.VolumeMount) bool { | ||||
| 	for _, m := range mounts { | ||||
| 		if reflect.DeepEqual(mount, m) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func (c *Cluster) compareAnnotations(old, new map[string]string) (bool, string) { | ||||
| 	reason := "" | ||||
| 	ignoredAnnotations := make(map[string]bool) | ||||
|  |  | |||
|  | @ -18,9 +18,11 @@ import ( | |||
| 	"github.com/zalando/postgres-operator/pkg/util/config" | ||||
| 	"github.com/zalando/postgres-operator/pkg/util/constants" | ||||
| 	"github.com/zalando/postgres-operator/pkg/util/k8sutil" | ||||
| 	"github.com/zalando/postgres-operator/pkg/util/patroni" | ||||
| 	"github.com/zalando/postgres-operator/pkg/util/teams" | ||||
| 	batchv1 "k8s.io/api/batch/v1" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/client-go/kubernetes/fake" | ||||
| 	"k8s.io/client-go/tools/record" | ||||
|  | @ -1464,7 +1466,7 @@ func TestCompareServices(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func newCronJob(image, schedule string, vars []v1.EnvVar) *batchv1.CronJob { | ||||
| func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMount) *batchv1.CronJob { | ||||
| 	cron := &batchv1.CronJob{ | ||||
| 		Spec: batchv1.CronJobSpec{ | ||||
| 			Schedule: schedule, | ||||
|  | @ -1477,6 +1479,37 @@ func newCronJob(image, schedule string, vars []v1.EnvVar) *batchv1.CronJob { | |||
| 									Name:  "logical-backup", | ||||
| 									Image: image, | ||||
| 									Env:   vars, | ||||
| 									Ports: []v1.ContainerPort{ | ||||
| 										{ | ||||
| 											ContainerPort: patroni.ApiPort, | ||||
| 											Protocol:      v1.ProtocolTCP, | ||||
| 										}, | ||||
| 										{ | ||||
| 											ContainerPort: pgPort, | ||||
| 											Protocol:      v1.ProtocolTCP, | ||||
| 										}, | ||||
| 										{ | ||||
| 											ContainerPort: operatorPort, | ||||
| 											Protocol:      v1.ProtocolTCP, | ||||
| 										}, | ||||
| 									}, | ||||
| 									Resources: v1.ResourceRequirements{ | ||||
| 										Requests: v1.ResourceList{ | ||||
| 											v1.ResourceCPU:    resource.MustParse("100m"), | ||||
| 											v1.ResourceMemory: resource.MustParse("100Mi"), | ||||
| 										}, | ||||
| 										Limits: v1.ResourceList{ | ||||
| 											v1.ResourceCPU:    resource.MustParse("100m"), | ||||
| 											v1.ResourceMemory: resource.MustParse("100Mi"), | ||||
| 										}, | ||||
| 									}, | ||||
| 									SecurityContext: &v1.SecurityContext{ | ||||
| 										AllowPrivilegeEscalation: nil, | ||||
| 										Privileged:               util.False(), | ||||
| 										ReadOnlyRootFilesystem:   util.False(), | ||||
| 										Capabilities:             nil, | ||||
| 									}, | ||||
| 									VolumeMounts: mounts, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
|  | @ -1493,37 +1526,110 @@ func TestCompareLogicalBackupJob(t *testing.T) { | |||
| 	img1 := "registry.opensource.zalan.do/acid/logical-backup:v1.0" | ||||
| 	img2 := "registry.opensource.zalan.do/acid/logical-backup:v2.0" | ||||
| 
 | ||||
| 	clientSet := fake.NewSimpleClientset() | ||||
| 	acidClientSet := fakeacidv1.NewSimpleClientset() | ||||
| 	namespace := "default" | ||||
| 
 | ||||
| 	client := k8sutil.KubernetesClient{ | ||||
| 		CronJobsGetter:    clientSet.BatchV1(), | ||||
| 		PostgresqlsGetter: acidClientSet.AcidV1(), | ||||
| 	} | ||||
| 	pg := acidv1.Postgresql{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name:      "acid-cron-cluster", | ||||
| 			Namespace: namespace, | ||||
| 		}, | ||||
| 		Spec: acidv1.PostgresSpec{ | ||||
| 			Volume: acidv1.Volume{ | ||||
| 				Size: "1Gi", | ||||
| 			}, | ||||
| 			EnableLogicalBackup:    true, | ||||
| 			LogicalBackupSchedule:  "0 0 * * *", | ||||
| 			LogicalBackupRetention: "3 months", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	var cluster = New( | ||||
| 		Config{ | ||||
| 			OpConfig: config.Config{ | ||||
| 				PodManagementPolicy: "ordered_ready", | ||||
| 				Resources: config.Resources{ | ||||
| 					ClusterLabels:        map[string]string{"application": "spilo"}, | ||||
| 					ClusterNameLabel:     "cluster-name", | ||||
| 					DefaultCPURequest:    "300m", | ||||
| 					DefaultCPULimit:      "300m", | ||||
| 					DefaultMemoryRequest: "300Mi", | ||||
| 					DefaultMemoryLimit:   "300Mi", | ||||
| 					PodRoleLabel:         "spilo-role", | ||||
| 				}, | ||||
| 				LogicalBackup: config.LogicalBackup{ | ||||
| 					LogicalBackupSchedule:                 "30 00 * * *", | ||||
| 					LogicalBackupDockerImage:              img1, | ||||
| 					LogicalBackupJobPrefix:                "logical-backup-", | ||||
| 					LogicalBackupCPURequest:               "100m", | ||||
| 					LogicalBackupCPULimit:                 "100m", | ||||
| 					LogicalBackupMemoryRequest:            "100Mi", | ||||
| 					LogicalBackupMemoryLimit:              "100Mi", | ||||
| 					LogicalBackupProvider:                 "s3", | ||||
| 					LogicalBackupS3Bucket:                 "testBucket", | ||||
| 					LogicalBackupS3BucketPrefix:           "spilo", | ||||
| 					LogicalBackupS3Region:                 "eu-central-1", | ||||
| 					LogicalBackupS3Endpoint:               "https://s3.amazonaws.com", | ||||
| 					LogicalBackupS3AccessKeyID:            "access", | ||||
| 					LogicalBackupS3SecretAccessKey:        "secret", | ||||
| 					LogicalBackupS3SSE:                    "aws:kms", | ||||
| 					LogicalBackupS3RetentionTime:          "3 months", | ||||
| 					LogicalBackupCronjobEnvironmentSecret: "", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, client, pg, logger, eventRecorder) | ||||
| 
 | ||||
| 	desiredCronJob, err := cluster.generateLogicalBackupJob() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Could not generate logical backup job with error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = cluster.createLogicalBackupJob() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Could not create logical backup job with error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	currentCronJob, err := cluster.KubeClient.CronJobs(namespace).Get(context.TODO(), cluster.getLogicalBackupJobName(), metav1.GetOptions{}) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Could not create logical backup job with error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		about   string | ||||
| 		current *batchv1.CronJob | ||||
| 		new     *batchv1.CronJob | ||||
| 		cronjob *batchv1.CronJob | ||||
| 		match   bool | ||||
| 		reason  string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			about:   "two equal cronjobs", | ||||
| 			current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), | ||||
| 			new:     newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), | ||||
| 			cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}, []v1.VolumeMount{}), | ||||
| 			match:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			about:   "two cronjobs with different image", | ||||
| 			current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), | ||||
| 			new:     newCronJob(img2, "0 0 * * *", []v1.EnvVar{}), | ||||
| 			cronjob: newCronJob(img2, "0 0 * * *", []v1.EnvVar{}, []v1.VolumeMount{}), | ||||
| 			match:   false, | ||||
| 			reason:  fmt.Sprintf("new job's image %q does not match the current one %q", img2, img1), | ||||
| 		}, | ||||
| 		{ | ||||
| 			about:   "two cronjobs with different schedule", | ||||
| 			current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}), | ||||
| 			new:     newCronJob(img1, "0 * * * *", []v1.EnvVar{}), | ||||
| 			cronjob: newCronJob(img1, "0 * * * *", []v1.EnvVar{}, []v1.VolumeMount{}), | ||||
| 			match:   false, | ||||
| 			reason:  fmt.Sprintf("new job's schedule %q does not match the current one %q", "0 * * * *", "0 0 * * *"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			about:   "two cronjobs with empty and nil volume mounts", | ||||
| 			cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{}, nil), | ||||
| 			match:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			about:   "two cronjobs with different environment variables", | ||||
| 			current: newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "spilo"}}), | ||||
| 			new:     newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "logical-backup"}}), | ||||
| 			cronjob: newCronJob(img1, "0 0 * * *", []v1.EnvVar{{Name: "LOGICAL_BACKUP_S3_BUCKET_PREFIX", Value: "logical-backup"}}, []v1.VolumeMount{}), | ||||
| 			match:   false, | ||||
| 			reason:  "logical backup container specs do not match: new cronjob container's logical-backup (index 0) environment does not match the current one", | ||||
| 		}, | ||||
|  | @ -1531,9 +1637,21 @@ func TestCompareLogicalBackupJob(t *testing.T) { | |||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.about, func(t *testing.T) { | ||||
| 			match, reason := cl.compareLogicalBackupJob(tt.current, tt.new) | ||||
| 			desiredCronJob.Spec.Schedule = tt.cronjob.Spec.Schedule | ||||
| 			desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image = tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Image | ||||
| 			desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts | ||||
| 
 | ||||
| 			for _, testEnv := range tt.cronjob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env { | ||||
| 				for i, env := range desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env { | ||||
| 					if env.Name == testEnv.Name { | ||||
| 						desiredCronJob.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Env[i] = testEnv | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			match, reason := cluster.compareLogicalBackupJob(currentCronJob, desiredCronJob) | ||||
| 			if match != tt.match { | ||||
| 				t.Errorf("%s - unexpected match result %t when comparing cronjobs %q and %q", t.Name(), match, tt.current, tt.new) | ||||
| 				t.Errorf("%s - unexpected match result %t when comparing cronjobs %#v and %#v", t.Name(), match, currentCronJob, desiredCronJob) | ||||
| 			} else { | ||||
| 				if !strings.HasPrefix(reason, tt.reason) { | ||||
| 					t.Errorf("%s - expected reason prefix %s, found %s", t.Name(), tt.reason, reason) | ||||
|  | @ -1728,3 +1846,183 @@ func TestComparePorts(t *testing.T) { | |||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCompareVolumeMounts(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name     string | ||||
| 		mountsA  []v1.VolumeMount | ||||
| 		mountsB  []v1.VolumeMount | ||||
| 		expected bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "empty vs nil", | ||||
| 			mountsA:  []v1.VolumeMount{}, | ||||
| 			mountsB:  nil, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "both empty", | ||||
| 			mountsA:  []v1.VolumeMount{}, | ||||
| 			mountsB:  []v1.VolumeMount{}, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "same mounts", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "different mounts", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:        "data", | ||||
| 					ReadOnly:    false, | ||||
| 					MountPath:   "/data", | ||||
| 					SubPathExpr: "$(POD_NAME)", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "one equal mount one different", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:        "poddata", | ||||
| 					ReadOnly:    false, | ||||
| 					MountPath:   "/poddata", | ||||
| 					SubPathExpr: "$(POD_NAME)", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:      "etc", | ||||
| 					ReadOnly:  true, | ||||
| 					MountPath: "/etc", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "same mounts, different order", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:      "etc", | ||||
| 					ReadOnly:  true, | ||||
| 					MountPath: "/etc", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "etc", | ||||
| 					ReadOnly:  true, | ||||
| 					MountPath: "/etc", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "new mounts added", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "etc", | ||||
| 					ReadOnly:  true, | ||||
| 					MountPath: "/etc", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "one mount removed", | ||||
| 			mountsA: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name:      "etc", | ||||
| 					ReadOnly:  true, | ||||
| 					MountPath: "/etc", | ||||
| 				}, | ||||
| 			}, | ||||
| 			mountsB: []v1.VolumeMount{ | ||||
| 				{ | ||||
| 					Name:      "data", | ||||
| 					ReadOnly:  false, | ||||
| 					MountPath: "/data", | ||||
| 					SubPath:   "subdir", | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: false, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range testCases { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got := compareVolumeMounts(tt.mountsA, tt.mountsB) | ||||
| 			assert.Equal(t, tt.expected, got) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -892,7 +892,7 @@ func (c *Cluster) generatePodTemplate( | |||
| 		addSecretVolume(&podSpec, additionalSecretMount, additionalSecretMountPath) | ||||
| 	} | ||||
| 
 | ||||
| 	if additionalVolumes != nil { | ||||
| 	if len(additionalVolumes) > 0 { | ||||
| 		c.addAdditionalVolumes(&podSpec, additionalVolumes) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue