Skip to content

Commit

Permalink
Merge pull request kubevirt#8521 from akalenyu/ttl-export
Browse files Browse the repository at this point in the history
Add TTL field for VMExport objects
  • Loading branch information
kubevirt-bot authored Oct 12, 2022
2 parents 7bdb63e + 444d54b commit 84cf205
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 44 deletions.
8 changes: 8 additions & 0 deletions api/openapi-spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -19893,6 +19893,10 @@
"tokenSecretRef": {
"description": "TokenSecretRef is the name of the custom-defined secret that contains the token used by the export server pod",
"type": "string"
},
"ttlDuration": {
"description": "ttlDuration limits the lifetime of an export If this field is set, after this duration has passed from counting from CreationTimestamp, the export is eligible to be automatically deleted. If this field is omitted, a reasonable default is applied.",
"$ref": "#/definitions/k8s.io.apimachinery.pkg.apis.meta.v1.Duration"
}
}
},
Expand Down Expand Up @@ -19921,6 +19925,10 @@
"tokenSecretRef": {
"description": "TokenSecretRef is the name of the secret that contains the token used by the export server pod",
"type": "string"
},
"ttlExpirationTime": {
"description": "The time at which the VM Export will be completely removed according to specified TTL Formula is CreationTimestamp + TTL",
"$ref": "#/definitions/k8s.io.apimachinery.pkg.apis.meta.v1.Time"
}
}
},
Expand Down
57 changes: 45 additions & 12 deletions pkg/storage/export/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ func (ctrl *VMExportController) updateVMExport(vmExport *exportv1.VirtualMachine
return 0, err
}

if vmExport.Status == nil {
populateInitialVMExportStatus(vmExport)
}

if err := ctrl.handleVMExportToken(vmExport); err != nil {
return 0, err
}
Expand Down Expand Up @@ -510,6 +514,13 @@ func (ctrl *VMExportController) checkPod(vmExport *exportv1.VirtualMachineExport
return nil
}

if ttlExpiration := getExpirationTime(vmExport); !time.Now().Before(ttlExpiration) {
if err := ctrl.Client.VirtualMachineExport(vmExport.Namespace).Delete(context.Background(), vmExport.Name, metav1.DeleteOptions{}); err != nil {
return err
}
return nil
}

