From a8ede941176332e5c1305110d4907bb6501d5790 Mon Sep 17 00:00:00 2001
From: BornChanger BackupTemplate is the specification of the backup structure to get scheduled. LogBackupTemplate is the specification of the log backup structure to get scheduled. BackupTemplate is the specification of the backup structure to get scheduled. LogBackupTemplate is the specification of the log backup structure to get scheduled. Schedule specifies the cron string used for backup scheduling. Pause means paused backupSchedule MaxBackups is to specify how many backups we want to keep
+0 is magic number to indicate un-limited backups.
+if MaxBackups and MaxReservedTime are set at the same time, MaxReservedTime is preferred
+and MaxBackups is ignored. MaxReservedTime is to specify how long backups we want to keep. BackupTemplate is the specification of the volume backup structure to get scheduled.
VolumeBackupScheduleSpec describes the attributes that a user creates on a volume backup schedule.
-(Optional)
@@ -592,6 +591,7 @@ BackupSpec
+(Optional)
@@ -3694,7 +3694,6 @@ BackupSpec
-(Optional)
@@ -3708,6 +3707,7 @@ BackupSpec
+(Optional)
diff --git a/docs/api-references/federation-docs.md b/docs/api-references/federation-docs.md
index 75ca98728ec..2f8c510677d 100644
--- a/docs/api-references/federation-docs.md
+++ b/docs/api-references/federation-docs.md
@@ -166,6 +166,66 @@ VolumeBackupScheduleSpec
+
@@ -713,6 +773,76 @@ string
+
+
+
+schedule
+
+string
+
+
+
+
+
+
+
+pause
+
+bool
+
+
+
+
+
+
+
+maxBackups
+
+int32
+
+
+
+
+
+
+
+maxReservedTime
+
+string
+
+
+
+
+
+
+backupTemplate
+
+
+VolumeBackupSpec
+
+
+
+
+
Field | +Description | +
---|---|
+schedule
+
+string
+
+ |
+
+ Schedule specifies the cron string used for backup scheduling. + |
+
+pause
+
+bool
+
+ |
+
+ Pause means paused backupSchedule + |
+
+maxBackups
+
+int32
+
+ |
+
+ MaxBackups is to specify how many backups we want to keep +0 is magic number to indicate un-limited backups. +if MaxBackups and MaxReservedTime are set at the same time, MaxReservedTime is preferred +and MaxBackups is ignored. + |
+
+maxReservedTime
+
+string
+
+ |
+
+ MaxReservedTime is to specify how long backups we want to keep. + |
+
+backupTemplate
+
+
+VolumeBackupSpec
+
+
+ |
+
+ BackupTemplate is the specification of the volume backup structure to get scheduled. + |
+
(Appears on: @@ -721,10 +851,58 @@ string
VolumeBackupScheduleStatus represents the current status of a volume backup schedule.
+Field | +Description | +
---|---|
+lastBackup
+
+string
+
+ |
+
+ LastBackup represents the last backup. + |
+
+lastBackupTime
+
+
+Kubernetes meta/v1.Time
+
+
+ |
+
+ LastBackupTime represents the last time the backup was successfully created. + |
+
+allBackupCleanTime
+
+
+Kubernetes meta/v1.Time
+
+
+ |
+
+ AllBackupCleanTime represents the time when all backup entries are cleaned up + |
+
(Appears on: -VolumeBackup) +VolumeBackup, +VolumeBackupScheduleSpec)
VolumeBackupSpec describes the attributes that a user creates on a volume backup.
diff --git a/manifests/crd.yaml b/manifests/crd.yaml index b82bbf49710..af635095bf0 100644 --- a/manifests/crd.yaml +++ b/manifests/crd.yaml @@ -1554,6 +1554,10 @@ spec: jsonPath: .spec.maxBackups name: MaxBackups type: integer + - description: How long backups we want to keep + jsonPath: .spec.maxReservedTime + name: MaxReservedTime + type: string - description: The last backup CR name jsonPath: .status.lastBackup name: LastBackup @@ -4232,7 +4236,7 @@ spec: storageSize: type: string required: - - logBackupTemplate + - backupTemplate - schedule type: object status: diff --git a/manifests/crd/federation/v1/federation.pingcap.com_volumebackupschedules.yaml b/manifests/crd/federation/v1/federation.pingcap.com_volumebackupschedules.yaml index 748bab61d90..386ccf98e5b 100644 --- a/manifests/crd/federation/v1/federation.pingcap.com_volumebackupschedules.yaml +++ b/manifests/crd/federation/v1/federation.pingcap.com_volumebackupschedules.yaml @@ -14,11 +14,37 @@ spec: listKind: VolumeBackupScheduleList plural: volumebackupschedules shortNames: - - vbks + - vbfs singular: volumebackupschedule scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: The cron format string used for backup scheduling + jsonPath: .spec.schedule + name: Schedule + type: string + - description: The max number of backups we want to keep + jsonPath: .spec.maxBackups + name: MaxBackups + type: integer + - description: How long backups we want to keep + jsonPath: .spec.maxReservedTime + name: MaxReservedTime + type: string + - description: The last backup CR name + jsonPath: .status.lastBackup + name: LastBackup + priority: 1 + type: string + - description: The last time the backup was successfully created + jsonPath: .status.lastBackupTime + name: LastBackupTime + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: properties: @@ -29,8 +55,930 @@ spec: metadata: type: object spec: + properties: + backupTemplate: + properties: + clusters: + items: + properties: + k8sClusterName: + type: string + tcName: + type: string + tcNamespace: + type: string + type: object + type: array + template: + properties: + azblob: + properties: + accessTier: + type: string + container: + type: string + path: + type: string + prefix: + type: string + secretName: + type: string + type: object + br: + properties: + checkRequirements: + type: boolean + concurrency: + format: int32 + type: integer + options: + items: + type: string + type: array + sendCredToTikv: + type: boolean + type: object + cleanPolicy: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + gcs: + properties: + bucket: + type: string + bucketAcl: + type: string + location: + type: string + objectAcl: + type: string + path: + type: string + prefix: + type: string + projectId: + type: string + secretName: + type: string + storageClass: + type: string + required: + - projectId + type: object + imagePullSecrets: + items: + properties: + name: + type: string + type: object + type: array + local: + properties: + prefix: + type: string + volume: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + readOnly: + type: boolean + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + volumeMount: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + required: + - volume + - volumeMount + type: object + priorityClassName: + type: string + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + s3: + properties: + acl: + type: string + bucket: + type: string + endpoint: + type: string + options: + items: + type: string + type: array + path: + type: string + prefix: + type: string + provider: + type: string + region: + type: string + secretName: + type: string + sse: + type: string + storageClass: + type: string + required: + - provider + type: object + serviceAccount: + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + toolImage: + type: string + type: object + type: object + maxBackups: + format: int32 + type: integer + maxReservedTime: + type: string + pause: + type: boolean + schedule: + type: string + required: + - backupTemplate + - schedule type: object status: + properties: + allBackupCleanTime: + format: date-time + type: string + lastBackup: + type: string + lastBackupTime: + format: date-time + type: string type: object required: - metadata @@ -38,6 +986,7 @@ spec: type: object served: true storage: true + subresources: {} status: acceptedNames: kind: "" diff --git a/manifests/crd/federation/v1beta1/federation.pingcap.com_volumebackupschedules.yaml b/manifests/crd/federation/v1beta1/federation.pingcap.com_volumebackupschedules.yaml index 29d26b06e90..1809519859a 100644 --- a/manifests/crd/federation/v1beta1/federation.pingcap.com_volumebackupschedules.yaml +++ b/manifests/crd/federation/v1beta1/federation.pingcap.com_volumebackupschedules.yaml @@ -8,16 +8,43 @@ metadata: creationTimestamp: null name: volumebackupschedules.federation.pingcap.com spec: + additionalPrinterColumns: + - JSONPath: .spec.schedule + description: The cron format string used for backup scheduling + name: Schedule + type: string + - JSONPath: .spec.maxBackups + description: The max number of backups we want to keep + name: MaxBackups + type: integer + - JSONPath: .spec.maxReservedTime + description: How long backups we want to keep + name: MaxReservedTime + type: string + - JSONPath: .status.lastBackup + description: The last backup CR name + name: LastBackup + priority: 1 + type: string + - JSONPath: .status.lastBackupTime + description: The last time the backup was successfully created + name: LastBackupTime + priority: 1 + type: date + - JSONPath: .metadata.creationTimestamp + name: Age + type: date group: federation.pingcap.com names: kind: VolumeBackupSchedule listKind: VolumeBackupScheduleList plural: volumebackupschedules shortNames: - - vbks + - vbfs singular: volumebackupschedule preserveUnknownFields: false scope: Namespaced + subresources: {} validation: openAPIV3Schema: properties: @@ -28,8 +55,930 @@ spec: metadata: type: object spec: + properties: + backupTemplate: + properties: + clusters: + items: + properties: + k8sClusterName: + type: string + tcName: + type: string + tcNamespace: + type: string + type: object + type: array + template: + properties: + azblob: + properties: + accessTier: + type: string + container: + type: string + path: + type: string + prefix: + type: string + secretName: + type: string + type: object + br: + properties: + checkRequirements: + type: boolean + concurrency: + format: int32 + type: integer + options: + items: + type: string + type: array + sendCredToTikv: + type: boolean + type: object + cleanPolicy: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + gcs: + properties: + bucket: + type: string + bucketAcl: + type: string + location: + type: string + objectAcl: + type: string + path: + type: string + prefix: + type: string + projectId: + type: string + secretName: + type: string + storageClass: + type: string + required: + - projectId + type: object + imagePullSecrets: + items: + properties: + name: + type: string + type: object + type: array + local: + properties: + prefix: + type: string + volume: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + readOnly: + type: boolean + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + volumeMount: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + required: + - volume + - volumeMount + type: object + priorityClassName: + type: string + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + s3: + properties: + acl: + type: string + bucket: + type: string + endpoint: + type: string + options: + items: + type: string + type: array + path: + type: string + prefix: + type: string + provider: + type: string + region: + type: string + secretName: + type: string + sse: + type: string + storageClass: + type: string + required: + - provider + type: object + serviceAccount: + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + toolImage: + type: string + type: object + type: object + maxBackups: + format: int32 + type: integer + maxReservedTime: + type: string + pause: + type: boolean + schedule: + type: string + required: + - backupTemplate + - schedule type: object status: + properties: + allBackupCleanTime: + format: date-time + type: string + lastBackup: + type: string + lastBackupTime: + format: date-time + type: string type: object required: - metadata diff --git a/manifests/crd/v1/pingcap.com_backupschedules.yaml b/manifests/crd/v1/pingcap.com_backupschedules.yaml index 43175cd21e8..c6479d6916f 100644 --- a/manifests/crd/v1/pingcap.com_backupschedules.yaml +++ b/manifests/crd/v1/pingcap.com_backupschedules.yaml @@ -27,6 +27,10 @@ spec: jsonPath: .spec.maxBackups name: MaxBackups type: integer + - description: How long backups we want to keep + jsonPath: .spec.maxReservedTime + name: MaxReservedTime + type: string - description: The last backup CR name jsonPath: .status.lastBackup name: LastBackup @@ -2705,7 +2709,7 @@ spec: storageSize: type: string required: - - logBackupTemplate + - backupTemplate - schedule type: object status: diff --git a/manifests/crd/v1beta1/pingcap.com_backupschedules.yaml b/manifests/crd/v1beta1/pingcap.com_backupschedules.yaml index e865a48de32..c8ce37aef50 100644 --- a/manifests/crd/v1beta1/pingcap.com_backupschedules.yaml +++ b/manifests/crd/v1beta1/pingcap.com_backupschedules.yaml @@ -17,6 +17,10 @@ spec: description: The max number of backups we want to keep name: MaxBackups type: integer + - JSONPath: .spec.maxReservedTime + description: How long backups we want to keep + name: MaxReservedTime + type: string - JSONPath: .status.lastBackup description: The last backup CR name name: LastBackup @@ -2695,7 +2699,7 @@ spec: storageSize: type: string required: - - logBackupTemplate + - backupTemplate - schedule type: object status: diff --git a/manifests/crd_v1beta1.yaml b/manifests/crd_v1beta1.yaml index 9b909aca102..53742a0c660 100644 --- a/manifests/crd_v1beta1.yaml +++ b/manifests/crd_v1beta1.yaml @@ -1541,6 +1541,10 @@ spec: description: The max number of backups we want to keep name: MaxBackups type: integer + - JSONPath: .spec.maxReservedTime + description: How long backups we want to keep + name: MaxReservedTime + type: string - JSONPath: .status.lastBackup description: The last backup CR name name: LastBackup @@ -4219,7 +4223,7 @@ spec: storageSize: type: string required: - - logBackupTemplate + - backupTemplate - schedule type: object status: diff --git a/manifests/federation-crd.yaml b/manifests/federation-crd.yaml index ac30a565b30..7a3690d86f0 100644 --- a/manifests/federation-crd.yaml +++ b/manifests/federation-crd.yaml @@ -1042,11 +1042,37 @@ spec: listKind: VolumeBackupScheduleList plural: volumebackupschedules shortNames: - - vbks + - vbfs singular: volumebackupschedule scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: The cron format string used for backup scheduling + jsonPath: .spec.schedule + name: Schedule + type: string + - description: The max number of backups we want to keep + jsonPath: .spec.maxBackups + name: MaxBackups + type: integer + - description: How long backups we want to keep + jsonPath: .spec.maxReservedTime + name: MaxReservedTime + type: string + - description: The last backup CR name + jsonPath: .status.lastBackup + name: LastBackup + priority: 1 + type: string + - description: The last time the backup was successfully created + jsonPath: .status.lastBackupTime + name: LastBackupTime + priority: 1 + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: properties: @@ -1057,8 +1083,930 @@ spec: metadata: type: object spec: + properties: + backupTemplate: + properties: + clusters: + items: + properties: + k8sClusterName: + type: string + tcName: + type: string + tcNamespace: + type: string + type: object + type: array + template: + properties: + azblob: + properties: + accessTier: + type: string + container: + type: string + path: + type: string + prefix: + type: string + secretName: + type: string + type: object + br: + properties: + checkRequirements: + type: boolean + concurrency: + format: int32 + type: integer + options: + items: + type: string + type: array + sendCredToTikv: + type: boolean + type: object + cleanPolicy: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + gcs: + properties: + bucket: + type: string + bucketAcl: + type: string + location: + type: string + objectAcl: + type: string + path: + type: string + prefix: + type: string + projectId: + type: string + secretName: + type: string + storageClass: + type: string + required: + - projectId + type: object + imagePullSecrets: + items: + properties: + name: + type: string + type: object + type: array + local: + properties: + prefix: + type: string + volume: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + readOnly: + type: boolean + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + volumeMount: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + required: + - volume + - volumeMount + type: object + priorityClassName: + type: string + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + s3: + properties: + acl: + type: string + bucket: + type: string + endpoint: + type: string + options: + items: + type: string + type: array + path: + type: string + prefix: + type: string + provider: + type: string + region: + type: string + secretName: + type: string + sse: + type: string + storageClass: + type: string + required: + - provider + type: object + serviceAccount: + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + toolImage: + type: string + type: object + type: object + maxBackups: + format: int32 + type: integer + maxReservedTime: + type: string + pause: + type: boolean + schedule: + type: string + required: + - backupTemplate + - schedule type: object status: + properties: + allBackupCleanTime: + format: date-time + type: string + lastBackup: + type: string + lastBackupTime: + format: date-time + type: string type: object required: - metadata @@ -1066,6 +2014,7 @@ spec: type: object served: true storage: true + subresources: {} status: acceptedNames: kind: "" diff --git a/manifests/federation-crd_v1beta1.yaml b/manifests/federation-crd_v1beta1.yaml index 2f0438a4aa8..00dff0db474 100644 --- a/manifests/federation-crd_v1beta1.yaml +++ b/manifests/federation-crd_v1beta1.yaml @@ -1038,16 +1038,43 @@ metadata: creationTimestamp: null name: volumebackupschedules.federation.pingcap.com spec: + additionalPrinterColumns: + - JSONPath: .spec.schedule + description: The cron format string used for backup scheduling + name: Schedule + type: string + - JSONPath: .spec.maxBackups + description: The max number of backups we want to keep + name: MaxBackups + type: integer + - JSONPath: .spec.maxReservedTime + description: How long backups we want to keep + name: MaxReservedTime + type: string + - JSONPath: .status.lastBackup + description: The last backup CR name + name: LastBackup + priority: 1 + type: string + - JSONPath: .status.lastBackupTime + description: The last time the backup was successfully created + name: LastBackupTime + priority: 1 + type: date + - JSONPath: .metadata.creationTimestamp + name: Age + type: date group: federation.pingcap.com names: kind: VolumeBackupSchedule listKind: VolumeBackupScheduleList plural: volumebackupschedules shortNames: - - vbks + - vbfs singular: volumebackupschedule preserveUnknownFields: false scope: Namespaced + subresources: {} validation: openAPIV3Schema: properties: @@ -1058,8 +1085,930 @@ spec: metadata: type: object spec: + properties: + backupTemplate: + properties: + clusters: + items: + properties: + k8sClusterName: + type: string + tcName: + type: string + tcNamespace: + type: string + type: object + type: array + template: + properties: + azblob: + properties: + accessTier: + type: string + container: + type: string + path: + type: string + prefix: + type: string + secretName: + type: string + type: object + br: + properties: + checkRequirements: + type: boolean + concurrency: + format: int32 + type: integer + options: + items: + type: string + type: array + sendCredToTikv: + type: boolean + type: object + cleanPolicy: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + gcs: + properties: + bucket: + type: string + bucketAcl: + type: string + location: + type: string + objectAcl: + type: string + path: + type: string + prefix: + type: string + projectId: + type: string + secretName: + type: string + storageClass: + type: string + required: + - projectId + type: object + imagePullSecrets: + items: + properties: + name: + type: string + type: object + type: array + local: + properties: + prefix: + type: string + volume: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + readOnly: + type: boolean + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + volumeMount: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + required: + - volume + - volumeMount + type: object + priorityClassName: + type: string + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + s3: + properties: + acl: + type: string + bucket: + type: string + endpoint: + type: string + options: + items: + type: string + type: array + path: + type: string + prefix: + type: string + provider: + type: string + region: + type: string + secretName: + type: string + sse: + type: string + storageClass: + type: string + required: + - provider + type: object + serviceAccount: + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + toolImage: + type: string + type: object + type: object + maxBackups: + format: int32 + type: integer + maxReservedTime: + type: string + pause: + type: boolean + schedule: + type: string + required: + - backupTemplate + - schedule type: object status: + properties: + allBackupCleanTime: + format: date-time + type: string + lastBackup: + type: string + lastBackupTime: + format: date-time + type: string type: object required: - metadata diff --git a/pkg/apis/federation/pingcap/v1alpha1/openapi_generated.go b/pkg/apis/federation/pingcap/v1alpha1/openapi_generated.go index 16fd40b1ca5..51adaa8a29c 100644 --- a/pkg/apis/federation/pingcap/v1alpha1/openapi_generated.go +++ b/pkg/apis/federation/pingcap/v1alpha1/openapi_generated.go @@ -417,8 +417,49 @@ func schema_apis_federation_pingcap_v1alpha1_VolumeBackupScheduleSpec(ref common SchemaProps: spec.SchemaProps{ Description: "VolumeBackupScheduleSpec describes the attributes that a user creates on a volume backup schedule.", Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "schedule": { + SchemaProps: spec.SchemaProps{ + Description: "Schedule specifies the cron string used for backup scheduling.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "pause": { + SchemaProps: spec.SchemaProps{ + Description: "Pause means paused backupSchedule", + Type: []string{"boolean"}, + Format: "", + }, + }, + "maxBackups": { + SchemaProps: spec.SchemaProps{ + Description: "MaxBackups is to specify how many backups we want to keep 0 is magic number to indicate un-limited backups. if MaxBackups and MaxReservedTime are set at the same time, MaxReservedTime is preferred and MaxBackups is ignored.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "maxReservedTime": { + SchemaProps: spec.SchemaProps{ + Description: "MaxReservedTime is to specify how long backups we want to keep.", + Type: []string{"string"}, + Format: "", + }, + }, + "backupTemplate": { + SchemaProps: spec.SchemaProps{ + Description: "BackupTemplate is the specification of the volume backup structure to get scheduled.", + Default: map[string]interface{}{}, + Ref: ref("github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1.VolumeBackupSpec"), + }, + }, + }, + Required: []string{"schedule", "backupTemplate"}, }, }, + Dependencies: []string{ + "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1.VolumeBackupSpec"}, } } diff --git a/pkg/apis/federation/pingcap/v1alpha1/types.go b/pkg/apis/federation/pingcap/v1alpha1/types.go index a2c5b4c4e28..3fa2cade293 100644 --- a/pkg/apis/federation/pingcap/v1alpha1/types.go +++ b/pkg/apis/federation/pingcap/v1alpha1/types.go @@ -186,6 +186,8 @@ type VolumeBackupConditionType string const ( // VolumeBackupInvalid means the VolumeBackup is invalid VolumeBackupInvalid VolumeBackupConditionType = "Invalid" + // VolumeBackupPrepared means the VolumeBackup preparation is done + VolumeBackupPrepared VolumeBackupConditionType = "Prepared" // VolumeBackupRunning means the VolumeBackup is running VolumeBackupRunning VolumeBackupConditionType = "Running" // VolumeBackupComplete means all the backups in data plane are complete and the VolumeBackup is complete @@ -196,8 +198,6 @@ const ( VolumeBackupCleaned VolumeBackupConditionType = "Cleaned" // VolumeBackupCleanFailed means the VolumeBackup cleanup is failed VolumeBackupCleanFailed VolumeBackupConditionType = "CleanFailed" - // VolumeBackupPrepared means the VolumeBackup is prepared - VolumeBackupPrepared VolumeBackupConditionType = "Prepared" ) // +genclient @@ -206,8 +206,14 @@ const ( // VolumeBackupSchedule is the control script's spec // // +k8s:openapi-gen=true -// +kubebuilder:resource:shortName="vbks" +// +kubebuilder:resource:shortName="vbfs" // +genclient:noStatus +// +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=`.spec.schedule`,description="The cron format string used for backup scheduling" +// +kubebuilder:printcolumn:name="MaxBackups",type=integer,JSONPath=`.spec.maxBackups`,description="The max number of backups we want to keep" +// +kubebuilder:printcolumn:name="MaxReservedTime",type=string,JSONPath=`.spec.maxReservedTime`,description="How long backups we want to keep" +// +kubebuilder:printcolumn:name="LastBackup",type=string,JSONPath=`.status.lastBackup`,description="The last backup CR name",priority=1 +// +kubebuilder:printcolumn:name="LastBackupTime",type=date,JSONPath=`.status.lastBackupTime`,description="The last time the backup was successfully created",priority=1 +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` type VolumeBackupSchedule struct { metav1.TypeMeta `json:",inline"` // +k8s:openapi-gen=false @@ -232,10 +238,29 @@ type VolumeBackupScheduleList struct { // VolumeBackupScheduleSpec describes the attributes that a user creates on a volume backup schedule. // +k8s:openapi-gen=true type VolumeBackupScheduleSpec struct { + // Schedule specifies the cron string used for backup scheduling. + Schedule string `json:"schedule"` + // Pause means paused backupSchedule + Pause bool `json:"pause,omitempty"` + // MaxBackups is to specify how many backups we want to keep + // 0 is magic number to indicate un-limited backups. + // if MaxBackups and MaxReservedTime are set at the same time, MaxReservedTime is preferred + // and MaxBackups is ignored. + MaxBackups *int32 `json:"maxBackups,omitempty"` + // MaxReservedTime is to specify how long backups we want to keep. + MaxReservedTime *string `json:"maxReservedTime,omitempty"` + // BackupTemplate is the specification of the volume backup structure to get scheduled. + BackupTemplate VolumeBackupSpec `json:"backupTemplate"` } // VolumeBackupScheduleStatus represents the current status of a volume backup schedule. type VolumeBackupScheduleStatus struct { + // LastBackup represents the last backup. + LastBackup string `json:"lastBackup,omitempty"` + // LastBackupTime represents the last time the backup was successfully created. + LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"` + // AllBackupCleanTime represents the time when all backup entries are cleaned up + AllBackupCleanTime *metav1.Time `json:"allBackupCleanTime,omitempty"` } // +genclient diff --git a/pkg/apis/federation/pingcap/v1alpha1/volume_backup.go b/pkg/apis/federation/pingcap/v1alpha1/volume_backup.go index a8115ddf7a9..dedfe72b40e 100644 --- a/pkg/apis/federation/pingcap/v1alpha1/volume_backup.go +++ b/pkg/apis/federation/pingcap/v1alpha1/volume_backup.go @@ -88,6 +88,12 @@ func IsVolumeBackupRunning(volumeBackup *VolumeBackup) bool { return condition != nil && condition.Status == corev1.ConditionTrue } +// IsBackupPrepared returns true if VolumeBackup is running +func IsBackupPrepared(volumeBackup *VolumeBackup) bool { + _, condition := GetVolumeBackupCondition(&volumeBackup.Status, VolumeBackupPrepared) + return condition != nil && condition.Status == corev1.ConditionTrue +} + // IsVolumeBackupComplete returns true if VolumeBackup is complete func IsVolumeBackupComplete(volumeBackup *VolumeBackup) bool { _, condition := GetVolumeBackupCondition(&volumeBackup.Status, VolumeBackupComplete) diff --git a/pkg/apis/federation/pingcap/v1alpha1/volume_backup_schedule.go b/pkg/apis/federation/pingcap/v1alpha1/volume_backup_schedule.go new file mode 100644 index 00000000000..a76db525f94 --- /dev/null +++ b/pkg/apis/federation/pingcap/v1alpha1/volume_backup_schedule.go @@ -0,0 +1,25 @@ +// Copyright 2023 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + "fmt" + "time" + + constants "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1" +) + +func (vbfs *VolumeBackupSchedule) GetBackupCRDName(timestamp time.Time) string { + return fmt.Sprintf("%s-%s", vbfs.GetName(), timestamp.UTC().Format(constants.BackupNameTimeFormat)) +} diff --git a/pkg/apis/federation/pingcap/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/federation/pingcap/v1alpha1/zz_generated.deepcopy.go index 44f24e3898f..9d6e4a1afad 100644 --- a/pkg/apis/federation/pingcap/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/federation/pingcap/v1alpha1/zz_generated.deepcopy.go @@ -216,8 +216,8 @@ func (in *VolumeBackupSchedule) DeepCopyInto(out *VolumeBackupSchedule) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } @@ -275,6 +275,17 @@ func (in *VolumeBackupScheduleList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeBackupScheduleSpec) DeepCopyInto(out *VolumeBackupScheduleSpec) { *out = *in + if in.MaxBackups != nil { + in, out := &in.MaxBackups, &out.MaxBackups + *out = new(int32) + **out = **in + } + if in.MaxReservedTime != nil { + in, out := &in.MaxReservedTime, &out.MaxReservedTime + *out = new(string) + **out = **in + } + in.BackupTemplate.DeepCopyInto(&out.BackupTemplate) return } @@ -291,6 +302,14 @@ func (in *VolumeBackupScheduleSpec) DeepCopy() *VolumeBackupScheduleSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeBackupScheduleStatus) DeepCopyInto(out *VolumeBackupScheduleStatus) { *out = *in + if in.LastBackupTime != nil { + in, out := &in.LastBackupTime, &out.LastBackupTime + *out = (*in).DeepCopy() + } + if in.AllBackupCleanTime != nil { + in, out := &in.AllBackupCleanTime, &out.AllBackupCleanTime + *out = (*in).DeepCopy() + } return } diff --git a/pkg/apis/pingcap/v1alpha1/openapi_generated.go b/pkg/apis/pingcap/v1alpha1/openapi_generated.go index 0b2d2748322..99e813abd07 100644 --- a/pkg/apis/pingcap/v1alpha1/openapi_generated.go +++ b/pkg/apis/pingcap/v1alpha1/openapi_generated.go @@ -945,7 +945,7 @@ func schema_pkg_apis_pingcap_v1alpha1_BackupScheduleSpec(ref common.ReferenceCal }, }, }, - Required: []string{"schedule", "logBackupTemplate"}, + Required: []string{"schedule", "backupTemplate"}, }, }, Dependencies: []string{ diff --git a/pkg/apis/pingcap/v1alpha1/types.go b/pkg/apis/pingcap/v1alpha1/types.go index 4e28b2d1230..788214f7d85 100644 --- a/pkg/apis/pingcap/v1alpha1/types.go +++ b/pkg/apis/pingcap/v1alpha1/types.go @@ -2197,6 +2197,7 @@ type BackupStatus struct { // +kubebuilder:resource:shortName="bks" // +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=`.spec.schedule`,description="The cron format string used for backup scheduling" // +kubebuilder:printcolumn:name="MaxBackups",type=integer,JSONPath=`.spec.maxBackups`,description="The max number of backups we want to keep" +// +kubebuilder:printcolumn:name="MaxReservedTime",type=string,JSONPath=`.spec.maxReservedTime`,description="How long backups we want to keep" // +kubebuilder:printcolumn:name="LastBackup",type=string,JSONPath=`.status.lastBackup`,description="The last backup CR name",priority=1 // +kubebuilder:printcolumn:name="LastBackupTime",type=date,JSONPath=`.status.lastBackupTime`,description="The last time the backup was successfully created",priority=1 // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` @@ -2236,9 +2237,9 @@ type BackupScheduleSpec struct { // MaxReservedTime is to specify how long backups we want to keep. MaxReservedTime *string `json:"maxReservedTime,omitempty"` // BackupTemplate is the specification of the backup structure to get scheduled. - // +optional BackupTemplate BackupSpec `json:"backupTemplate"` // LogBackupTemplate is the specification of the log backup structure to get scheduled. + // +optional LogBackupTemplate *BackupSpec `json:"logBackupTemplate"` // The storageClassName of the persistent volume for Backup data storage if not storage class name set in BackupSpec. // Defaults to Kubernetes default storage class. diff --git a/pkg/backup/backupschedule/backup_schedule_manager.go b/pkg/backup/backupschedule/backup_schedule_manager.go index 33279f8f754..843467f582d 100644 --- a/pkg/backup/backupschedule/backup_schedule_manager.go +++ b/pkg/backup/backupschedule/backup_schedule_manager.go @@ -70,6 +70,7 @@ func (bm *backupScheduleManager) Sync(bs *v1alpha1.BackupSchedule) error { } // delete the last backup job for release the backup PVC + if err := bm.deleteLastBackupJob(bs); err != nil { return nil } @@ -373,7 +374,7 @@ func (bm *backupScheduleManager) backupGCByMaxReservedTime(bs *v1alpha1.BackupSc return } } else { - expiredBackups, err = caculateExpiredBackups(ascBackups, reservedTime) + expiredBackups, err = calculateExpiredBackups(ascBackups, reservedTime) if err != nil { klog.Errorf("caculate expired backups without log backup, err: %s", err) return @@ -404,7 +405,7 @@ func (bm *backupScheduleManager) backupGCByMaxReservedTime(bs *v1alpha1.BackupSc } } -// separateSnapshotBackupsAndLogBackup return snapot backups ordry by create time asc and log backup +// separateSnapshotBackupsAndLogBackup return snapshot backups order by create time asc and log backup func separateSnapshotBackupsAndLogBackup(backupsList []*v1alpha1.Backup) ([]*v1alpha1.Backup, *v1alpha1.Backup) { var ( ascBackupList = make([]*v1alpha1.Backup, 0) @@ -479,7 +480,7 @@ func calExpiredBackupsAndLogBackup(backupsList []*v1alpha1.Backup, logBackup *v1 return expiredBackups, truncateTSO, nil } -func caculateExpiredBackups(backupsList []*v1alpha1.Backup, reservedTime time.Duration) ([]*v1alpha1.Backup, error) { +func calculateExpiredBackups(backupsList []*v1alpha1.Backup, reservedTime time.Duration) ([]*v1alpha1.Backup, error) { expiredTS := config.TSToTSO(time.Now().Add(-1 * reservedTime).Unix()) i := 0 for ; i < len(backupsList); i++ { diff --git a/pkg/controller/controller_utils.go b/pkg/controller/controller_utils.go index b2c141dd359..92e8d04ae18 100644 --- a/pkg/controller/controller_utils.go +++ b/pkg/controller/controller_utils.go @@ -186,6 +186,20 @@ func GetBackupScheduleOwnerRef(bs *v1alpha1.BackupSchedule) metav1.OwnerReferenc } } +// GetFedVolumeBackupScheduleOwnerRef returns FedVolumeBackupSchedule's OwnerReference +func GetFedVolumeBackupScheduleOwnerRef(vbs *fedv1alpha1.VolumeBackupSchedule) metav1.OwnerReference { + controller := true + blockOwnerDeletion := true + return metav1.OwnerReference{ + APIVersion: backupScheduleControllerKind.GroupVersion().String(), + Kind: backupScheduleControllerKind.Kind, + Name: vbs.GetName(), + UID: vbs.GetUID(), + Controller: &controller, + BlockOwnerDeletion: &blockOwnerDeletion, + } +} + func GetTiDBMonitorOwnerRef(monitor *v1alpha1.TidbMonitor) metav1.OwnerReference { controller := true blockOwnerDeletion := true diff --git a/pkg/controller/fed_backup_schedule_status_updater.go b/pkg/controller/fed_backup_schedule_status_updater.go new file mode 100644 index 00000000000..88613eb5b12 --- /dev/null +++ b/pkg/controller/fed_backup_schedule_status_updater.go @@ -0,0 +1,113 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "fmt" + + "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1" + informers "github.com/pingcap/tidb-operator/pkg/client/federation/informers/externalversions/pingcap/v1alpha1" + listers "github.com/pingcap/tidb-operator/pkg/client/federation/listers/pingcap/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" +) + +// VolumeBackupScheduleStatusUpdaterInterface is an interface used to update the VolumeBackupScheduleStatus associated with a VolumeBackupSchedule. +// For any use other than testing, clients should create an instance using NewRealBackupScheduleStatusUpdater. +type VolumeBackupScheduleStatusUpdaterInterface interface { + // UpdateBackupScheduleStatus sets the backupSchedule's Status to status. Implementations are required to retry on conflicts, + // but fail on other errors. If the returned error is nil backup's Status has been successfully set to status. + UpdateBackupScheduleStatus(*v1alpha1.VolumeBackupSchedule, *v1alpha1.VolumeBackupScheduleStatus, *v1alpha1.VolumeBackupScheduleStatus) error +} + +// NewRealVolumeBackupScheduleStatusUpdater returns a VolumeBackupScheduleStatusUpdaterInterface that updates the Status of a VolumeBackupScheduleStatus, +// using the supplied client and bsLister. +func NewRealVolumeBackupScheduleStatusUpdater(deps *BrFedDependencies) VolumeBackupScheduleStatusUpdaterInterface { + return &realVolumeBackupScheduleStatusUpdater{ + deps: deps, + } +} + +type realVolumeBackupScheduleStatusUpdater struct { + deps *BrFedDependencies +} + +func (u *realVolumeBackupScheduleStatusUpdater) UpdateBackupScheduleStatus( + bs *v1alpha1.VolumeBackupSchedule, + newStatus *v1alpha1.VolumeBackupScheduleStatus, + oldStatus *v1alpha1.VolumeBackupScheduleStatus) error { + + ns := bs.GetNamespace() + bsName := bs.GetName() + // don't wait due to limited number of clients, but backoff after the default number of steps + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + _, updateErr := u.deps.Clientset.FederationV1alpha1().VolumeBackupSchedules(ns).Update(context.TODO(), bs, metav1.UpdateOptions{}) + if updateErr == nil { + klog.Infof("BackupSchedule: [%s/%s] updated successfully", ns, bsName) + return nil + } + if updated, err := u.deps.VolumeBackupScheduleLister.VolumeBackupSchedules(ns).Get(bsName); err == nil { + // make a copy so we don't mutate the shared cache + bs = updated.DeepCopy() + bs.Status = *newStatus + } else { + utilruntime.HandleError(fmt.Errorf("error getting updated backupSchedule %s/%s from lister: %v", ns, bsName, err)) + } + + return updateErr + }) + return err +} + +var _ VolumeBackupScheduleStatusUpdaterInterface = &realVolumeBackupScheduleStatusUpdater{} + +// FakeVolumeBackupScheduleStatusUpdater is a fake VolumeBackupScheduleStatusUpdaterInterface +type FakeVolumeBackupScheduleStatusUpdater struct { + BsLister listers.VolumeBackupScheduleLister + BsIndexer cache.Indexer + updateBsTracker RequestTracker +} + +// NewFakeVolumeBackupScheduleStatusUpdater returns a FakeVolumeBackupScheduleStatusUpdater +func NewFakeVolumeBackupScheduleStatusUpdater(bsInformer informers.VolumeBackupScheduleInformer) *FakeVolumeBackupScheduleStatusUpdater { + return &FakeVolumeBackupScheduleStatusUpdater{ + bsInformer.Lister(), + bsInformer.Informer().GetIndexer(), + RequestTracker{}, + } +} + +// SetUpdateBackupScheduleError sets the error attributes of updateBackupScheduleTracker +func (u *FakeVolumeBackupScheduleStatusUpdater) SetUpdateBackupScheduleError(err error, after int) { + u.updateBsTracker.err = err + u.updateBsTracker.after = after + u.updateBsTracker.SetError(err).SetAfter(after) +} + +// UpdateBackupScheduleStatus updates the BackupSchedule +func (u *FakeVolumeBackupScheduleStatusUpdater) UpdateBackupScheduleStatus(bs *v1alpha1.VolumeBackupSchedule, _ *v1alpha1.VolumeBackupScheduleStatus, _ *v1alpha1.VolumeBackupScheduleStatus) error { + defer u.updateBsTracker.Inc() + if u.updateBsTracker.ErrorReady() { + defer u.updateBsTracker.Reset() + return u.updateBsTracker.GetError() + } + + return u.BsIndexer.Update(bs) +} + +var _ VolumeBackupScheduleStatusUpdaterInterface = &FakeVolumeBackupScheduleStatusUpdater{} diff --git a/pkg/controller/fed_volume_backup_control.go b/pkg/controller/fed_volume_backup_control.go index 0de42e3bb4c..13ff5892b58 100644 --- a/pkg/controller/fed_volume_backup_control.go +++ b/pkg/controller/fed_volume_backup_control.go @@ -31,7 +31,7 @@ import ( listers "github.com/pingcap/tidb-operator/pkg/client/federation/listers/pingcap/v1alpha1" ) -// FedVolumeBackupControlInterface manages federaton VolumeBackups used in VolumeBackupSchedule +// FedVolumeBackupControlInterface manages federation VolumeBackups used in VolumeBackupSchedule type FedVolumeBackupControlInterface interface { CreateVolumeBackup(backup *v1alpha1.VolumeBackup) (*v1alpha1.VolumeBackup, error) DeleteVolumeBackup(backup *v1alpha1.VolumeBackup) error @@ -108,16 +108,18 @@ type FakeFedVolumeBackupControl struct { volumeBackupLister listers.VolumeBackupLister volumeBackupIndexer cache.Indexer createVolumeBackupTracker RequestTracker + updateVolumeBackupTracker RequestTracker deleteVolumeBackupTracker RequestTracker } -// NewFakeBackupControl returns a FakeBackupControl +// NewFakeFedVolumeBackupControl returns a FakeFedVolumeBackupControl func NewFakeFedVolumeBackupControl(volumeBackupInformer informers.VolumeBackupInformer) *FakeFedVolumeBackupControl { return &FakeFedVolumeBackupControl{ volumeBackupInformer.Lister(), volumeBackupInformer.Informer().GetIndexer(), RequestTracker{}, RequestTracker{}, + RequestTracker{}, } } @@ -126,6 +128,11 @@ func (fbc *FakeFedVolumeBackupControl) SetCreateVolumeBackupError(err error, aft fbc.createVolumeBackupTracker.SetError(err).SetAfter(after) } +// SetUpdateVolumeBackupError sets the error attributes of createVolumeBackupTracker +func (fbc *FakeFedVolumeBackupControl) SetUpdateVolumeBackupError(err error, after int) { + fbc.updateVolumeBackupTracker.SetError(err).SetAfter(after) +} + // SetDeleteVolumeBackupError sets the error attributes of deleteVolumeBackupTracker func (fbc *FakeFedVolumeBackupControl) SetDeleteVolumeBackupError(err error, after int) { fbc.deleteVolumeBackupTracker.SetError(err).SetAfter(after) diff --git a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control.go b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control.go index ad568e2eaf8..8a7d965013c 100644 --- a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control.go +++ b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control.go @@ -14,10 +14,11 @@ package fedvolumebackupschedule import ( + apiequality "k8s.io/apimachinery/pkg/api/equality" + errorutils "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/cache" "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1" - "github.com/pingcap/tidb-operator/pkg/client/federation/clientset/versioned" informers "github.com/pingcap/tidb-operator/pkg/client/federation/informers/externalversions/pingcap/v1alpha1" "github.com/pingcap/tidb-operator/pkg/controller" "github.com/pingcap/tidb-operator/pkg/fedvolumebackup" @@ -31,24 +32,41 @@ type ControlInterface interface { UpdateBackupSchedule(volumeBackupSchedule *v1alpha1.VolumeBackupSchedule) error } -// NewDefaultVolumeBackupScheduleControl returns a new instance of the default VolumeBackupSchedue ControlInterface implementation. +// NewDefaultVolumeBackupScheduleControl returns a new instance of the default VolumeBackupSchedule ControlInterface implementation. func NewDefaultVolumeBackupScheduleControl( - cli versioned.Interface, + statusUpdater controller.VolumeBackupScheduleStatusUpdaterInterface, backupScheduleManager fedvolumebackup.BackupScheduleManager) ControlInterface { return &defaultBackupScheduleControl{ - cli, + statusUpdater, backupScheduleManager, } } type defaultBackupScheduleControl struct { - cli versioned.Interface - bsManager fedvolumebackup.BackupScheduleManager + statusUpdater controller.VolumeBackupScheduleStatusUpdaterInterface + bsManager fedvolumebackup.BackupScheduleManager } // UpdateBackupSchedule executes the core logic loop for a VolumeBackupSchedule. -func (c *defaultBackupScheduleControl) UpdateBackupSchedule(volumeBackupSchedule *v1alpha1.VolumeBackupSchedule) error { - return c.bsManager.Sync(volumeBackupSchedule) +func (c *defaultBackupScheduleControl) UpdateBackupSchedule(vbs *v1alpha1.VolumeBackupSchedule) error { + var errs []error + oldStatus := vbs.Status.DeepCopy() + + if err := c.updateBackupSchedule(vbs); err != nil { + errs = append(errs, err) + } + if apiequality.Semantic.DeepEqual(&vbs.Status, oldStatus) { + return errorutils.NewAggregate(errs) + } + if err := c.statusUpdater.UpdateBackupScheduleStatus(vbs.DeepCopy(), &vbs.Status, oldStatus); err != nil { + errs = append(errs, err) + } + + return errorutils.NewAggregate(errs) +} + +func (c *defaultBackupScheduleControl) updateBackupSchedule(vbs *v1alpha1.VolumeBackupSchedule) error { + return c.bsManager.Sync(vbs) } var _ ControlInterface = &defaultBackupScheduleControl{} diff --git a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control_test.go b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control_test.go new file mode 100644 index 00000000000..b35040c90d8 --- /dev/null +++ b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_control_test.go @@ -0,0 +1,120 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. +package fedvolumebackupschedule + +import ( + "fmt" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" + "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client/federation/clientset/versioned/fake" + informers "github.com/pingcap/tidb-operator/pkg/client/federation/informers/externalversions" + "github.com/pingcap/tidb-operator/pkg/controller" + "github.com/pingcap/tidb-operator/pkg/fedvolumebackup/backupschedule" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBackupScheduleControlUpdateBackupSchedule(t *testing.T) { + g := NewGomegaWithT(t) + + type testcase struct { + name string + update func(bs *v1alpha1.VolumeBackupSchedule) + syncBsManagerErr bool + updateStatusErr bool + errExpectFn func(*GomegaWithT, error) + } + testFn := func(test *testcase, t *testing.T) { + t.Log(test.name) + + bs := newBackupSchedule() + if test.update != nil { + test.update(bs) + } + control, bsManager, bsStatusUpdater := newFakeBackupScheduleControl() + + if test.syncBsManagerErr { + bsManager.SetSyncError(fmt.Errorf("backup schedule sync error")) + } + + if test.updateStatusErr { + bsStatusUpdater.SetUpdateBackupScheduleError(fmt.Errorf("update backupSchedule status error"), 0) + } + + err := control.UpdateBackupSchedule(bs) + if test.errExpectFn != nil { + test.errExpectFn(g, err) + } + } + tests := []testcase{ + { + name: "backup schedule sync error", + update: nil, + syncBsManagerErr: true, + updateStatusErr: false, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(strings.Contains(err.Error(), "backup schedule sync error")).To(Equal(true)) + }, + }, + { + name: "backup schedule status is not updated", + update: nil, + syncBsManagerErr: false, + updateStatusErr: false, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + { + name: "backup schedule status update failed", + update: func(bs *v1alpha1.VolumeBackupSchedule) { + bs.Status.LastBackupTime = &metav1.Time{Time: time.Now()} + }, + syncBsManagerErr: false, + updateStatusErr: true, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(strings.Contains(err.Error(), "update backupSchedule status error")).To(Equal(true)) + }, + }, + { + name: "normal", + update: func(bs *v1alpha1.VolumeBackupSchedule) { + bs.Status.LastBackupTime = &metav1.Time{Time: time.Now()} + }, + syncBsManagerErr: false, + updateStatusErr: false, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + } + + for i := range tests { + testFn(&tests[i], t) + } +} + +func newFakeBackupScheduleControl() (ControlInterface, *backupschedule.FakeBackupScheduleManager, *controller.FakeVolumeBackupScheduleStatusUpdater) { + cli := fake.NewSimpleClientset() + bsInformer := informers.NewSharedInformerFactory(cli, 0).Federation().V1alpha1().VolumeBackupSchedules() + statusUpdater := controller.NewFakeVolumeBackupScheduleStatusUpdater(bsInformer) + bsManager := backupschedule.NewFakeBackupScheduleManager() + control := NewDefaultVolumeBackupScheduleControl(statusUpdater, bsManager) + + return control, bsManager, statusUpdater +} diff --git a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller.go b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller.go index d3db48aa160..705500e0aa5 100644 --- a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller.go +++ b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller.go @@ -45,7 +45,7 @@ type Controller struct { func NewController(deps *controller.BrFedDependencies) *Controller { c := &Controller{ deps: deps, - control: NewDefaultVolumeBackupScheduleControl(deps.Clientset, backupschedule.NewBackupScheduleManager(deps)), + control: NewDefaultVolumeBackupScheduleControl(controller.NewRealVolumeBackupScheduleStatusUpdater(deps), backupschedule.NewBackupScheduleManager(deps)), queue: workqueue.NewNamedRateLimitingQueue( controller.NewControllerRateLimiter(1*time.Second, 100*time.Second), "volumeBackupSchedule", diff --git a/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller_test.go b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller_test.go new file mode 100644 index 00000000000..02a118e5b0f --- /dev/null +++ b/pkg/controller/fedvolumebackupschedule/fed_volume_backup_schedule_controller_test.go @@ -0,0 +1,180 @@ +// Copyright 2023 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package fedvolumebackupschedule + +import ( + "fmt" + "strings" + "testing" + + "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/controller" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + "k8s.io/utils/pointer" +) + +func TestBackupScheduleControllerEnqueueBackupSchedule(t *testing.T) { + g := NewGomegaWithT(t) + bks := newBackupSchedule() + bsc, _, _ := newFakeBackupScheduleController() + bsc.enqueueBackupSchedule(bks) + g.Expect(bsc.queue.Len()).To(Equal(1)) +} + +func TestBackupScheduleControllerEnqueueBackupScheduleFailed(t *testing.T) { + g := NewGomegaWithT(t) + bsc, _, _ := newFakeBackupScheduleController() + bsc.enqueueBackupSchedule(struct{}{}) + g.Expect(bsc.queue.Len()).To(Equal(0)) +} + +func TestBackupScheduleControllerSync(t *testing.T) { + g := NewGomegaWithT(t) + type testcase struct { + name string + addBsToIndexer bool + errWhenUpdateBackupSchedule bool + invalidKeyFn func(bs *v1alpha1.VolumeBackupSchedule) string + errExpectFn func(*GomegaWithT, error) + } + + testFn := func(test *testcase, t *testing.T) { + t.Log(test.name) + + bs := newBackupSchedule() + bsc, bsIndexer, bsControl := newFakeBackupScheduleController() + + if test.addBsToIndexer { + err := bsIndexer.Add(bs) + g.Expect(err).NotTo(HaveOccurred()) + } + + key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(bs) + if test.invalidKeyFn != nil { + key = test.invalidKeyFn(bs) + } + + if test.errWhenUpdateBackupSchedule { + bsControl.SetUpdateVolumeBackupError(fmt.Errorf("update backup schedule failed"), 0) + } + + err := bsc.sync(key) + + if test.errExpectFn != nil { + test.errExpectFn(g, err) + } + } + + tests := []testcase{ + { + name: "normal", + addBsToIndexer: true, + errWhenUpdateBackupSchedule: false, + invalidKeyFn: nil, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + { + name: "invalid backup key", + addBsToIndexer: true, + errWhenUpdateBackupSchedule: false, + invalidKeyFn: func(bs *v1alpha1.VolumeBackupSchedule) string { + return fmt.Sprintf("test/demo/%s", bs.GetName()) + }, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).To(HaveOccurred()) + }, + }, + { + name: "can't found backup schedule", + addBsToIndexer: false, + errWhenUpdateBackupSchedule: false, + invalidKeyFn: nil, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).NotTo(HaveOccurred()) + }, + }, + { + name: "update backup schedule failed", + addBsToIndexer: true, + errWhenUpdateBackupSchedule: true, + invalidKeyFn: nil, + errExpectFn: func(g *GomegaWithT, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(strings.Contains(err.Error(), "update backup schedule failed")).To(Equal(true)) + }, + }, + } + + for i := range tests { + testFn(&tests[i], t) + } + +} + +func newFakeBackupScheduleController() (*Controller, cache.Indexer, *controller.FakeFedVolumeBackupControl) { + fakeDeps := controller.NewFakeDependencies() + bsc := NewController(fakeDeps) + bsInformer := fakeDeps.InformerFactory.Pingcap().V1alpha1().BackupSchedules() + backupScheduleControl := NewFakeBackupScheduleControl(bsInformer) + bsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: bsc.enqueueBackupSchedule, + UpdateFunc: func(old, cur interface{}) { + bsc.enqueueBackupSchedule(cur) + }, + DeleteFunc: bsc.enqueueBackupSchedule, + }) + bsc.control = backupScheduleControl + return bsc, bsInformer.Informer().GetIndexer(), backupScheduleControl +} + +func newBackupSchedule() *v1alpha1.VolumeBackupSchedule { + return &v1alpha1.VolumeBackupSchedule{ + TypeMeta: metav1.TypeMeta{ + Kind: "BackupScheduler", + APIVersion: "pingcap.com/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bks", + Namespace: corev1.NamespaceDefault, + UID: types.UID("test-bks"), + }, + Spec: v1alpha1.VolumeBackupScheduleSpec{ + Schedule: "1 */10 * * *", + MaxBackups: pointer.Int32Ptr(10), + BackupTemplate: v1alpha1.BackupSpec{ + From: &v1alpha1.TiDBAccessConfig{ + Host: "10.1.1.2", + Port: v1alpha1.DefaultTiDBServicePort, + User: v1alpha1.DefaultTidbUser, + SecretName: "demo1-tidb-secret", + }, + StorageProvider: v1alpha1.StorageProvider{ + S3: &v1alpha1.S3StorageProvider{ + Provider: v1alpha1.S3StorageProviderTypeCeph, + Endpoint: "http://10.0.0.1", + SecretName: "demo", + Bucket: "test1-demo1", + }, + }, + }, + }, + } +} diff --git a/pkg/fedvolumebackup/backup/backup_manager.go b/pkg/fedvolumebackup/backup/backup_manager.go index 0c183625717..59d6b0ae0cc 100644 --- a/pkg/fedvolumebackup/backup/backup_manager.go +++ b/pkg/fedvolumebackup/backup/backup_manager.go @@ -118,20 +118,6 @@ func (bm *backupManager) runBackup(ctx context.Context, volumeBackup *v1alpha1.V bm.setVolumeBackupRunning(&volumeBackup.Status) } -<<<<<<< HEAD -======= - if bm.skipSync(volumeBackup) { - klog.Infof("skip VolumeBackup %s/%s", ns, name) - return nil - } - - ctx := context.Background() - backupMembers, err := bm.listAllBackupMembers(ctx, volumeBackup) - if err != nil { - return err - } - ->>>>>>> 7d84cf89e (br: update backup member to status) if len(backupMembers) == 0 { return false, bm.initializeVolumeBackup(ctx, volumeBackup) } @@ -184,6 +170,14 @@ func (bm *backupManager) setVolumeBackupRunning(volumeBackupStatus *v1alpha1.Vol }) } +func (bm *backupManager) setVolumeBackupPrepared(volumeBackupStatus *v1alpha1.VolumeBackupStatus) { + volumeBackupStatus.TimeStarted = metav1.Now() + v1alpha1.UpdateVolumeBackupCondition(volumeBackupStatus, &v1alpha1.VolumeBackupCondition{ + Type: v1alpha1.VolumeBackupPrepared, + Status: corev1.ConditionTrue, + }) +} + func (bm *backupManager) listAllBackupMembers(ctx context.Context, volumeBackup *v1alpha1.VolumeBackup) ([]*volumeBackupMember, error) { backupMembers := make([]*volumeBackupMember, 0, len(volumeBackup.Spec.Clusters)) for _, memberCluster := range volumeBackup.Spec.Clusters { @@ -349,14 +343,6 @@ func (bm *backupManager) setVolumeBackupCleaned(volumeBackupStatus *v1alpha1.Vol }) } -func (bm *backupManager) setVolumeBackupPrepared(volumeBackupStatus *v1alpha1.VolumeBackupStatus) { - volumeBackupStatus.TimeStarted = metav1.Now() - v1alpha1.UpdateVolumeBackupCondition(volumeBackupStatus, &v1alpha1.VolumeBackupCondition{ - Type: v1alpha1.VolumeBackupPrepared, - Status: corev1.ConditionTrue, - }) -} - func (bm *backupManager) setVolumeBackupSize(volumeBackupStatus *v1alpha1.VolumeBackupStatus, backupMembers []*volumeBackupMember) { var totalBackupSize int64 for _, backupMember := range backupMembers { @@ -417,11 +403,7 @@ func (bm *backupManager) buildBackupMember(volumeBackupName string, clusterMembe } func (bm *backupManager) skipSync(volumeBackup *v1alpha1.VolumeBackup) bool { -<<<<<<< HEAD return volumeBackup.DeletionTimestamp == nil && (v1alpha1.IsVolumeBackupComplete(volumeBackup) || v1alpha1.IsVolumeBackupFailed(volumeBackup)) -======= - return v1alpha1.IsVolumeBackupComplete(volumeBackup) || v1alpha1.IsVolumeBackupFailed(volumeBackup) ->>>>>>> 7d84cf89e (br: update backup member to status) } func (bm *backupManager) generateBackupMemberName(volumeBackupName, k8sClusterName string) string { diff --git a/pkg/fedvolumebackup/backupschedule/backup_schedule_manager.go b/pkg/fedvolumebackup/backupschedule/backup_schedule_manager.go index deeb552f971..be2d03f7e5e 100644 --- a/pkg/fedvolumebackup/backupschedule/backup_schedule_manager.go +++ b/pkg/fedvolumebackup/backupschedule/backup_schedule_manager.go @@ -14,8 +14,17 @@ package backupschedule import ( + "fmt" + "sort" "time" + perrors "github.com/pingcap/errors" + "github.com/pingcap/tidb-operator/pkg/apis/label" + "github.com/pingcap/tidb-operator/pkg/apis/util/config" + "github.com/pingcap/tidb-operator/pkg/util" + "github.com/robfig/cron" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" "github.com/pingcap/tidb-operator/pkg/apis/federation/pingcap/v1alpha1" @@ -38,15 +47,306 @@ func NewBackupScheduleManager(deps *controller.BrFedDependencies) fedvolumebacku } } -func (bm *backupScheduleManager) Sync(volumeBackupSchedule *v1alpha1.VolumeBackupSchedule) error { - ns := volumeBackupSchedule.GetNamespace() - name := volumeBackupSchedule.GetName() - // TODO(federation): implement the main logic of backupSchedule - klog.Infof("sync VolumeBackupSchedule %s/%s", ns, name) +func (bm *backupScheduleManager) Sync(vbs *v1alpha1.VolumeBackupSchedule) error { + defer bm.backupGC(vbs) + if vbs.Spec.Pause { + return controller.IgnoreErrorf("backupSchedule %s/%s has been paused", vbs.GetNamespace(), vbs.GetName()) + } + + if err := bm.canPerformNextBackup(vbs); err != nil { + return err + } + + scheduledTime, err := getLastScheduledTime(vbs, bm.now) + if scheduledTime == nil { + return err + } + + // TODO: do we need to delete last backup job also? + backup, err := createBackup(bm.deps.FedVolumeBackupControl, vbs, *scheduledTime) + if err != nil { + return err + } + + vbs.Status.LastBackup = backup.GetName() + vbs.Status.LastBackupTime = &metav1.Time{Time: *scheduledTime} + vbs.Status.AllBackupCleanTime = nil return nil } +// getLastScheduledTime return the newest time need to be scheduled according last backup time. +// the return time is not before now and return nil if there's no such time. +func getLastScheduledTime(vbs *v1alpha1.VolumeBackupSchedule, nowFn nowFn) (*time.Time, error) { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + sched, err := cron.ParseStandard(vbs.Spec.Schedule) + if err != nil { + return nil, fmt.Errorf("parse backup schedule %s/%s cron format %s failed, err: %v", ns, bsName, vbs.Spec.Schedule, err) + } + + var earliestTime time.Time + if vbs.Status.LastBackupTime != nil { + earliestTime = vbs.Status.LastBackupTime.Time + } else if vbs.Status.AllBackupCleanTime != nil { + // Recovery from a long paused backup schedule may cause problem like "incorrect clock", + // so we introduce AllBackupCleanTime field to solve this problem. + earliestTime = vbs.Status.AllBackupCleanTime.Time + } else { + // If none found, then this is either a recently created backupSchedule, + // or the backupSchedule status info was somehow lost, + // or that we have started a backup, but have not update backupSchedule status yet + // (distributed systems can have arbitrary delays). + // In any case, use the creation time of the backupSchedule as last known start time. + earliestTime = vbs.ObjectMeta.CreationTimestamp.Time + } + + now := nowFn() + if earliestTime.After(now) { + // timestamp fallback, waiting for the next backup schedule period + klog.Errorf("backup schedule %s/%s timestamp fallback, lastBackupTime: %s, now: %s", + ns, bsName, earliestTime.Format(time.RFC3339), now.Format(time.RFC3339)) + return nil, nil + } + + var scheduledTimes []time.Time + for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) { + scheduledTimes = append(scheduledTimes, t) + // If there is a bug somewhere, or incorrect clock + // on controller's server or apiservers (for setting creationTimestamp) + // then there could be so many missed start times (it could be off + // by decades or more), that it would eat up all the CPU and memory + // of this controller. In that case, we want to not try to list + // all the missed start times. + // + // I've somewhat arbitrarily picked 100, as more than 80, + // but less than "lots". + if len(scheduledTimes) > 100 { + // We can't get the last backup schedule time + if vbs.Status.LastBackupTime == nil && vbs.Status.AllBackupCleanTime != nil { + // Recovery backup schedule from pause status, should refresh AllBackupCleanTime to avoid unschedulable problem + vbs.Status.AllBackupCleanTime = &metav1.Time{Time: nowFn()} + return nil, controller.RequeueErrorf("recovery backup schedule %s/%s from pause status, refresh AllBackupCleanTime.", ns, bsName) + } + klog.Error("Too many missed start backup schedule time (> 100). Check the clock.") + return nil, nil + } + } + + if len(scheduledTimes) == 0 { + klog.V(4).Infof("unmet backup schedule %s/%s start time, waiting for the next backup schedule period", ns, bsName) + return nil, nil + } + scheduledTime := scheduledTimes[len(scheduledTimes)-1] + return &scheduledTime, nil +} + +func buildBackup(vbs *v1alpha1.VolumeBackupSchedule, timestamp time.Time) *v1alpha1.VolumeBackup { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + backupSpec := *vbs.Spec.BackupTemplate.DeepCopy() + + bsLabel := util.CombineStringMap(label.NewBackupSchedule().Instance(bsName).BackupSchedule(bsName), vbs.Labels) + backup := &v1alpha1.VolumeBackup{ + Spec: backupSpec, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: vbs.GetBackupCRDName(timestamp), + Labels: bsLabel, + Annotations: vbs.Annotations, + OwnerReferences: []metav1.OwnerReference{ + controller.GetFedVolumeBackupScheduleOwnerRef(vbs), + }, + }, + } + + return backup +} + +func createBackup(bkController controller.FedVolumeBackupControlInterface, vbs *v1alpha1.VolumeBackupSchedule, timestamp time.Time) (*v1alpha1.VolumeBackup, error) { + bk := buildBackup(vbs, timestamp) + return bkController.CreateVolumeBackup(bk) +} + +func (bm *backupScheduleManager) canPerformNextBackup(vbs *v1alpha1.VolumeBackupSchedule) error { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + backup, err := bm.deps.VolumeBackupLister.VolumeBackups(ns).Get(vbs.Status.LastBackup) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return fmt.Errorf("backup schedule %s/%s, get backup %s failed, err: %v", ns, bsName, vbs.Status.LastBackup, err) + } + + if v1alpha1.IsVolumeBackupComplete(backup) { + return nil + } + // If the last backup is in a failed state, but it is not scheduled yet, + // skip this sync round of the backup schedule and waiting the last backup. + return controller.RequeueErrorf("backup schedule %s/%s, the last backup %s is still running", ns, bsName, vbs.Status.LastBackup) +} + +func (bm *backupScheduleManager) backupGC(vbs *v1alpha1.VolumeBackupSchedule) { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + // if MaxBackups and MaxReservedTime are set at the same time, MaxReservedTime is preferred. + if vbs.Spec.MaxReservedTime != nil { + bm.backupGCByMaxReservedTime(vbs) + return + } + + if vbs.Spec.MaxBackups != nil && *vbs.Spec.MaxBackups > 0 { + bm.backupGCByMaxBackups(vbs) + return + } + klog.Warningf("backup schedule %s/%s does not set backup gc policy", ns, bsName) +} + +func (bm *backupScheduleManager) backupGCByMaxReservedTime(vbs *v1alpha1.VolumeBackupSchedule) { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + reservedTime, err := time.ParseDuration(*vbs.Spec.MaxReservedTime) + if err != nil { + klog.Errorf("backup schedule %s/%s, invalid MaxReservedTime %s", ns, bsName, *vbs.Spec.MaxReservedTime) + return + } + + backupsList, err := bm.getBackupList(vbs) + if err != nil { + klog.Errorf("backupGCByMaxReservedTime, err: %s", err) + return + } + + ascBackups := sortSnapshotBackups(backupsList) + if len(ascBackups) == 0 { + return + } + + var expiredBackups []*v1alpha1.VolumeBackup + + expiredBackups, err = calculateExpiredBackups(ascBackups, reservedTime) + if err != nil { + klog.Errorf("caculate expired backups without log backup, err: %s", err) + return + } + + for _, backup := range expiredBackups { + // delete the expired backup + if err = bm.deps.FedVolumeBackupControl.DeleteVolumeBackup(backup); err != nil { + klog.Errorf("backup schedule %s/%s gc backup %s failed, err %v", ns, bsName, backup.GetName(), err) + return + } + klog.Infof("backup schedule %s/%s gc backup %s success", ns, bsName, backup.GetName()) + } + + if len(expiredBackups) == len(backupsList) && len(expiredBackups) > 0 { + // All backups have been deleted, so the last backup information in the backupSchedule should be reset + bm.resetLastBackup(vbs) + } +} + +// sortSnapshotBackups return snapshot backups to be GCed order by create time asc +func sortSnapshotBackups(backupsList []*v1alpha1.VolumeBackup) []*v1alpha1.VolumeBackup { + var ascBackupList = make([]*v1alpha1.VolumeBackup, 0) + + for _, backup := range backupsList { + // the backup status CommitTs will be empty after created. without this, all newly created backups will be GC'ed + if v1alpha1.IsVolumeBackupRunning(backup) || v1alpha1.IsBackupPrepared(backup) { + continue + } + ascBackupList = append(ascBackupList, backup) + } + + sort.Slice(ascBackupList, func(i, j int) bool { + return ascBackupList[i].CreationTimestamp.Unix() < ascBackupList[j].CreationTimestamp.Unix() + }) + return ascBackupList +} + +func calculateExpiredBackups(backupsList []*v1alpha1.VolumeBackup, reservedTime time.Duration) ([]*v1alpha1.VolumeBackup, error) { + expiredTS := config.TSToTSO(time.Now().Add(-1 * reservedTime).Unix()) + i := 0 + for ; i < len(backupsList); i++ { + startTS, err := config.ParseTSString(backupsList[i].Status.CommitTs) + if err != nil { + return nil, perrors.Annotatef(err, "parse start tso: %s", backupsList[i].Status.CommitTs) + } + if startTS >= expiredTS { + break + } + } + return backupsList[:i], nil +} + +func (bm *backupScheduleManager) getBackupList(bs *v1alpha1.VolumeBackupSchedule) ([]*v1alpha1.VolumeBackup, error) { + ns := bs.GetNamespace() + bsName := bs.GetName() + + backupLabels := label.NewBackupSchedule().Instance(bsName).BackupSchedule(bsName) + selector, err := backupLabels.Selector() + if err != nil { + return nil, fmt.Errorf("generate backup schedule %s/%s label selector failed, err: %v", ns, bsName, err) + } + backupsList, err := bm.deps.VolumeBackupLister.VolumeBackups(ns).List(selector) + if err != nil { + return nil, fmt.Errorf("get backup schedule %s/%s backup list failed, selector: %s, err: %v", ns, bsName, selector, err) + } + + return backupsList, nil +} + +func (bm *backupScheduleManager) backupGCByMaxBackups(vbs *v1alpha1.VolumeBackupSchedule) { + ns := vbs.GetNamespace() + bsName := vbs.GetName() + + backupsList, err := bm.getBackupList(vbs) + if err != nil { + klog.Errorf("backupGCByMaxBackups failed, err: %s", err) + return + } + + sort.Sort(byCreateTimeDesc(backupsList)) + + var deleteCount int + for i, backup := range backupsList { + if i < int(*vbs.Spec.MaxBackups) { + continue + } + // delete the backup + if err := bm.deps.FedVolumeBackupControl.DeleteVolumeBackup(backup); err != nil { + klog.Errorf("backup schedule %s/%s gc backup %s failed, err %v", ns, bsName, backup.GetName(), err) + return + } + deleteCount += 1 + klog.Infof("backup schedule %s/%s gc backup %s success", ns, bsName, backup.GetName()) + } + + if deleteCount == len(backupsList) && deleteCount > 0 { + // All backups have been deleted, so the last backup information in the backupSchedule should be reset + bm.resetLastBackup(vbs) + } +} + +func (bm *backupScheduleManager) resetLastBackup(vbs *v1alpha1.VolumeBackupSchedule) { + vbs.Status.LastBackupTime = nil + vbs.Status.LastBackup = "" + vbs.Status.AllBackupCleanTime = &metav1.Time{Time: bm.now()} +} + +type byCreateTimeDesc []*v1alpha1.VolumeBackup + +func (b byCreateTimeDesc) Len() int { return len(b) } +func (b byCreateTimeDesc) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byCreateTimeDesc) Less(i, j int) bool { + return b[j].ObjectMeta.CreationTimestamp.Before(&b[i].ObjectMeta.CreationTimestamp) +} + var _ fedvolumebackup.BackupScheduleManager = &backupScheduleManager{} type FakeBackupScheduleManager struct { diff --git a/pkg/fedvolumebackup/backupschedule/backup_schedule_manager_test.go b/pkg/fedvolumebackup/backupschedule/backup_schedule_manager_test.go new file mode 100644 index 00000000000..a17926a2693 --- /dev/null +++ b/pkg/fedvolumebackup/backupschedule/backup_schedule_manager_test.go @@ -0,0 +1,516 @@ +package backupschedule + +import ( + "fmt" + "github.com/google/go-cmp/cmp" + "github.com/pingcap/tidb-operator/pkg/apis/label" + "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/backup/constants" + "github.com/pingcap/tidb-operator/pkg/controller" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/utils/pointer" + "strconv" + "time" +) + +// Copyright 2023 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package backupschedule + +import ( +"context" +"fmt" +"strconv" +"testing" +"time" + +"github.com/google/go-cmp/cmp" +. "github.com/onsi/gomega" +"github.com/pingcap/tidb-operator/pkg/apis/label" +"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1" +"github.com/pingcap/tidb-operator/pkg/backup/constants" +"github.com/pingcap/tidb-operator/pkg/controller" +v1 "k8s.io/api/core/v1" +metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +"k8s.io/apimachinery/pkg/labels" +"k8s.io/utils/pointer" +) + +func TestManager(t *testing.T) { + g := NewGomegaWithT(t) + helper := newHelper(t) + defer helper.close() + deps := helper.deps + m := NewBackupScheduleManager(deps).(*backupScheduleManager) + var err error + bs := &v1alpha1.BackupSchedule{} + bs.Namespace = "ns" + bs.Name = "bsname" + + // test pause + bs.Spec.Pause = true + err = m.Sync(bs) + g.Expect(err).Should(BeAssignableToTypeOf(&controller.IgnoreError{})) + g.Expect(err.Error()).Should(MatchRegexp(".*has been paused.*")) + + // test canPerformNextBackup + // + // test not found last backup + bs.Spec.Pause = false + bs.Status.LastBackup = "backupname" + err = m.canPerformNextBackup(bs) + g.Expect(err).Should(BeNil()) + + // test backup complete + bk := &v1alpha1.Backup{} + bk.Namespace = bs.Namespace + bk.Name = bs.Status.LastBackup + bk.Status.Conditions = append(bk.Status.Conditions, v1alpha1.BackupCondition{ + Type: v1alpha1.BackupComplete, + Status: v1.ConditionTrue, + }) + helper.createBackup(bk) + err = m.canPerformNextBackup(bs) + g.Expect(err).Should(BeNil()) + helper.deleteBackup(bk) + + // test last backup failed state and not scheduled yet + bk.Status.Conditions = nil + bk.Status.Conditions = append(bk.Status.Conditions, v1alpha1.BackupCondition{ + Type: v1alpha1.BackupFailed, + Status: v1.ConditionTrue, + }) + helper.createBackup(bk) + err = m.canPerformNextBackup(bs) + g.Expect(err).Should(BeAssignableToTypeOf(&controller.RequeueError{})) + helper.deleteBackup(bk) + + t.Log("start test normal Sync") + bk.Status.Conditions = nil + bs.Spec.Schedule = "0 0 * * *" // Run at midnight every day + + now := time.Now() + m.now = func() time.Time { return now.AddDate(0, 0, -101) } + m.resetLastBackup(bs) + // 10 backup, one per day + for i := -9; i <= 0; i++ { + t.Log("loop id ", i) + m.now = func() time.Time { return now.AddDate(0, 0, i) } + err = m.Sync(bs) + g.Expect(err).Should(BeNil()) + bks := helper.checkBacklist(bs.Namespace, i+10, false) + // complete the backup created + for i := range bks.Items { + bk := bks.Items[i] + changed := v1alpha1.UpdateBackupCondition(&bk.Status, &v1alpha1.BackupCondition{ + Type: v1alpha1.BackupComplete, + Status: v1.ConditionTrue, + }) + if changed { + bk.CreationTimestamp = metav1.Time{Time: m.now()} + bk.Status.CommitTs = getTSOStr(m.now().Add(10 * time.Minute).Unix()) + t.Log("complete backup: ", bk.Name) + helper.updateBackup(&bk) + } + g.Expect(err).Should(BeNil()) + } + } + + t.Log("test setting MaxBackups") + m.now = time.Now + bs.Spec.MaxBackups = pointer.Int32Ptr(5) + err = m.Sync(bs) + g.Expect(err).Should(BeNil()) + helper.checkBacklist(bs.Namespace, 5, false) + + t.Log("test setting MaxReservedTime") + bs.Spec.MaxBackups = nil + bs.Spec.MaxReservedTime = pointer.StringPtr("71h") + err = m.Sync(bs) + g.Expect(err).Should(BeNil()) + helper.checkBacklist(bs.Namespace, 3, false) + + t.Log("test has log backup") + bs.Spec.LogBackupTemplate = &v1alpha1.BackupSpec{Mode: v1alpha1.BackupModeLog} + logBackup := buildLogBackup(bs, now.Add(-72*time.Hour)) + logBackup.Status.CommitTs = getTSOStr(now.Add(-72 * time.Hour).Unix()) + logBackup.Status.LogCheckpointTs = getTSOStr(now.Unix()) + helper.createBackup(logBackup) + bs.Status.LogBackup = &logBackup.Name + bs.Spec.MaxReservedTime = pointer.StringPtr("23h") + err = m.Sync(bs) + g.Expect(err).Should(BeNil()) + helper.checkBacklist(bs.Namespace, 2, true) +} + +func TestGetLastScheduledTime(t *testing.T) { + g := NewGomegaWithT(t) + + bs := &v1alpha1.BackupSchedule{ + Spec: v1alpha1.BackupScheduleSpec{}, + Status: v1alpha1.BackupScheduleStatus{ + LastBackupTime: &metav1.Time{}, + }, + } + var getTime *time.Time + var err error + + // test invalid format schedule + bs.Spec.Schedule = "#$#$#$@" + _, err = getLastScheduledTime(bs, time.Now) + g.Expect(err).ShouldNot(BeNil()) + + bs.Spec.Schedule = "0 0 * * *" // Run once a day at midnight + now := time.Now() + + // test last backup time after now + bs.Status.LastBackupTime.Time = now.AddDate(0, 0, 1) + getTime, err = getLastScheduledTime(bs, time.Now) + g.Expect(err).Should(BeNil()) + g.Expect(getTime).Should(BeNil()) + + // test scheduled + for i := 0; i < 10; i++ { + bs.Status.LastBackupTime.Time = now.AddDate(0, 0, -i-1) + getTime, err = getLastScheduledTime(bs, time.Now) + g.Expect(err).Should(BeNil()) + g.Expect(getTime).ShouldNot(BeNil()) + expectTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + g.Expect(*getTime).Should(Equal(expectTime)) + } + + // test too many miss + bs.Status.LastBackupTime.Time = now.AddDate(-1000, 0, 0) + getTime, err = getLastScheduledTime(bs, time.Now) + g.Expect(err).Should(BeNil()) + g.Expect(getTime).Should(BeNil()) +} + +func TestBuildBackup(t *testing.T) { + now := time.Now() + var get *v1alpha1.Backup + + // build BackupSchedule template + bs := &v1alpha1.BackupSchedule{ + Spec: v1alpha1.BackupScheduleSpec{}, + Status: v1alpha1.BackupScheduleStatus{ + LastBackupTime: &metav1.Time{}, + }, + } + bs.Namespace = "ns" + bs.Name = "bsname" + + // build Backup template + bk := &v1alpha1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: bs.Namespace, + Name: bs.GetBackupCRDName(now), + Labels: label.NewBackupSchedule().Instance(bs.Name).BackupSchedule(bs.Name).Labels(), + OwnerReferences: []metav1.OwnerReference{ + controller.GetBackupScheduleOwnerRef(bs), + }, + }, + Spec: v1alpha1.BackupSpec{ + StorageSize: constants.DefaultStorageSize, + }, + } + + // test BR == nil + get = buildBackup(bs, now) + if diff := cmp.Diff(bk, get); diff != "" { + t.Errorf("unexpected (-want, +got): %s", diff) + } + // should keep StorageSize from BackupSchedule + bs.Spec.StorageSize = "9527G" + bk.Spec.StorageSize = bs.Spec.StorageSize + get = buildBackup(bs, now) + if diff := cmp.Diff(bk, get); diff != "" { + t.Errorf("unexpected (-want, +got): %s", diff) + } + + // test BR != nil + bs.Spec.BackupTemplate.BR = &v1alpha1.BRConfig{} + bk.Spec.BR = bs.Spec.BackupTemplate.BR.DeepCopy() + bk.Spec.StorageSize = "" // no use for BR + get = buildBackup(bs, now) + if diff := cmp.Diff(bk, get); diff != "" { + t.Errorf("unexpected (-want, +got): %s", diff) + } +} + +func TestCaculateExpiredBackupsWithLogBackup(t *testing.T) { + g := NewGomegaWithT(t) + type testCase struct { + backups []*v1alpha1.Backup + logBackup *v1alpha1.Backup + reservedTime time.Duration + expectedDeleteBackupCount int + expectedTruncateTS uint64 + } + + var ( + now = time.Now() + last10Min = now.Add(-time.Minute * 10).Unix() + last1Day = now.Add(-time.Hour * 24 * 1).Unix() + last2Day = now.Add(-time.Hour * 24 * 2).Unix() + last3Day = now.Add(-time.Hour * 24 * 3).Unix() + last4Day = now.Add(-time.Hour * 24 * 4).Unix() + ) + + testCases := []*testCase{ + // no backup should be deleted and log backup just start, no commit ts/checkpoint ts + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last10Min), + }, + logBackup: fakeLogBackup(nil, nil), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 0, + expectedTruncateTS: 0, + }, + // backup should be delete and log backup just start, no commit ts/checkpoint ts + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last3Day), + fakeBackup(&last2Day), + fakeBackup(&last1Day), + fakeBackup(&last10Min), + }, + logBackup: fakeLogBackup(&last10Min, nil), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 1, + expectedTruncateTS: 0, + }, + // no backup should be deleted, has log backup + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last3Day), + fakeBackup(&last1Day), + }, + logBackup: fakeLogBackup(&last4Day, &last10Min), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 0, + expectedTruncateTS: 0, + }, + // 1 backup should be deleted, no log backup should be truncated + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last3Day), + fakeBackup(&last2Day), + fakeBackup(&last1Day), + }, + logBackup: fakeLogBackup(&last1Day, &last10Min), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 1, + expectedTruncateTS: 0, + }, + // 2 backup should be deleted, no log backup should be truncated + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last4Day), + fakeBackup(&last3Day), + fakeBackup(&last2Day), + fakeBackup(&last1Day), + }, + logBackup: fakeLogBackup(&last1Day, &last10Min), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 2, + expectedTruncateTS: 0, + }, + // 2 backup should be deleted, has log backup should be truncated + { + backups: []*v1alpha1.Backup{ + fakeBackup(&last4Day), + fakeBackup(&last3Day), + fakeBackup(&last2Day), + fakeBackup(&last1Day), + }, + logBackup: fakeLogBackup(&last3Day, &last10Min), + reservedTime: 24 * time.Hour, + expectedDeleteBackupCount: 2, + expectedTruncateTS: getTSO(last2Day), + }, + } + + for _, tc := range testCases { + deletedBackups, truncateTS, err := calExpiredBackupsAndLogBackup(tc.backups, tc.logBackup, tc.reservedTime) + g.Expect(err).Should(BeNil()) + g.Expect(len(deletedBackups)).Should(Equal(tc.expectedDeleteBackupCount)) + g.Expect(truncateTS).Should(Equal(tc.expectedTruncateTS)) + } +} + +type helper struct { + t *testing.T + deps *controller.Dependencies + stop chan struct{} +} + +func newHelper(t *testing.T) *helper { + deps := controller.NewSimpleClientDependencies() + stop := make(chan struct{}) + deps.InformerFactory.Start(stop) + deps.KubeInformerFactory.Start(stop) + deps.InformerFactory.WaitForCacheSync(stop) + deps.KubeInformerFactory.WaitForCacheSync(stop) + + return &helper{ + t: t, + deps: deps, + stop: stop, + } +} + +func (h *helper) close() { + close(h.stop) +} + +// check for exists num Backup and return the exists backups "BackupList". +func (h *helper) checkBacklist(ns string, num int, checkLogBackupTruncate bool) (bks *v1alpha1.BackupList) { + t := h.t + deps := h.deps + g := NewGomegaWithT(t) + + check := func(backups []*v1alpha1.Backup) error { + snapshotBackups, logBackup := separateSnapshotBackupsAndLogBackup(backups) + // check snapshot backup num + if len(snapshotBackups) != num { + var names []string + for _, bk := range snapshotBackups { + names = append(names, bk.Name) + } + return fmt.Errorf("there %d backup, but should be %d, cur backups: %v", len(snapshotBackups), num, names) + } + if !checkLogBackupTruncate { + return nil + } + // check has log backup + if logBackup == nil { + return fmt.Errorf("there is no log backup, but should have") + } + // check truncateTSO, it should equal the earliest snapshot backup after gc + if len(snapshotBackups) == 0 { + return fmt.Errorf("there should have snapshot backup if need check log backup truncate tso") + } + if logBackup.Status.LogSuccessTruncateUntil != snapshotBackups[0].Spec.CommitTs { + return fmt.Errorf("log backup truncate tso should be %s, but cur is %s", snapshotBackups[0].Spec.CommitTs, logBackup.Status.LogSuccessTruncateUntil) + } + return nil + } + + t.Helper() + g.Eventually(func() error { + var err error + bks, err = deps.Clientset.PingcapV1alpha1().Backups(ns).List(context.TODO(), metav1.ListOptions{}) + g.Expect(err).Should(BeNil()) + backups := convertToBackupPtrList(bks.Items) + return check(backups) + }, time.Second*30).Should(BeNil()) + + g.Eventually(func() error { + var err error + backups, err := deps.BackupLister.Backups(ns).List(labels.Everything()) + g.Expect(err).Should(BeNil()) + return check(backups) + }, time.Second*30).Should(BeNil()) + + return +} + +func convertToBackupPtrList(backups []v1alpha1.Backup) []*v1alpha1.Backup { + backupPtrs := make([]*v1alpha1.Backup, 0) + for i := 0; i < len(backups); i++ { + backupPtrs = append(backupPtrs, &backups[i]) + } + return backupPtrs +} + +func (h *helper) updateBackup(bk *v1alpha1.Backup) { + t := h.t + deps := h.deps + g := NewGomegaWithT(t) + _, err := deps.Clientset.PingcapV1alpha1().Backups(bk.Namespace).Update(context.TODO(), bk, metav1.UpdateOptions{}) + g.Expect(err).Should(BeNil()) + + g.Eventually(func() error { + get, err := deps.BackupLister.Backups(bk.Namespace).Get(bk.Name) + if err != nil { + return err + } + + diff := cmp.Diff(get, bk) + if diff == "" { + return nil + } + + return fmt.Errorf("not synced yet: %s", diff) + }, time.Second*10).Should(BeNil()) +} + +func (h *helper) createBackup(bk *v1alpha1.Backup) { + t := h.t + deps := h.deps + g := NewGomegaWithT(t) + _, err := deps.Clientset.PingcapV1alpha1().Backups(bk.Namespace).Create(context.TODO(), bk, metav1.CreateOptions{}) + g.Expect(err).Should(BeNil()) + g.Eventually(func() error { + _, err := deps.BackupLister.Backups(bk.Namespace).Get(bk.Name) + return err + }, time.Second*10).Should(BeNil()) +} + +func (h *helper) deleteBackup(bk *v1alpha1.Backup) { + t := h.t + deps := h.deps + g := NewGomegaWithT(t) + err := deps.Clientset.PingcapV1alpha1().Backups(bk.Namespace).Delete(context.TODO(), bk.Name, metav1.DeleteOptions{}) + g.Expect(err).Should(BeNil()) + g.Eventually(func() error { + _, err := deps.BackupLister.Backups(bk.Namespace).Get(bk.Name) + return err + }, time.Second*10).ShouldNot(BeNil()) +} + +func fakeBackup(ts *int64) *v1alpha1.Backup { + backup := &v1alpha1.Backup{} + if ts == nil { + return backup + } + backup.Status.CommitTs = getTSOStr(*ts) + return backup +} + +func fakeLogBackup(startTS, checkPointTS *int64) *v1alpha1.Backup { + logBackup := &v1alpha1.Backup{} + if startTS == nil { + return logBackup + } + logBackup.Status.CommitTs = getTSOStr(*startTS) + if checkPointTS == nil { + return logBackup + } + logBackup.Status.LogCheckpointTs = getTSOStr(*checkPointTS) + return logBackup +} + +func getTSOStr(ts int64) string { + tso := getTSO(ts) + return strconv.FormatUint(tso, 10) +} + +func getTSO(ts int64) uint64 { + return uint64((ts << 18) * 1000) +} +