diff --git a/api/core/v1alpha2/cluster_virtual_image.go b/api/core/v1alpha2/cluster_virtual_image.go index 12adb60c2b..739a85974c 100644 --- a/api/core/v1alpha2/cluster_virtual_image.go +++ b/api/core/v1alpha2/cluster_virtual_image.go @@ -133,7 +133,8 @@ type ClusterVirtualImageStatus struct { // * `Ready`: The resource has been created and is ready to use. // * `Failed`: There was an error when creating the resource. // * `Terminating`: The resource is being deleted. - // +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,Ready,Failed,Terminating} + // * `ImageLost`: The image is missing in DVCR. The resource cannot be used. + // +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,Ready,Failed,Terminating,ImageLost} Phase ImagePhase `json:"phase,omitempty"` // Progress of copying an image from the source to DVCR. Appears only during the `Provisioning' phase. Progress string `json:"progress,omitempty"` diff --git a/api/core/v1alpha2/cvicondition/condition.go b/api/core/v1alpha2/cvicondition/condition.go index 1d55753286..3ddd38d68e 100644 --- a/api/core/v1alpha2/cvicondition/condition.go +++ b/api/core/v1alpha2/cvicondition/condition.go @@ -73,4 +73,6 @@ const ( ProvisioningFailed ReadyReason = "ProvisioningFailed" // Ready indicates that the import process is complete and the `ClusterVirtualImage` is ready for use. Ready ReadyReason = "Ready" + // ImageLost indicates that the image in DVCR has been lost and the `ClusterVirtualImage` can no longer be used. + ImageLost ReadyReason = "ImageLost" ) diff --git a/api/core/v1alpha2/image_status.go b/api/core/v1alpha2/image_status.go index ee764f73fe..b72e6f27e0 100644 --- a/api/core/v1alpha2/image_status.go +++ b/api/core/v1alpha2/image_status.go @@ -25,7 +25,8 @@ const ( ImageReady ImagePhase = "Ready" ImageFailed ImagePhase = "Failed" ImageTerminating ImagePhase = "Terminating" - ImageLost ImagePhase = "PVCLost" + ImageLost ImagePhase = "ImageLost" + ImagePVCLost ImagePhase = "PVCLost" ) type ImageUploadURLs struct { diff --git a/api/core/v1alpha2/vicondition/condition.go b/api/core/v1alpha2/vicondition/condition.go index 86dbd2d0af..3062c2a5cb 100644 --- a/api/core/v1alpha2/vicondition/condition.go +++ b/api/core/v1alpha2/vicondition/condition.go @@ -90,7 +90,11 @@ const ( // DatasourceNotReady indicates that the datasource is not ready, which prevents the import process from starting. DatasourceNotReady ReadyReason = "DatasourceNotReady" - // Lost indicates that the underlying PersistentVolumeClaim has been lost and the `VirtualImage` can no longer be used. + // ImageLost indicates that the image in DVCR has been lost and the `VirtualImage` can no longer be used. + ImageLost ReadyReason = "ImageLost" + // PVCLost indicates that the underlying PersistentVolumeClaim has been lost and the `VirtualImage` can no longer be used. + PVCLost ReadyReason = "PVCLost" + // Lost is deprecated, use PVCLost instead. Lost ReadyReason = "PVCLost" // StorageClassReady indicates that the chosen StorageClass exists. diff --git a/api/core/v1alpha2/virtual_image.go b/api/core/v1alpha2/virtual_image.go index 1cfbf4d2e7..0e14f0016d 100644 --- a/api/core/v1alpha2/virtual_image.go +++ b/api/core/v1alpha2/virtual_image.go @@ -89,8 +89,9 @@ type VirtualImageStatus struct { // * `Ready`: The resource has been created and is ready to use. // * `Failed`: There was an error when creating the resource. // * `Terminating`: The resource is being deleted. + // * `ImageLost`: The image is missing in DVCR. The resource cannot be used. // * `PVCLost`: The child PVC of the resource is missing. The resource cannot be used. - // +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,Ready,Failed,Terminating,PVCLost} + // +kubebuilder:validation:Enum:={Pending,Provisioning,WaitForUserUpload,Ready,Failed,Terminating,ImageLost,PVCLost} Phase ImagePhase `json:"phase,omitempty"` // Progress of copying an image from a source to DVCR. Progress string `json:"progress,omitempty"` diff --git a/crds/clustervirtualimages.yaml b/crds/clustervirtualimages.yaml index 93bc215d84..74b4da92ed 100644 --- a/crds/clustervirtualimages.yaml +++ b/crds/clustervirtualimages.yaml @@ -357,6 +357,7 @@ spec: * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted. + * `ImageLost`: The image is missing in DVCR. The resource cannot be used. enum: - Pending - Provisioning @@ -364,6 +365,7 @@ spec: - Ready - Failed - Terminating + - ImageLost type: string progress: description: diff --git a/crds/doc-ru-clustervirtualimages.yaml b/crds/doc-ru-clustervirtualimages.yaml index 02c0dc1d02..77d54c11cf 100644 --- a/crds/doc-ru-clustervirtualimages.yaml +++ b/crds/doc-ru-clustervirtualimages.yaml @@ -145,6 +145,7 @@ spec: * `WaitForUserUpload` - ожидание загрузки образа пользователем. Путь для загрузки образа указывается в `.status.uploadCommand`; * `Ready` - ресурс создан и готов к использованию; * `Failed` - при создании ресурса произошла ошибка; + * `ImageLost` — образ отсутствует в DVCR. Ресурс не может быть использован; * `Terminating` - ресурс находится в процессе удаления. progress: description: | diff --git a/crds/doc-ru-virtualimages.yaml b/crds/doc-ru-virtualimages.yaml index 90a417c362..28d9557b87 100644 --- a/crds/doc-ru-virtualimages.yaml +++ b/crds/doc-ru-virtualimages.yaml @@ -152,6 +152,7 @@ spec: * `WaitForUserUpload` — ожидание загрузки образа пользователем. Путь для загрузки образа указывается в `.status.uploadCommand`; * `Ready` — ресурс создан и готов к использованию; * `Failed` — при создании ресурса возникла ошибка. За подробностями обратитесь к полям `.status.failureReason` и `.status.failureMessage`; + * `ImageLost` — образ отсутствует в DVCR. Ресурс не может быть использован; * `PVCLost` — дочерний PVC ресурса отсутствует. Ресурс не может быть использован; * `Terminating` - ресурс находится в процессе удаления. progress: diff --git a/crds/virtualimages.yaml b/crds/virtualimages.yaml index 700a9418b2..86cb76a1df 100644 --- a/crds/virtualimages.yaml +++ b/crds/virtualimages.yaml @@ -362,6 +362,7 @@ spec: * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted. + * `ImageLost`: The image is missing in DVCR. The resource cannot be used. * `PVCLost`: The child PVC of the resource is missing. The resource cannot be used. enum: - Pending @@ -370,6 +371,7 @@ spec: - Ready - Failed - Terminating + - ImageLost - PVCLost type: string progress: diff --git a/images/hooks/go.mod b/images/hooks/go.mod index 027e9646b4..93a3354974 100644 --- a/images/hooks/go.mod +++ b/images/hooks/go.mod @@ -46,7 +46,7 @@ require ( github.com/google/certificate-transparency-go v1.1.7 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.17.0 // indirect + github.com/google/go-containerregistry v0.20.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/images/hooks/go.sum b/images/hooks/go.sum index d39e4d9de0..6bd475cc90 100644 --- a/images/hooks/go.sum +++ b/images/hooks/go.sum @@ -135,8 +135,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk= -github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg= +github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index f4b0f9d26a..42c0ce6cfc 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -314,7 +314,7 @@ func main() { } cviLogger := logger.NewControllerLogger(cvi.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = cvi.NewController(ctx, mgr, cviLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, controllerNamespace); err != nil { + if _, err = cvi.NewController(ctx, mgr, cviLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.Requirements, dvcrSettings, controllerNamespace, gcSettings.ImageMonitor.Schedule); err != nil { log.Error(err.Error()) os.Exit(1) } @@ -326,7 +326,7 @@ func main() { } viLogger := logger.NewControllerLogger(vi.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings); err != nil { + if _, err = vi.NewController(ctx, mgr, viLogger, importSettings.ImporterImage, importSettings.UploaderImage, importSettings.BounderImage, importSettings.Requirements, dvcrSettings, viStorageClassSettings, gcSettings.ImageMonitor.Schedule); err != nil { log.Error(err.Error()) os.Exit(1) } diff --git a/images/virtualization-artifact/go.mod b/images/virtualization-artifact/go.mod index 7acf9e46f5..fe888596ce 100644 --- a/images/virtualization-artifact/go.mod +++ b/images/virtualization-artifact/go.mod @@ -11,9 +11,10 @@ require ( github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250226105106-176cd3afcdd5 github.com/deckhouse/virtualization/api v0.0.0-00010101000000-000000000000 github.com/distribution/reference v0.5.0 - github.com/docker/cli v23.0.5+incompatible + github.com/docker/cli v24.0.0+incompatible github.com/fsnotify/fsnotify v1.7.0 github.com/go-logr/logr v1.4.2 + github.com/google/go-containerregistry v0.20.0 github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 @@ -42,11 +43,17 @@ require ( cel.dev/expr v0.19.1 // indirect github.com/DataDog/gostackparse v0.7.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/matryer/moq v0.5.3 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect diff --git a/images/virtualization-artifact/go.sum b/images/virtualization-artifact/go.sum index e1a15aaec0..876f2487aa 100644 --- a/images/virtualization-artifact/go.sum +++ b/images/virtualization-artifact/go.sum @@ -5,6 +5,7 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= @@ -33,10 +34,13 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -51,8 +55,10 @@ github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250226105106-176cd3afcdd5 h1:PsN github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250226105106-176cd3afcdd5/go.mod h1:Mk5HRzkc5pIcDIZ2JJ6DPuuqnwhXVkb3you8M8Mg+4w= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE= -github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= @@ -150,6 +156,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg= +github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -210,6 +218,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/moq v0.5.3 h1:4femQCFmBUwFPYs8VfM5ID7AI67/DTEDRBbTtSWy7GU= github.com/matryer/moq v0.5.3/go.mod h1:8288Qkw7gMZhUP3cIN86GG7g5p9jRuZH8biXLW4RXvQ= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= @@ -281,6 +291,8 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= @@ -305,6 +317,7 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -336,6 +349,9 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= @@ -462,6 +478,7 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 60f515657c..9d38b5074c 100644 --- a/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -1257,7 +1257,7 @@ func schema_virtualization_api_core_v1alpha2_ClusterVirtualImageStatus(ref commo }, "phase": { SchemaProps: spec.SchemaProps{ - Description: "Current status of the ClusterVirtualImage resource: * `Pending`: The resource has been created and is on a waiting queue. * `Provisioning`: The resource is being created: copying, downloading, or building of the image is in progress. * `WaitForUserUpload`: Waiting for the user to upload the image. The endpoint to upload the image is specified in `.status.uploadCommand`. * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted.", + Description: "Current status of the ClusterVirtualImage resource: * `Pending`: The resource has been created and is on a waiting queue. * `Provisioning`: The resource is being created: copying, downloading, or building of the image is in progress. * `WaitForUserUpload`: Waiting for the user to upload the image. The endpoint to upload the image is specified in `.status.uploadCommand`. * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted. * `ImageLost`: The image is missing in DVCR. The resource cannot be used.", Type: []string{"string"}, Format: "", }, @@ -3267,7 +3267,7 @@ func schema_virtualization_api_core_v1alpha2_VirtualImageStatus(ref common.Refer }, "phase": { SchemaProps: spec.SchemaProps{ - Description: "Current status of the ClusterVirtualImage resource: * `Pending`: The resource has been created and is on a waiting queue. * `Provisioning`: The resource is being created: copying, downloading, or building the image. * `WaitForUserUpload`: Waiting for the user to upload the image. The endpoint to upload the image is specified in `.status.uploadCommand`. * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted. * `PVCLost`: The child PVC of the resource is missing. The resource cannot be used.", + Description: "Current status of the ClusterVirtualImage resource: * `Pending`: The resource has been created and is on a waiting queue. * `Provisioning`: The resource is being created: copying, downloading, or building the image. * `WaitForUserUpload`: Waiting for the user to upload the image. The endpoint to upload the image is specified in `.status.uploadCommand`. * `Ready`: The resource has been created and is ready to use. * `Failed`: There was an error when creating the resource. * `Terminating`: The resource is being deleted. * `ImageLost`: The image is missing in DVCR. The resource cannot be used. * `PVCLost`: The child PVC of the resource is missing. The resource cannot be used.", Type: []string{"string"}, Format: "", }, diff --git a/images/virtualization-artifact/pkg/config/load_gc_settings.go b/images/virtualization-artifact/pkg/config/load_gc_settings.go index 7cac99823f..6281c35137 100644 --- a/images/virtualization-artifact/pkg/config/load_gc_settings.go +++ b/images/virtualization-artifact/pkg/config/load_gc_settings.go @@ -29,11 +29,13 @@ const ( GcVmopScheduleVar = "GC_VMOP_SCHEDULE" GcVMIMigrationTTLVar = "GC_VMI_MIGRATION_TTL" GcVMIMigrationScheduleVar = "GC_VMI_MIGRATION_SCHEDULE" + GcImageMonitorScheduleVar = "GC_IMAGE_MONITOR_SCHEDULE" ) type GCSettings struct { VMOP BaseGcSettings VMIMigration BaseGcSettings + ImageMonitor BaseGcSettings } type BaseGcSettings struct { @@ -55,6 +57,12 @@ func LoadGcSettings() (GCSettings, error) { } gcSettings.VMIMigration = base + base, err = GetImageMonitorSettingsFromEnv(GcImageMonitorScheduleVar) + if err != nil { + return gcSettings, err + } + gcSettings.ImageMonitor = base + return gcSettings, nil } @@ -82,3 +90,18 @@ func NewDefaultBaseGcSettings() BaseGcSettings { Schedule: "0 0 * * *", } } + +func GetImageMonitorSettingsFromEnv(envSchedule string) (BaseGcSettings, error) { + base := NewDefaultImageMonitorSettings() + if v, ok := os.LookupEnv(envSchedule); ok { + base.Schedule = v + } + return base, nil +} + +func NewDefaultImageMonitorSettings() BaseGcSettings { + return BaseGcSettings{ + TTL: metav1.Duration{Duration: 0}, + Schedule: "0 * * * *", + } +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go index cd4631c642..ad54bb43cd 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_controller.go @@ -58,6 +58,7 @@ func NewController( requirements corev1.ResourceRequirements, dvcr *dvcr.Settings, ns string, + imageMonitorSchedule string, ) (controller.Controller, error) { stat := service.NewStatService(log) protection := service.NewProtectionService(mgr.GetClient(), v1alpha2.FinalizerCVIProtection) @@ -74,8 +75,11 @@ func NewController( reconciler := NewReconciler( mgr.GetClient(), + imageMonitorSchedule, + log, internal.NewDatasourceReadyHandler(sources), internal.NewLifeCycleHandler(sources, mgr.GetClient()), + internal.NewImagePresenceHandler(mgr.GetClient(), dvcr), internal.NewDeletionHandler(sources), internal.NewAttacheeHandler(mgr.GetClient()), ) diff --git a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go index a568441588..ac198ac40d 100644 --- a/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/cvi/cvi_reconciler.go @@ -30,7 +30,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/controller/cvi/internal" "github.com/deckhouse/virtualization-controller/pkg/controller/cvi/internal/watcher" + "github.com/deckhouse/virtualization-controller/pkg/controller/gc" "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -45,14 +48,18 @@ type Handler interface { } type Reconciler struct { - handlers []Handler - client client.Client + handlers []Handler + client client.Client + imageMonitorSchedule string + log *log.Logger } -func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { +func NewReconciler(client client.Client, imageMonitorSchedule string, log *log.Logger, handlers ...Handler) *Reconciler { return &Reconciler{ - client: client, - handlers: handlers, + client: client, + imageMonitorSchedule: imageMonitorSchedule, + log: log, + handlers: handlers, } } @@ -121,6 +128,18 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr return fmt.Errorf("error setting watch on CVIs: %w", err) } + if r.imageMonitorSchedule != "" { + enqueuer := internal.NewPeriodicEnqueuer(mgr.GetClient()) + cronSource, err := gc.NewCronSource(r.imageMonitorSchedule, enqueuer, r.log.With("source", "image-monitor")) + if err != nil { + return fmt.Errorf("failed to create cron source for image monitoring: %w", err) + } + + if err := ctr.Watch(cronSource); err != nil { + return fmt.Errorf("failed to setup periodic image check: %w", err) + } + } + return nil } diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/image_presence.go b/images/virtualization-artifact/pkg/controller/cvi/internal/image_presence.go new file mode 100644 index 0000000000..21fe811831 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/image_presence.go @@ -0,0 +1,108 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/cvicondition" +) + +type ImagePresenceHandler struct { + client client.Client + dvcrSettings *dvcr.Settings +} + +func NewImagePresenceHandler(client client.Client, dvcrSettings *dvcr.Settings) *ImagePresenceHandler { + return &ImagePresenceHandler{ + client: client, + dvcrSettings: dvcrSettings, + } +} + +func (h *ImagePresenceHandler) Handle(ctx context.Context, cvi *v1alpha2.ClusterVirtualImage) (reconcile.Result, error) { + if cvi.Status.Phase != v1alpha2.ImageReady { + return reconcile.Result{}, nil + } + + registryURL := cvi.Status.Target.RegistryURL + if registryURL == "" { + return reconcile.Result{}, nil + } + + exists, err := h.checkImageExists(ctx, registryURL) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to check image existence in DVCR: %w", err) + } + + if !exists { + cvi.Status.Phase = v1alpha2.ImageLost + + cb := conditions.NewConditionBuilder(cvicondition.ReadyType). + Generation(cvi.Generation). + Status(metav1.ConditionFalse). + Reason(cvicondition.ImageLost). + Message(fmt.Sprintf("Image %q not found in DVCR.", registryURL)) + + conditions.SetCondition(cb, &cvi.Status.Conditions) + } + + return reconcile.Result{}, nil +} + +func (h *ImagePresenceHandler) checkImageExists(ctx context.Context, registryURL string) (bool, error) { + username, password, err := h.getAuthCredentials(ctx) + if err != nil { + return false, err + } + + insecure := h.dvcrSettings.InsecureTLS == "true" + checker := dvcr.NewImageChecker(username, password, insecure) + + return checker.CheckImageExists(ctx, registryURL) +} + +func (h *ImagePresenceHandler) getAuthCredentials(ctx context.Context) (string, string, error) { + if h.dvcrSettings.AuthSecret == "" { + return "", "", nil + } + + secret := &corev1.Secret{} + err := h.client.Get(ctx, types.NamespacedName{ + Name: h.dvcrSettings.AuthSecret, + Namespace: h.dvcrSettings.AuthSecretNamespace, + }, secret) + if err != nil { + return "", "", fmt.Errorf("failed to get auth secret %s/%s: %w", + h.dvcrSettings.AuthSecretNamespace, h.dvcrSettings.AuthSecret, err) + } + + username := string(secret.Data["username"]) + password := string(secret.Data["password"]) + + return username, password, nil +} diff --git a/images/virtualization-artifact/pkg/controller/cvi/internal/periodic_enqueuer.go b/images/virtualization-artifact/pkg/controller/cvi/internal/periodic_enqueuer.go new file mode 100644 index 0000000000..8748383a5a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/cvi/internal/periodic_enqueuer.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type PeriodicEnqueuer struct { + client client.Client +} + +func NewPeriodicEnqueuer(client client.Client) *PeriodicEnqueuer { + return &PeriodicEnqueuer{ + client: client, + } +} + +func (e *PeriodicEnqueuer) ListForDelete(ctx context.Context, now time.Time) ([]client.Object, error) { + cviList := &v1alpha2.ClusterVirtualImageList{} + if err := e.client.List(ctx, cviList); err != nil { + return nil, err + } + + objs := make([]client.Object, 0) + + for i := range cviList.Items { + cvi := &cviList.Items[i] + + if cvi.Status.Phase != v1alpha2.ImageReady { + continue + } + + objs = append(objs, cvi) + } + + return objs, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/image_presence.go b/images/virtualization-artifact/pkg/controller/vi/internal/image_presence.go new file mode 100644 index 0000000000..b485f6bda3 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/image_presence.go @@ -0,0 +1,116 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/dvcr" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vicondition" +) + +type ImagePresenceHandler struct { + client client.Client + dvcrSettings *dvcr.Settings +} + +func NewImagePresenceHandler(client client.Client, dvcrSettings *dvcr.Settings) *ImagePresenceHandler { + return &ImagePresenceHandler{ + client: client, + dvcrSettings: dvcrSettings, + } +} + +func (h *ImagePresenceHandler) Name() string { + return "ImagePresenceHandler" +} + +func (h *ImagePresenceHandler) Handle(ctx context.Context, vi *v1alpha2.VirtualImage) (reconcile.Result, error) { + if vi.Status.Phase != v1alpha2.ImageReady { + return reconcile.Result{}, nil + } + + if vi.Spec.Storage != v1alpha2.StorageContainerRegistry { + return reconcile.Result{}, nil + } + + registryURL := vi.Status.Target.RegistryURL + if registryURL == "" { + return reconcile.Result{}, nil + } + + exists, err := h.checkImageExists(ctx, registryURL) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to check image existence in DVCR: %w", err) + } + + if !exists { + vi.Status.Phase = v1alpha2.ImageLost + + cb := conditions.NewConditionBuilder(vicondition.ReadyType). + Generation(vi.Generation). + Status(metav1.ConditionFalse). + Reason(vicondition.ImageLost). + Message(fmt.Sprintf("Image %q not found in DVCR.", registryURL)) + + conditions.SetCondition(cb, &vi.Status.Conditions) + } + + return reconcile.Result{}, nil +} + +func (h *ImagePresenceHandler) checkImageExists(ctx context.Context, registryURL string) (bool, error) { + username, password, err := h.getAuthCredentials(ctx) + if err != nil { + return false, err + } + + insecure := h.dvcrSettings.InsecureTLS == "true" + checker := dvcr.NewImageChecker(username, password, insecure) + + return checker.CheckImageExists(ctx, registryURL) +} + +func (h *ImagePresenceHandler) getAuthCredentials(ctx context.Context) (string, string, error) { + if h.dvcrSettings.AuthSecret == "" { + return "", "", nil + } + + secret := &corev1.Secret{} + err := h.client.Get(ctx, types.NamespacedName{ + Name: h.dvcrSettings.AuthSecret, + Namespace: h.dvcrSettings.AuthSecretNamespace, + }, secret) + if err != nil { + return "", "", fmt.Errorf("failed to get auth secret %s/%s: %w", + h.dvcrSettings.AuthSecretNamespace, h.dvcrSettings.AuthSecret, err) + } + + username := string(secret.Data["username"]) + password := string(secret.Data["password"]) + + return username, password, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/periodic_enqueuer.go b/images/virtualization-artifact/pkg/controller/vi/internal/periodic_enqueuer.go new file mode 100644 index 0000000000..073fd3ece4 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vi/internal/periodic_enqueuer.go @@ -0,0 +1,61 @@ +/* +Copyright 2025 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 internal + +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type PeriodicEnqueuer struct { + client client.Client +} + +func NewPeriodicEnqueuer(client client.Client) *PeriodicEnqueuer { + return &PeriodicEnqueuer{ + client: client, + } +} + +func (e *PeriodicEnqueuer) ListForDelete(ctx context.Context, now time.Time) ([]client.Object, error) { + viList := &v1alpha2.VirtualImageList{} + if err := e.client.List(ctx, viList); err != nil { + return nil, err + } + + objs := make([]client.Object, 0) + + for i := range viList.Items { + vi := &viList.Items[i] + + if vi.Status.Phase != v1alpha2.ImageReady { + continue + } + + if vi.Spec.Storage != v1alpha2.StorageContainerRegistry { + continue + } + + objs = append(objs, vi) + } + + return objs, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go index a28aa9aabd..0183374be7 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/object_ref_vdsnapshot_pvc_test.go @@ -231,8 +231,8 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() Expect(err).ToNot(HaveOccurred()) Expect(res.IsZero()).To(BeTrue()) - ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) - Expect(vi.Status.Phase).To(Equal(v1alpha2.ImageLost)) + ExpectCondition(vi, metav1.ConditionFalse, vicondition.PVCLost, true) + Expect(vi.Status.Phase).To(Equal(v1alpha2.ImagePVCLost)) Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) }) @@ -247,8 +247,8 @@ var _ = Describe("ObjectRef VirtualImageSnapshot PersistentVolumeClaim", func() Expect(err).ToNot(HaveOccurred()) Expect(res.IsZero()).To(BeTrue()) - ExpectCondition(vi, metav1.ConditionFalse, vicondition.Lost, true) - Expect(vi.Status.Phase).To(Equal(v1alpha2.ImageLost)) + ExpectCondition(vi, metav1.ConditionFalse, vicondition.PVCLost, true) + Expect(vi.Status.Phase).To(Equal(v1alpha2.ImagePVCLost)) Expect(vi.Status.Target.PersistentVolumeClaim).NotTo(BeEmpty()) }) }) diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go index 8075929648..303e8cd50c 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/sources.go @@ -117,10 +117,10 @@ func setPhaseConditionForFinishedImage( ) { switch { case pvc == nil: - *phase = v1alpha2.ImageLost + *phase = v1alpha2.ImagePVCLost cb. Status(metav1.ConditionFalse). - Reason(vicondition.Lost). + Reason(vicondition.PVCLost). Message(fmt.Sprintf("PVC %s not found.", supgen.PersistentVolumeClaim().String())) default: *phase = v1alpha2.ImageReady diff --git a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go index b66ade793b..504a81ca70 100644 --- a/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go +++ b/images/virtualization-artifact/pkg/controller/vi/internal/source/step/ready_pvc_step.go @@ -69,10 +69,10 @@ func (s ReadyPersistentVolumeClaimStep) Take(ctx context.Context, vi *v1alpha2.V if ready.Status == metav1.ConditionTrue { log.Debug("PVC is lost", ".status.target.pvc", vi.Status.Target.PersistentVolumeClaim) - vi.Status.Phase = v1alpha2.ImageLost + vi.Status.Phase = v1alpha2.ImagePVCLost s.cb. Status(metav1.ConditionFalse). - Reason(vicondition.Lost). + Reason(vicondition.PVCLost). Message(fmt.Sprintf("PersistentVolumeClaim %q not found.", vi.Status.Target.PersistentVolumeClaim)) return &reconcile.Result{}, nil } @@ -86,10 +86,10 @@ func (s ReadyPersistentVolumeClaimStep) Take(ctx context.Context, vi *v1alpha2.V case corev1.ClaimLost: log.Warn("Image is Lost: underlying PVC is Lost") - vi.Status.Phase = v1alpha2.ImageLost + vi.Status.Phase = v1alpha2.ImagePVCLost s.cb. Status(metav1.ConditionFalse). - Reason(vdcondition.Lost). + Reason(vicondition.PVCLost). Message(fmt.Sprintf("PersistentVolume %q not found.", s.pvc.Spec.VolumeName)) return &reconcile.Result{}, nil diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go index 8a37796a9f..a53032f12f 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_controller.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_controller.go @@ -61,6 +61,7 @@ func NewController( requirements corev1.ResourceRequirements, dvcr *dvcr.Settings, storageClassSettings config.VirtualImageStorageClassSettings, + imageMonitorSchedule string, ) (controller.Controller, error) { stat := service.NewStatService(log) protection := service.NewProtectionService(mgr.GetClient(), v1alpha2.FinalizerVIProtection) @@ -79,9 +80,12 @@ func NewController( reconciler := NewReconciler( mgr.GetClient(), + imageMonitorSchedule, + log, internal.NewStorageClassReadyHandler(recorder, scService), internal.NewDatasourceReadyHandler(sources), internal.NewLifeCycleHandler(recorder, sources, mgr.GetClient()), + internal.NewImagePresenceHandler(mgr.GetClient(), dvcr), internal.NewDeletionHandler(sources), internal.NewAttacheeHandler(mgr.GetClient()), ) diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go index ed4ae610e4..01613457fe 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_reconciler.go @@ -30,7 +30,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/virtualization-controller/pkg/controller/gc" "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" + "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal" "github.com/deckhouse/virtualization-controller/pkg/controller/vi/internal/watcher" "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -46,14 +49,18 @@ type Handler interface { } type Reconciler struct { - handlers []Handler - client client.Client + handlers []Handler + client client.Client + imageMonitorSchedule string + log *log.Logger } -func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { +func NewReconciler(client client.Client, imageMonitorSchedule string, log *log.Logger, handlers ...Handler) *Reconciler { return &Reconciler{ - client: client, - handlers: handlers, + client: client, + imageMonitorSchedule: imageMonitorSchedule, + log: log, + handlers: handlers, } } @@ -124,6 +131,18 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr } } + if r.imageMonitorSchedule != "" { + enqueuer := internal.NewPeriodicEnqueuer(mgr.GetClient()) + cronSource, err := gc.NewCronSource(r.imageMonitorSchedule, enqueuer, r.log.With("source", "image-monitor")) + if err != nil { + return fmt.Errorf("failed to create cron source for image monitoring: %w", err) + } + + if err := ctr.Watch(cronSource); err != nil { + return fmt.Errorf("failed to setup periodic image check: %w", err) + } + } + return nil } diff --git a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go index 63702c1de8..09dab5ff9c 100644 --- a/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go +++ b/images/virtualization-artifact/pkg/controller/vi/vi_webhook.go @@ -131,7 +131,7 @@ func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.O ready, _ := conditions.GetCondition(vicondition.ReadyType, newVI.Status.Conditions) switch { - case ready.Status == metav1.ConditionTrue, newVI.Status.Phase == v1alpha2.ImageReady, newVI.Status.Phase == v1alpha2.ImageLost: + case ready.Status == metav1.ConditionTrue, newVI.Status.Phase == v1alpha2.ImageReady, newVI.Status.Phase == v1alpha2.ImageLost, newVI.Status.Phase == v1alpha2.ImagePVCLost: if !reflect.DeepEqual(oldVI.Spec.DataSource, newVI.Spec.DataSource) { return nil, errors.New("data source cannot be changed if the VirtualImage has already been provisioned") } diff --git a/images/virtualization-artifact/pkg/dvcr/checker.go b/images/virtualization-artifact/pkg/dvcr/checker.go new file mode 100644 index 0000000000..acfd156fa2 --- /dev/null +++ b/images/virtualization-artifact/pkg/dvcr/checker.go @@ -0,0 +1,119 @@ +/* +Copyright 2025 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 dvcr + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// ImageChecker provides functionality to check if images exist in a registry. +type ImageChecker interface { + CheckImageExists(ctx context.Context, imageURL string) (bool, error) +} + +// DefaultImageChecker implements ImageChecker using go-containerregistry. +type DefaultImageChecker struct { + username string + password string + insecure bool +} + +// NewImageChecker creates a new ImageChecker with the provided authentication credentials. +func NewImageChecker(username, password string, insecure bool) ImageChecker { + return &DefaultImageChecker{ + username: username, + password: password, + insecure: insecure, + } +} + +// CheckImageExists checks if an image exists in the registry by performing a lightweight HEAD request. +// It returns true if the image exists, false if it doesn't exist, and an error for other failures. +func (c *DefaultImageChecker) CheckImageExists(ctx context.Context, imageURL string) (bool, error) { + if imageURL == "" { + return false, fmt.Errorf("image URL is empty") + } + + ref, err := name.ParseReference(imageURL, c.nameOptions()...) + if err != nil { + return false, fmt.Errorf("failed to parse image reference %q: %w", imageURL, err) + } + + _, err = remote.Head(ref, c.remoteOptions(ctx)...) + if err != nil { + if isNotFoundError(err) { + return false, nil + } + return false, fmt.Errorf("failed to check image existence for %q: %w", imageURL, err) + } + + return true, nil +} + +// nameOptions returns the name options for parsing image references. +func (c *DefaultImageChecker) nameOptions() []name.Option { + opts := []name.Option{} + if c.insecure { + opts = append(opts, name.Insecure) + } + return opts +} + +// remoteOptions returns the remote options for registry operations. +func (c *DefaultImageChecker) remoteOptions(ctx context.Context) []remote.Option { + opts := []remote.Option{ + remote.WithContext(ctx), + } + + if c.username != "" || c.password != "" { + opts = append(opts, remote.WithAuth(&authn.Basic{ + Username: c.username, + Password: c.password, + })) + } + + if c.insecure { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = tlsConfig + opts = append(opts, remote.WithTransport(transport)) + } + + return opts +} + +// isNotFoundError checks if the error indicates that the image was not found. +func isNotFoundError(err error) bool { + if err == nil { + return false + } + errStr := err.Error() + return strings.Contains(errStr, "MANIFEST_UNKNOWN") || + strings.Contains(errStr, "NAME_UNKNOWN") || + strings.Contains(errStr, "not found") || + strings.Contains(errStr, "404") +} diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/cvi/scraper.go b/images/virtualization-artifact/pkg/monitoring/metrics/cvi/scraper.go index f967050efa..a32b9f86a9 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/cvi/scraper.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/cvi/scraper.go @@ -54,6 +54,7 @@ func (s *scraper) updateMetricClusterVirtualImageStatusPhase(m *dataMetric) { {phase == v1alpha2.ImageReady, string(v1alpha2.ImageReady)}, {phase == v1alpha2.ImageFailed, string(v1alpha2.ImageFailed)}, {phase == v1alpha2.ImageTerminating, string(v1alpha2.ImageTerminating)}, + {phase == v1alpha2.ImageLost, string(v1alpha2.ImageLost)}, } for _, p := range phases { diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/vi/scraper.go b/images/virtualization-artifact/pkg/monitoring/metrics/vi/scraper.go index bb58aa6656..c7f465fe94 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/vi/scraper.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/vi/scraper.go @@ -55,6 +55,7 @@ func (s *scraper) updateMetricVirtualImageStatusPhase(m *dataMetric) { {phase == v1alpha2.ImageFailed, string(v1alpha2.ImageFailed)}, {phase == v1alpha2.ImageTerminating, string(v1alpha2.ImageTerminating)}, {phase == v1alpha2.ImageLost, string(v1alpha2.ImageLost)}, + {phase == v1alpha2.ImagePVCLost, string(v1alpha2.ImagePVCLost)}, } for _, p := range phases {