if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
// The server died or completed, delete the pod.
return ctrl.deleteExporterPod(vmExport, pod, exporterPodFailedOrCompletedEvent, fmt.Sprintf("Exporter pod %s/%s is in phase %s", pod.Namespace, pod.Name, pod.Status.Phase))
Expand Down Expand Up @@ -611,16 +622,6 @@ func (ctrl *VMExportController) createCertSecretManifest(vmExport *exportv1.Virt

// handleVMExportToken checks if a secret has been specified for the current export object and, if not, creates one specific to it
func (ctrl *VMExportController) handleVMExportToken(vmExport *exportv1.VirtualMachineExport) error {
if vmExport.Status == nil {
vmExport.Status = &exportv1.VirtualMachineExportStatus{
Phase: exportv1.Pending,
Conditions: []exportv1.Condition{
newReadyCondition(corev1.ConditionFalse, initializingReason, ""),
newPvcCondition(corev1.ConditionFalse, unknownReason, ""),
},
}
}

// If a tokenSecretRef has been specified, we assume that the corresponding
// secret has already been created and managed appropiately by the user
if vmExport.Spec.TokenSecretRef != nil {
Expand Down Expand Up @@ -839,7 +840,7 @@ func (ctrl *VMExportController) createExporterPodManifest(vmExport *exportv1.Vir
Value: "/token/token",
}, corev1.EnvVar{
Name: "DEADLINE",
Value: currentTime().Add(deadline).Format(time.RFC3339),
Value: getDeadlineValue(deadline, vmExport).Format(time.RFC3339),
})

tokenSecretRef := ""
Expand Down Expand Up @@ -971,7 +972,7 @@ func (ctrl *VMExportController) updateCommonVMExportStatusFields(vmExport, vmExp
}

func (ctrl *VMExportController) updateVMExportStatus(vmExport, vmExportCopy *exportv1.VirtualMachineExport) error {
if !equality.Semantic.DeepEqual(vmExport, vmExportCopy) {
if !equality.Semantic.DeepEqual(vmExport.Status, vmExportCopy.Status) {
if _, err := ctrl.Client.VirtualMachineExport(vmExportCopy.Namespace).Update(context.Background(), vmExportCopy, metav1.UpdateOptions{}); err != nil {
return err
}
Expand All @@ -992,6 +993,38 @@ func (ctrl *VMExportController) getCertParams() (*CertParams, error) {
}, nil
}

func populateInitialVMExportStatus(vmExport *exportv1.VirtualMachineExport) {
expireAt := metav1.NewTime(getExpirationTime(vmExport))
vmExport.Status = &exportv1.VirtualMachineExportStatus{
Phase: exportv1.Pending,
Conditions: []exportv1.Condition{
newReadyCondition(corev1.ConditionFalse, initializingReason, ""),
newPvcCondition(corev1.ConditionFalse, unknownReason, ""),
},
TTLExpirationTime: &expireAt,
}
}

func getDeadlineValue(deadline time.Duration, vmExport *exportv1.VirtualMachineExport) time.Time {
// Pod needs to shutdown to either cert rotate or because export TTL expired altogether
rotate := currentTime().Add(deadline)
ttlExpiration := getExpirationTime(vmExport)

if ttlExpiration.After(rotate) {
return rotate
}
return ttlExpiration
}

func getExpirationTime(vmExport *exportv1.VirtualMachineExport) time.Time {
ttl := exportv1.DefaultDurationTTL
if vmExport.Spec.TTLDuration != nil {
ttl = vmExport.Spec.TTLDuration.Duration
}

return vmExport.GetCreationTimestamp().Time.Add(ttl)
}

func newReadyCondition(status corev1.ConditionStatus, reason, message string) exportv1.Condition {
return exportv1.Condition{
Type: exportv1.ConditionReady,
Expand Down
72 changes: 49 additions & 23 deletions pkg/storage/export/export/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,19 +656,7 @@ var _ = Describe("Export controller", func() {

It("Should create a service based on the name of the VMExport", func() {
var service *k8sv1.Service
testVMExport := &exportv1.VirtualMachineExport{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: testNamespace,
},
Spec: exportv1.VirtualMachineExportSpec{
Source: k8sv1.TypedLocalObjectReference{
APIGroup: &k8sv1.SchemeGroupVersion.Group,
Kind: "PersistentVolumeClaim",
Name: testPVCName,
},
},
}
testVMExport := createPVCVMExport()
k8sClient.Fake.PrependReactor("create", "services", func(action testing.Action) (handled bool, obj runtime.Object, err error) {
create, ok := action.(testing.CreateAction)
Expect(ok).To(BeTrue())
Expand Down Expand Up @@ -716,7 +704,7 @@ var _ = Describe("Export controller", func() {
},
}
testVMExport := createPVCVMExport()
// We call handleVMExportToken to populate the Status field appropiately
populateInitialVMExportStatus(testVMExport)
err := controller.handleVMExportToken(testVMExport)
Expect(testVMExport.Status.TokenSecretRef).ToNot(BeNil())
Expect(err).ToNot(HaveOccurred())
Expand Down Expand Up @@ -787,7 +775,7 @@ var _ = Describe("Export controller", func() {
scp, err := serializeCertParams(cp)
Expect(err).ToNot(HaveOccurred())
testVMExport := createPVCVMExport()
// We call handleVMExportToken to populate the Status field appropiately
populateInitialVMExportStatus(testVMExport)
err = controller.handleVMExportToken(testVMExport)
Expect(err).ToNot(HaveOccurred())
testExportPod := &k8sv1.Pod{
Expand Down Expand Up @@ -859,6 +847,7 @@ var _ = Describe("Export controller", func() {
Expect(secret.GetNamespace()).To(Equal(testNamespace))
return true, secret, nil
})
populateInitialVMExportStatus(testVMExport)
err := controller.handleVMExportToken(testVMExport)
Expect(err).ToNot(HaveOccurred())
Expect(testVMExport.Status.TokenSecretRef).ToNot(BeNil())
Expand Down Expand Up @@ -894,12 +883,46 @@ var _ = Describe("Export controller", func() {
testVMExport := createPVCVMExport()
Expect(testVMExport.Spec.TokenSecretRef).ToNot(BeNil())
expectedName := *testVMExport.Spec.TokenSecretRef
populateInitialVMExportStatus(testVMExport)
err := controller.handleVMExportToken(testVMExport)
Expect(err).ToNot(HaveOccurred())
Expect(testVMExport.Status.TokenSecretRef).ToNot(BeNil())
Expect(*testVMExport.Status.TokenSecretRef).To(Equal(expectedName))
})

It("Should completely clean up VM export, when TTL is reached", func() {
var deleted bool
testVMExport := createPVCVMExport()
ttl := &metav1.Duration{Duration: time.Minute}
testVMExport.Spec.TTLDuration = ttl
// Artificially reach TTL expiration time
testVMExport.SetCreationTimestamp(metav1.NewTime(time.Now().Add(-1 * ttl.Duration)))
pvc := &k8sv1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: testPVCName,
Namespace: testNamespace,
},
Status: k8sv1.PersistentVolumeClaimStatus{
Phase: k8sv1.ClaimBound,
},
}
Expect(controller.PVCInformer.GetStore().Add(pvc)).To(Succeed())

vmExportClient.Fake.PrependReactor("delete", "virtualmachineexports", func(action testing.Action) (handled bool, obj runtime.Object, err error) {
delete, ok := action.(testing.DeleteAction)
Expect(ok).To(BeTrue())
Expect(delete.GetName()).To(Equal(testVMExport.GetName()))
deleted = true
return true, nil, nil
})
retry, err := controller.updateVMExport(testVMExport)
Expect(deleted).To(BeTrue())
// Status update fails (call UPDATE on deleted VMExport), but its fine in real world
// since requeue will back out of the reconcile loop if a deletion timestamp is set
Expect(err).To(HaveOccurred())
Expect(retry).To(BeEquivalentTo(0))
})

DescribeTable("Should ignore invalid VMExports kind/api combinations", func(kind, apigroup string) {
testVMExport := createPVCVMExport()
testVMExport.Spec.Source.Kind = kind
Expand Down Expand Up @@ -1068,8 +1091,9 @@ func writeCertsToDir(dir string) {
func createPVCVMExport() *exportv1.VirtualMachineExport {
return &exportv1.VirtualMachineExport{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: testNamespace,
Name: "test",
Namespace: testNamespace,
CreationTimestamp: metav1.Now(),
},
Spec: exportv1.VirtualMachineExportSpec{
Source: k8sv1.TypedLocalObjectReference{
Expand Down Expand Up @@ -1101,9 +1125,10 @@ func createPVCVMExportWithoutSecret() *exportv1.VirtualMachineExport {
func createSnapshotVMExport() *exportv1.VirtualMachineExport {
return &exportv1.VirtualMachineExport{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: testNamespace,
UID: "11111-22222-33333",
Name: "test",
Namespace: testNamespace,
UID: "11111-22222-33333",
CreationTimestamp: metav1.Now(),
},
Spec: exportv1.VirtualMachineExportSpec{
Source: k8sv1.TypedLocalObjectReference{
Expand All @@ -1119,9 +1144,10 @@ func createSnapshotVMExport() *exportv1.VirtualMachineExport {
func createVMVMExport() *exportv1.VirtualMachineExport {
return &exportv1.VirtualMachineExport{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: testNamespace,
UID: "44444-555555-666666",
Name: "test",
Namespace: testNamespace,
UID: "44444-555555-666666",
CreationTimestamp: metav1.Now(),
},
Spec: exportv1.VirtualMachineExportSpec{
Source: k8sv1.TypedLocalObjectReference{
Expand Down
14 changes: 14 additions & 0 deletions pkg/storage/export/export/pvc-source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ var _ = Describe("PVC source", func() {
Namespace: controller.KubevirtNamespace,
Name: "kv",
},
Spec: virtv1.KubeVirtSpec{
CertificateRotationStrategy: virtv1.KubeVirtCertificateRotateStrategy{
SelfSigned: &virtv1.KubeVirtSelfSignConfiguration{
CA: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 24 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 3 * time.Hour},
},
Server: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 2 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 1 * time.Hour},
},
},
},
},
Status: virtv1.KubeVirtStatus{
Phase: virtv1.KubeVirtPhaseDeployed,
},
Expand Down
14 changes: 14 additions & 0 deletions pkg/storage/export/export/vm-source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,20 @@ var _ = Describe("PVC source", func() {
Namespace: controller.KubevirtNamespace,
Name: "kv",
},
Spec: virtv1.KubeVirtSpec{
CertificateRotationStrategy: virtv1.KubeVirtCertificateRotateStrategy{
SelfSigned: &virtv1.KubeVirtSelfSignConfiguration{
CA: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 24 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 3 * time.Hour},
},
Server: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 2 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 1 * time.Hour},
},
},
},
},
Status: virtv1.KubeVirtStatus{
Phase: virtv1.KubeVirtPhaseDeployed,
},
Expand Down
14 changes: 14 additions & 0 deletions pkg/storage/export/export/vmsnapshot-source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ var _ = Describe("VMSnapshot source", func() {
Namespace: controller.KubevirtNamespace,
Name: "kv",
},
Spec: virtv1.KubeVirtSpec{
CertificateRotationStrategy: virtv1.KubeVirtCertificateRotateStrategy{
SelfSigned: &virtv1.KubeVirtSelfSignConfiguration{
CA: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 24 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 3 * time.Hour},
},
Server: &virtv1.CertConfig{
Duration: &metav1.Duration{Duration: 2 * time.Hour},
RenewBefore: &metav1.Duration{Duration: 1 * time.Hour},
},
},
},
},
Status: virtv1.KubeVirtStatus{
Phase: virtv1.KubeVirtPhaseDeployed,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7805,6 +7805,12 @@ var CRDsValidation map[string]string = map[string]string{
description: TokenSecretRef is the name of the custom-defined secret that
contains the token used by the export server pod
type: string
ttlDuration:
description: ttlDuration limits the lifetime of an export If this field
is set, after this duration has passed from counting from CreationTimestamp,
the export is eligible to be automatically deleted. If this field is omitted,
a reasonable default is applied.
type: string
required:
- source
type: object
Expand Down Expand Up @@ -7950,6 +7956,11 @@ var CRDsValidation map[string]string = map[string]string{
description: TokenSecretRef is the name of the secret that contains the
token used by the export server pod
type: string
ttlExpirationTime:
description: The time at which the VM Export will be completely removed
according to specified TTL Formula is CreationTimestamp + TTL
format: date-time
type: string
type: object
required:
- spec
Expand Down
15 changes: 15 additions & 0 deletions pkg/virtctl/vmexport/vmexport.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const (
INSECURE_FLAG = "--insecure"
KEEP_FLAG = "--keep-vme"
PVC_FLAG = "--pvc"
TTL_FLAG = "--ttl"

// processingWaitInterval is the time interval used to wait for a virtualMachineExport to be ready
processingWaitInterval = 2 * time.Second
Expand Down Expand Up @@ -100,6 +101,7 @@ var (
keepVme bool
shouldCreate bool
volumeName string
ttl string
)

type exportFunc func(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error
Expand All @@ -121,6 +123,7 @@ type VMExportInfo struct {
Namespace string
Name string
ExportSource k8sv1.TypedLocalObjectReference
TTL metav1.Duration
}

type command struct {
Expand Down Expand Up @@ -189,6 +192,7 @@ func NewVirtualMachineExportCommand(clientConfig clientcmd.ClientConfig) *cobra.
cmd.Flags().StringVar(&volumeName, "volume", "", "Specifies the volume to be downloaded.")
cmd.Flags().BoolVar(&insecure, "insecure", false, "When used with the 'download' option, specifies that the http request should be insecure.")
cmd.Flags().BoolVar(&keepVme, "keep-vme", false, "When used with the 'download' option, specifies that the vmexport object should not be deleted after the download finishes.")
cmd.Flags().StringVar(&ttl, "ttl", "", "The time after the export was created that it is eligible to be automatically deleted, defaults to 2 hours by the server side if not specified")
cmd.SetUsageTemplate(templates.UsageTemplate())

return cmd
Expand Down Expand Up @@ -257,6 +261,14 @@ func parseExportArguments(args []string, vmeInfo *VMExportInfo) error {
vmeInfo.Insecure = insecure
vmeInfo.KeepVme = keepVme
vmeInfo.VolumeName = volumeName
vmeInfo.TTL = metav1.Duration{}
if ttl != "" {
duration, err := time.ParseDuration(ttl)
if err != nil {
return err
}
vmeInfo.TTL = metav1.Duration{Duration: duration}
}

return nil
}
Expand Down Expand Up @@ -295,6 +307,9 @@ func CreateVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExport
Source: vmeInfo.ExportSource,
},
}
if vmeInfo.TTL.Duration > 0 {
vmexport.Spec.TTLDuration = &vmeInfo.TTL
}

vmexport, err = client.VirtualMachineExport(vmeInfo.Namespace).Create(context.TODO(), vmexport, metav1.CreateOptions{})
if err != nil {
Expand Down
Loading

0 comments on commit 84cf205

Please sign in to comment.