From 04b776eea3e0b78b981a38ff9d5cf7b145afe3e8 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Mon, 6 Jul 2026 18:20:20 +0300 Subject: [PATCH 1/3] wip Signed-off-by: Daniil Loktev --- api/core/v1alpha2/cvicondition/condition.go | 2 + api/core/v1alpha2/vdcondition/condition.go | 2 + api/core/v1alpha2/vicondition/condition.go | 2 + docs/USER_GUIDE.md | 4 +- docs/USER_GUIDE.ru.md | 4 +- .../controller/cvi/internal/source/upload.go | 33 +++ .../cvi/internal/source/upload_test.go | 201 ++++++++++++++++++ .../pkg/controller/uploader/uploader_pod.go | 15 ++ .../controller/vd/internal/source/upload.go | 19 ++ .../controller/vi/internal/source/upload.go | 48 +++++ 10 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go diff --git a/api/core/v1alpha2/cvicondition/condition.go b/api/core/v1alpha2/cvicondition/condition.go index 3ddd38d68e..3aae5759e4 100644 --- a/api/core/v1alpha2/cvicondition/condition.go +++ b/api/core/v1alpha2/cvicondition/condition.go @@ -65,6 +65,8 @@ const ( // WaitForUserUpload indicates that the `ClusterVirtualImage` is waiting for the user to upload a datasource for the import process to continue. WaitForUserUpload ReadyReason = "WaitForUserUpload" + // WaitForUserUploadTimeout indicates that the user did not start the upload in time, so the import process has failed. + WaitForUserUploadTimeout ReadyReason = "WaitForUserUploadTimeout" // Provisioning indicates that the provisioning process is currently in progress. Provisioning ReadyReason = "Provisioning" // ProvisioningNotStarted indicates that the provisioning process has not started yet. diff --git a/api/core/v1alpha2/vdcondition/condition.go b/api/core/v1alpha2/vdcondition/condition.go index 76606f7a54..29a3d93550 100644 --- a/api/core/v1alpha2/vdcondition/condition.go +++ b/api/core/v1alpha2/vdcondition/condition.go @@ -103,6 +103,8 @@ const ( // WaitForUserUpload indicates that the `VirtualDisk` is waiting for the user to upload a datasource for the import process to continue. WaitForUserUpload ReadyReason = "WaitForUserUpload" + // WaitForUserUploadTimeout indicates that the user did not start the upload in time, so the import process has failed. + WaitForUserUploadTimeout ReadyReason = "WaitForUserUploadTimeout" // Provisioning indicates that the provisioning process is currently in progress. Provisioning ReadyReason = "Provisioning" // ProvisioningNotStarted indicates that the provisioning process has not started yet. diff --git a/api/core/v1alpha2/vicondition/condition.go b/api/core/v1alpha2/vicondition/condition.go index 3062c2a5cb..48749e6c36 100644 --- a/api/core/v1alpha2/vicondition/condition.go +++ b/api/core/v1alpha2/vicondition/condition.go @@ -73,6 +73,8 @@ const ( // WaitForUserUpload indicates that the `VirtualImage` is waiting for the user to upload a datasource for the import process to continue. WaitForUserUpload ReadyReason = "WaitForUserUpload" + // WaitForUserUploadTimeout indicates that the user did not start the upload in time, so the import process has failed. + WaitForUserUploadTimeout ReadyReason = "WaitForUserUploadTimeout" // Provisioning indicates that the provisioning process is currently in progress. Provisioning ReadyReason = "Provisioning" // ProvisioningNotStarted indicates that the provisioning process has not started yet. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index ae584d9d62..954c505c76 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -504,7 +504,7 @@ spec: EOF ``` -Once created, the resource will enter the `WaitForUserUpload` phase, which means it is ready for image upload. +Once created, the resource will enter the `WaitForUserUpload` phase, which means it is ready for image upload. If the upload is not started within 30 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. There are two options available for uploading from a cluster node and from an arbitrary node outside the cluster: @@ -852,7 +852,7 @@ spec: EOF ``` -Once created, the resource enters the `WaitForUserUpload` phase, which means it is ready to accept a disk upload. +Once created, the resource enters the `WaitForUserUpload` phase, which means it is ready to accept a disk upload. If the upload is not started within 30 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. Two upload options are available: from a cluster node and from any node outside the cluster: diff --git a/docs/USER_GUIDE.ru.md b/docs/USER_GUIDE.ru.md index 42df93d945..53acde4b8e 100644 --- a/docs/USER_GUIDE.ru.md +++ b/docs/USER_GUIDE.ru.md @@ -512,7 +512,7 @@ spec: EOF ``` -После создания, ресурс перейдет в фазу `WaitForUserUpload`, а это значит, что он готов для загрузки образа. +После создания, ресурс перейдет в фазу `WaitForUserUpload`, а это значит, что он готов для загрузки образа. Если загрузка не начнётся в течение 30 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. Доступно два варианта загрузки с узла кластера и с произвольного узла за пределами кластера: @@ -860,7 +860,7 @@ spec: EOF ``` -После создания ресурс перейдет в фазу `WaitForUserUpload`, что означает, что он готов для загрузки диска. +После создания ресурс перейдет в фазу `WaitForUserUpload`, что означает, что он готов для загрузки диска. Если загрузка не начнётся в течение 30 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. Доступно два варианта загрузки: с узла кластера и с произвольного узла за пределами кластера: diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go index 6391d7779a..62b4365c12 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go @@ -144,6 +144,22 @@ func (ds UploadDataSource) Sync(ctx context.Context, cvi *v1alpha2.ClusterVirtua return reconcile.Result{}, err } + return reconcile.Result{}, nil + case condition.Reason == cvicondition.WaitForUserUploadTimeout.String(): + log.Debug("Upload wait timed out: clean up") + + cvi.Status.Phase = v1alpha2.ImageFailed + cvi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(cvicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + + _, err = CleanUp(ctx, cvi, ds) + if err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil case object.AnyTerminating(pod, svc, ing): cvi.Status.Phase = v1alpha2.ImagePending @@ -251,6 +267,23 @@ func (ds UploadDataSource) Sync(ctx context.Context, cvi *v1alpha2.ClusterVirtua } log.Info("Provisioning...", "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) + case uploader.IsWaitForUserUploadTimeoutExpired(pod): + log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) + ds.recorder.Event(cvi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) + + cvi.Status.Phase = v1alpha2.ImageFailed + cvi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(cvicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + + _, err = CleanUp(ctx, cvi, ds) + if err != nil { + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil case isUploaderReady: cb. Status(metav1.ConditionFalse). diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go new file mode 100644 index 0000000000..117e3f5dae --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go @@ -0,0 +1,201 @@ +/* +Copyright 2026 Flant JSC + +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, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/controller/uploader" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization-controller/pkg/eventrecord" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +var _ = Describe("Upload DataSource", func() { + var ( + ctx context.Context + cvi *v1alpha2.ClusterVirtualImage + pod *corev1.Pod + svc *corev1.Service + ing *netv1.Ingress + uploaderMock *UploaderMock + statMock *StatMock + recordedEvents []string + ds *UploadDataSource + ) + + newUploadDataSource := func() *UploadDataSource { + scheme := runtime.NewScheme() + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + recorder := &eventrecord.EventRecorderLoggerMock{ + EventFunc: func(_ client.Object, _, reason, _ string) { + recordedEvents = append(recordedEvents, reason) + }, + } + return NewUploadDataSource( + recorder, + statMock, + uploaderMock, + &dvcr.Settings{}, + "controller-ns", + fake.NewClientBuilder().WithScheme(scheme).Build(), + ) + } + + BeforeEach(func() { + ctx = context.Background() + recordedEvents = nil + + cvi = &v1alpha2.ClusterVirtualImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvi", + Generation: 1, + UID: "11111111-1111-1111-1111-111111111111", + }, + Spec: v1alpha2.ClusterVirtualImageSpec{ + DataSource: v1alpha2.ClusterVirtualImageDataSource{ + Type: v1alpha2.DataSourceTypeUpload, + }, + }, + Status: v1alpha2.ClusterVirtualImageStatus{ + ImageUploadURLs: &v1alpha2.ImageUploadURLs{External: "https://upload.example.com"}, + }, + } + + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "uploader", + CreationTimestamp: metav1.NewTime(time.Now()), + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + svc = &corev1.Service{} + ing = &netv1.Ingress{} + + uploaderMock = &UploaderMock{ + GetPodFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { + return pod, nil + }, + GetServiceFunc: func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { + return svc, nil + }, + GetIngressFunc: func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { + return ing, nil + }, + IngressHostDriftedFunc: func(_ *netv1.Ingress) bool { + return false + }, + CleanUpFunc: func(_ context.Context, _ supplements.Generator) (bool, error) { + return true, nil + }, + GetExternalURLFunc: func(_ context.Context, _ *netv1.Ingress) string { + return "https://upload.example.com" + }, + GetInClusterURLFunc: func(_ context.Context, _ *corev1.Service) string { + return "http://10.0.0.1/upload" + }, + } + statMock = &StatMock{ + IsUploaderReadyFunc: func(_ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress, _ *corev1.Secret) (bool, error) { + return true, nil + }, + IsUploadStartedFunc: func(_ types.UID, _ *corev1.Pod) bool { + return false + }, + GetDVCRImageNameFunc: func(_ *corev1.Pod) string { + return "registry.example.com/image:test" + }, + } + }) + + It("waits for the user upload while the timeout has not expired", func() { + ds = newUploadDataSource() + + res, err := ds.Sync(ctx, cvi) + + Expect(err).NotTo(HaveOccurred()) + Expect(res.RequeueAfter).To(Equal(time.Second)) + Expect(cvi.Status.Phase).To(Equal(v1alpha2.ImageWaitForUserUpload)) + ready, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + Expect(ready.Reason).To(Equal(cvicondition.WaitForUserUpload.String())) + Expect(uploaderMock.CleanUpCalls()).To(BeEmpty()) + }) + + It("fails the import when the upload has not started within the timeout", func() { + pod.CreationTimestamp = metav1.NewTime(time.Now().Add(-uploader.WaitForUserUploadTimeout - time.Minute)) + ds = newUploadDataSource() + + res, err := ds.Sync(ctx, cvi) + + Expect(err).NotTo(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + Expect(cvi.Status.Phase).To(Equal(v1alpha2.ImageFailed)) + Expect(cvi.Status.ImageUploadURLs).To(BeNil()) + ready, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(cvicondition.WaitForUserUploadTimeout.String())) + Expect(ready.Message).To(Equal(uploader.WaitForUserUploadTimeoutMessage)) + Expect(uploaderMock.CleanUpCalls()).To(HaveLen(1)) + Expect(recordedEvents).To(ContainElement(v1alpha2.ReasonDataSourceSyncFailed)) + }) + + It("keeps the failed state and does not recreate the uploader after the timeout", func() { + cvi.Status.Phase = v1alpha2.ImageFailed + cvi.Status.Conditions = []metav1.Condition{{ + Type: cvicondition.ReadyType.String(), + Status: metav1.ConditionFalse, + Reason: cvicondition.WaitForUserUploadTimeout.String(), + }} + uploaderMock.GetPodFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Pod, error) { + return nil, nil + } + uploaderMock.GetServiceFunc = func(_ context.Context, _ supplements.Generator) (*corev1.Service, error) { + return nil, nil + } + uploaderMock.GetIngressFunc = func(_ context.Context, _ supplements.Generator) (*netv1.Ingress, error) { + return nil, nil + } + statMock.IsUploaderReadyFunc = func(_ *corev1.Pod, _ *corev1.Service, _ *netv1.Ingress, _ *corev1.Secret) (bool, error) { + return false, nil + } + ds = newUploadDataSource() + + res, err := ds.Sync(ctx, cvi) + + Expect(err).NotTo(HaveOccurred()) + Expect(res.IsZero()).To(BeTrue()) + Expect(cvi.Status.Phase).To(Equal(v1alpha2.ImageFailed)) + ready, _ := conditions.GetCondition(cvicondition.ReadyType, cvi.Status.Conditions) + Expect(ready.Reason).To(Equal(cvicondition.WaitForUserUploadTimeout.String())) + // Start must not be called: StartFunc is nil and would panic. + Expect(uploaderMock.StartCalls()).To(BeEmpty()) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go index 570566ca57..38e5891971 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go @@ -18,6 +18,8 @@ package uploader import ( "context" + "fmt" + "time" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -35,8 +37,21 @@ import ( const ( // destinationAuthVol is the name of the volume containing DVCR docker auth config. destinationAuthVol = "dvcr-secret-vol" + + // WaitForUserUploadTimeout limits how long the uploader waits for the user to start the upload + // before the import process is considered failed. + WaitForUserUploadTimeout = 30 * time.Minute ) +// WaitForUserUploadTimeoutMessage explains the failure when the upload was not started within WaitForUserUploadTimeout. +var WaitForUserUploadTimeoutMessage = fmt.Sprintf("The user upload has not started within %s: the import process has failed. Recreate the resource to start the upload again.", WaitForUserUploadTimeout) + +// IsWaitForUserUploadTimeoutExpired reports whether the uploader pod has been waiting +// for the user to start the upload longer than WaitForUserUploadTimeout. +func IsWaitForUserUploadTimeoutExpired(pod *corev1.Pod) bool { + return pod != nil && time.Since(pod.CreationTimestamp.Time) > WaitForUserUploadTimeout +} + // These constants can't be imported from "images/dvcr-artifact/pkg/uploader/uploader.go" due to conflicts with the CDI version. const ( healthzPort = 8080 diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go index b286ee3fa7..9d30128b00 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -171,6 +171,14 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( return reconcile.Result{}, err } + return CleanUpSupplements(ctx, vd, ds) + case condition.Reason == vdcondition.WaitForUserUploadTimeout.String(): + log.Debug("Upload wait timed out: clean up") + + vd.Status.Phase = v1alpha2.DiskFailed + vd.Status.ImageUploadURLs = nil + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.WaitForUserUploadTimeout, uploader.WaitForUserUploadTimeoutMessage) + return CleanUpSupplements(ctx, vd, ds) case object.AnyTerminating(pod, svc, ing, dv, pvc): log.Info("Waiting for supplements to be terminated") @@ -213,6 +221,17 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( } if !ds.statService.IsUploadStarted(vd.GetUID(), pod) { + if uploader.IsWaitForUserUploadTimeoutExpired(pod) { + log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) + ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) + + vd.Status.Phase = v1alpha2.DiskFailed + vd.Status.ImageUploadURLs = nil + setReadyConditionWithWFFCAccounting(vd, cb, metav1.ConditionFalse, vdcondition.WaitForUserUploadTimeout, uploader.WaitForUserUploadTimeoutMessage) + + return CleanUpSupplements(ctx, vd, ds) + } + if isUploaderReady { log.Info("Waiting for the user upload", "pod.phase", pod.Status.Phase) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go index a8bdc0aba6..e609254d5a 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go @@ -160,6 +160,17 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI return reconcile.Result{}, err } + return CleanUpSupplements(ctx, vi, ds) + case condition.Reason == vicondition.WaitForUserUploadTimeout.String(): + log.Debug("Upload wait timed out: clean up") + + vi.Status.Phase = v1alpha2.ImageFailed + vi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + return CleanUpSupplements(ctx, vi, ds) case object.AnyTerminating(pod, svc, ing, dv, pvc): log.Info("Waiting for supplements to be terminated") @@ -202,6 +213,20 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI } if !ds.statService.IsUploadStarted(vi.GetUID(), pod) { + if uploader.IsWaitForUserUploadTimeoutExpired(pod) { + log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) + ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) + + vi.Status.Phase = v1alpha2.ImageFailed + vi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + + return CleanUpSupplements(ctx, vi, ds) + } + if isUploaderReady { log.Info("Waiting for the user upload", "pod.phase", pod.Status.Phase) @@ -429,6 +454,17 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual return reconcile.Result{}, err } + return CleanUpSupplements(ctx, vi, ds) + case condition.Reason == vicondition.WaitForUserUploadTimeout.String(): + log.Debug("Upload wait timed out: clean up") + + vi.Status.Phase = v1alpha2.ImageFailed + vi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + return CleanUpSupplements(ctx, vi, ds) case object.AnyTerminating(pod, svc, ing): vi.Status.Phase = v1alpha2.ImagePending @@ -510,6 +546,18 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual } log.Info("Provisioning...", "pod.phase", pod.Status.Phase) + case uploader.IsWaitForUserUploadTimeoutExpired(pod): + log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) + ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) + + vi.Status.Phase = v1alpha2.ImageFailed + vi.Status.ImageUploadURLs = nil + cb. + Status(metav1.ConditionFalse). + Reason(vicondition.WaitForUserUploadTimeout). + Message(uploader.WaitForUserUploadTimeoutMessage) + + return CleanUpSupplements(ctx, vi, ds) case isUploaderReady: cb. Status(metav1.ConditionFalse). From 25aaa0a0baba3b612761765aa08d4208d0a4dc99 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Wed, 8 Jul 2026 16:19:53 +0300 Subject: [PATCH 2/3] wip Signed-off-by: Daniil Loktev --- docs/USER_GUIDE.md | 4 ++-- docs/USER_GUIDE.ru.md | 4 ++-- .../pkg/controller/uploader/uploader_pod.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 954c505c76..4e99d61643 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -504,7 +504,7 @@ spec: EOF ``` -Once created, the resource will enter the `WaitForUserUpload` phase, which means it is ready for image upload. If the upload is not started within 30 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. +Once created, the resource will enter the `WaitForUserUpload` phase, which means it is ready for image upload. If the upload is not started within 10 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. There are two options available for uploading from a cluster node and from an arbitrary node outside the cluster: @@ -852,7 +852,7 @@ spec: EOF ``` -Once created, the resource enters the `WaitForUserUpload` phase, which means it is ready to accept a disk upload. If the upload is not started within 30 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. +Once created, the resource enters the `WaitForUserUpload` phase, which means it is ready to accept a disk upload. If the upload is not started within 10 minutes, the resource will transition to the `Failed` phase; to try again, recreate the resource. Two upload options are available: from a cluster node and from any node outside the cluster: diff --git a/docs/USER_GUIDE.ru.md b/docs/USER_GUIDE.ru.md index 53acde4b8e..8d4db7caa9 100644 --- a/docs/USER_GUIDE.ru.md +++ b/docs/USER_GUIDE.ru.md @@ -512,7 +512,7 @@ spec: EOF ``` -После создания, ресурс перейдет в фазу `WaitForUserUpload`, а это значит, что он готов для загрузки образа. Если загрузка не начнётся в течение 30 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. +После создания, ресурс перейдет в фазу `WaitForUserUpload`, а это значит, что он готов для загрузки образа. Если загрузка не начнётся в течение 10 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. Доступно два варианта загрузки с узла кластера и с произвольного узла за пределами кластера: @@ -860,7 +860,7 @@ spec: EOF ``` -После создания ресурс перейдет в фазу `WaitForUserUpload`, что означает, что он готов для загрузки диска. Если загрузка не начнётся в течение 30 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. +После создания ресурс перейдет в фазу `WaitForUserUpload`, что означает, что он готов для загрузки диска. Если загрузка не начнётся в течение 10 минут, ресурс перейдет в фазу `Failed`; для повторной попытки пересоздайте ресурс. Доступно два варианта загрузки: с узла кластера и с произвольного узла за пределами кластера: diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go index 38e5891971..b51ecd129c 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go @@ -40,7 +40,7 @@ const ( // WaitForUserUploadTimeout limits how long the uploader waits for the user to start the upload // before the import process is considered failed. - WaitForUserUploadTimeout = 30 * time.Minute + WaitForUserUploadTimeout = 10 * time.Minute ) // WaitForUserUploadTimeoutMessage explains the failure when the upload was not started within WaitForUserUploadTimeout. From 49773dc397af49d6738d3382d9fe08b17a179762 Mon Sep 17 00:00:00 2001 From: Daniil Loktev Date: Wed, 8 Jul 2026 18:34:06 +0300 Subject: [PATCH 3/3] wip Signed-off-by: Daniil Loktev --- .../pkg/controller/cvi/internal/source/upload.go | 2 +- .../pkg/controller/cvi/internal/source/upload_test.go | 7 ++++++- .../pkg/controller/uploader/uploader_pod.go | 6 ++---- .../pkg/controller/vd/internal/source/upload.go | 2 +- .../pkg/controller/vi/internal/source/upload.go | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go index 62b4365c12..d59333f0e6 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload.go @@ -267,7 +267,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, cvi *v1alpha2.ClusterVirtua } log.Info("Provisioning...", "progress", cvi.Status.Progress, "pod.phase", pod.Status.Phase) - case uploader.IsWaitForUserUploadTimeoutExpired(pod): + case condition.Reason == cvicondition.WaitForUserUpload.String() && uploader.IsWaitForUserUploadTimeoutExpired(condition.LastTransitionTime): log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) ds.recorder.Event(cvi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go index 117e3f5dae..aa8c20cf22 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/source/upload_test.go @@ -150,7 +150,12 @@ var _ = Describe("Upload DataSource", func() { }) It("fails the import when the upload has not started within the timeout", func() { - pod.CreationTimestamp = metav1.NewTime(time.Now().Add(-uploader.WaitForUserUploadTimeout - time.Minute)) + cvi.Status.Conditions = []metav1.Condition{{ + Type: cvicondition.ReadyType.String(), + Status: metav1.ConditionFalse, + Reason: cvicondition.WaitForUserUpload.String(), + LastTransitionTime: metav1.NewTime(time.Now().Add(-uploader.WaitForUserUploadTimeout - time.Minute)), + }} ds = newUploadDataSource() res, err := ds.Sync(ctx, cvi) diff --git a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go index b51ecd129c..335fc1dfaa 100644 --- a/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go +++ b/images/virtualization-artifact/pkg/controller/uploader/uploader_pod.go @@ -46,10 +46,8 @@ const ( // WaitForUserUploadTimeoutMessage explains the failure when the upload was not started within WaitForUserUploadTimeout. var WaitForUserUploadTimeoutMessage = fmt.Sprintf("The user upload has not started within %s: the import process has failed. Recreate the resource to start the upload again.", WaitForUserUploadTimeout) -// IsWaitForUserUploadTimeoutExpired reports whether the uploader pod has been waiting -// for the user to start the upload longer than WaitForUserUploadTimeout. -func IsWaitForUserUploadTimeoutExpired(pod *corev1.Pod) bool { - return pod != nil && time.Since(pod.CreationTimestamp.Time) > WaitForUserUploadTimeout +func IsWaitForUserUploadTimeoutExpired(waitStartedAt metav1.Time) bool { + return !waitStartedAt.IsZero() && time.Since(waitStartedAt.Time) > WaitForUserUploadTimeout } // These constants can't be imported from "images/dvcr-artifact/pkg/uploader/uploader.go" due to conflicts with the CDI version. diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go index 9d30128b00..9ef9752d27 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -221,7 +221,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *v1alpha2.VirtualDisk) ( } if !ds.statService.IsUploadStarted(vd.GetUID(), pod) { - if uploader.IsWaitForUserUploadTimeoutExpired(pod) { + if condition.Reason == vdcondition.WaitForUserUpload.String() && uploader.IsWaitForUserUploadTimeoutExpired(condition.LastTransitionTime) { log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) ds.recorder.Event(vd, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go index e609254d5a..f88c093e3a 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/upload.go @@ -213,7 +213,7 @@ func (ds UploadDataSource) StoreToPVC(ctx context.Context, vi *v1alpha2.VirtualI } if !ds.statService.IsUploadStarted(vi.GetUID(), pod) { - if uploader.IsWaitForUserUploadTimeoutExpired(pod) { + if condition.Reason == vicondition.WaitForUserUpload.String() && uploader.IsWaitForUserUploadTimeoutExpired(condition.LastTransitionTime) { log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage) @@ -546,7 +546,7 @@ func (ds UploadDataSource) StoreToDVCR(ctx context.Context, vi *v1alpha2.Virtual } log.Info("Provisioning...", "pod.phase", pod.Status.Phase) - case uploader.IsWaitForUserUploadTimeoutExpired(pod): + case condition.Reason == vicondition.WaitForUserUpload.String() && uploader.IsWaitForUserUploadTimeoutExpired(condition.LastTransitionTime): log.Info("Upload has not started in time: the import process has failed", "pod.name", pod.Name) ds.recorder.Event(vi, corev1.EventTypeWarning, v1alpha2.ReasonDataSourceSyncFailed, uploader.WaitForUserUploadTimeoutMessage)