From 3152c3db2fc120b940841e0276320157fbbc4fde Mon Sep 17 00:00:00 2001 From: Isteb4k Date: Wed, 4 Sep 2024 12:16:26 +0200 Subject: [PATCH] feat(vmsnapshot): create new controller Signed-off-by: Isteb4k --- CONTRIBUTING.md | 8 +- .../typed/core/v1alpha2/core_client.go | 5 + .../core/v1alpha2/fake/fake_core_client.go | 4 + .../fake/fake_virtualmachinesnapshot.go | 140 ++++ .../core/v1alpha2/generated_expansion.go | 2 + .../core/v1alpha2/virtualmachinesnapshot.go | 194 +++++ .../core/v1alpha2/interface.go | 7 + .../core/v1alpha2/virtualmachinesnapshot.go | 89 +++ .../informers/externalversions/generic.go | 2 + .../core/v1alpha2/expansion_generated.go | 8 + .../core/v1alpha2/virtualmachinesnapshot.go | 98 +++ api/core/v1alpha2/finalizers.go | 2 + api/core/v1alpha2/register.go | 2 + api/core/v1alpha2/virtual_machine_snapshot.go | 131 +++ api/core/v1alpha2/vmcondition/condition.go | 4 + api/core/v1alpha2/vmscondition/condition.go | 66 ++ api/core/v1alpha2/zz_generated.deepcopy.go | 131 +++ .../generated/openapi/zz_generated.openapi.go | 254 ++++++ crds/doc-ru-virtualmachinesnapshots.yaml | 70 ++ crds/virtualmachinesnapshots.yaml | 230 ++++++ .../cmd/virtualization-controller/main.go | 6 + .../controller/service/snapshot_service.go | 83 +- .../controller/vd/internal/source/blank.go | 2 +- .../pkg/controller/vd/internal/source/http.go | 2 +- .../vd/internal/source/object_ref.go | 2 +- .../internal/source/object_ref_vdsnapshot.go | 2 +- .../controller/vd/internal/source/registry.go | 2 +- .../controller/vd/internal/source/upload.go | 2 +- .../vdsnapshot/internal/interfaces.go | 2 +- .../vdsnapshot/internal/life_cycle.go | 34 +- .../vdsnapshot/internal/life_cycle_test.go | 2 +- .../controller/vdsnapshot/internal/mock.go | 34 +- .../vdsnapshot/vdsnapshot_reconciler.go | 16 +- .../controller/vm/internal/snapshotting.go | 95 +++ .../pkg/controller/vm/internal/sync_kvvm.go | 4 + .../internal/watchers/vmsnapshot_watcher.go | 104 +++ .../pkg/controller/vm/vm_controller.go | 1 + .../pkg/controller/vm/vm_reconciler.go | 7 + .../vmsnapshot/internal/deletion.go | 42 + .../vmsnapshot/internal/handler_test.go | 37 + .../vmsnapshot/internal/interfaces.go | 46 ++ .../vmsnapshot/internal/life_cycle.go | 490 ++++++++++++ .../vmsnapshot/internal/life_cycle_test.go | 329 ++++++++ .../controller/vmsnapshot/internal/mock.go | 747 ++++++++++++++++++ .../vmsnapshot/internal/secret_builder.go | 235 ++++++ .../internal/virtual_machine_ready.go | 116 +++ .../vmsnapshot/internal/watcher/vd_watcher.go | 113 +++ .../internal/watcher/vdsnapshot_watcher.go | 102 +++ .../vmsnapshot/internal/watcher/vm_watcher.go | 110 +++ .../internal/watcher/vmsnapshot_watcher.go | 53 ++ .../vmsnapshot/vmsnapshot_controller.go | 80 ++ .../vmsnapshot/vmsnapshot_reconciler.go | 121 +++ .../vmsnapshot/vmsnapshot_webhook.go | 77 ++ .../rbac-for-us.yaml | 6 +- 54 files changed, 4471 insertions(+), 80 deletions(-) create mode 100644 api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachinesnapshot.go create mode 100644 api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachinesnapshot.go create mode 100644 api/client/generated/informers/externalversions/core/v1alpha2/virtualmachinesnapshot.go create mode 100644 api/client/generated/listers/core/v1alpha2/virtualmachinesnapshot.go create mode 100644 api/core/v1alpha2/virtual_machine_snapshot.go create mode 100644 api/core/v1alpha2/vmscondition/condition.go create mode 100644 crds/doc-ru-virtualmachinesnapshots.yaml create mode 100644 crds/virtualmachinesnapshots.yaml create mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/snapshotting.go create mode 100644 images/virtualization-artifact/pkg/controller/vm/internal/watchers/vmsnapshot_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/deletion.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/handler_test.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/interfaces.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle_test.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/mock.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/secret_builder.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/virtual_machine_ready.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vd_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vdsnapshot_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vm_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vmsnapshot_watcher.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_controller.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_reconciler.go create mode 100644 images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_webhook.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb7a658c8..8561266e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ Each commit message consists of a **header** and a [**body**](#body). The header - _feat(api): bump resource version to v1beta1_ - _feat(images): implement ImageLost phase_ - _feat(images, vi): add PVC as a storage_ - - _fix(vm, vmmodel): fix unsupported type of model_ + - _fix(vm, vmclass): fix unsupported type of class_ - _refactor(core): rename 3rd party resources_ - _docs(module): describe how to install module_ - _chore(core): use alt linux as base image_ @@ -92,11 +92,11 @@ Supported scopes are the following: - vm - vmop - vmbda - - vmcpu - - vmcpureq - - vmmodel + - vmclass - vmip - vmipl + - vdsnapshot + - vmsnapshot - disks - vd - images diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/core_client.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/core_client.go index 1b3542d11..9443fee15 100644 --- a/api/client/generated/clientset/versioned/typed/core/v1alpha2/core_client.go +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/core_client.go @@ -38,6 +38,7 @@ type VirtualizationV1alpha2Interface interface { VirtualMachineIPAddressesGetter VirtualMachineIPAddressLeasesGetter VirtualMachineOperationsGetter + VirtualMachineSnapshotsGetter } // VirtualizationV1alpha2Client is used to interact with features provided by the virtualization.deckhouse.io group. @@ -85,6 +86,10 @@ func (c *VirtualizationV1alpha2Client) VirtualMachineOperations(namespace string return newVirtualMachineOperations(c, namespace) } +func (c *VirtualizationV1alpha2Client) VirtualMachineSnapshots(namespace string) VirtualMachineSnapshotInterface { + return newVirtualMachineSnapshots(c, namespace) +} + // NewForConfig creates a new VirtualizationV1alpha2Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_core_client.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_core_client.go index 4daebc122..42c7892a5 100644 --- a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_core_client.go +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_core_client.go @@ -68,6 +68,10 @@ func (c *FakeVirtualizationV1alpha2) VirtualMachineOperations(namespace string) return &FakeVirtualMachineOperations{c, namespace} } +func (c *FakeVirtualizationV1alpha2) VirtualMachineSnapshots(namespace string) v1alpha2.VirtualMachineSnapshotInterface { + return &FakeVirtualMachineSnapshots{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeVirtualizationV1alpha2) RESTClient() rest.Interface { diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachinesnapshot.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachinesnapshot.go new file mode 100644 index 000000000..466f5c465 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachinesnapshot.go @@ -0,0 +1,140 @@ +/* +Copyright 2022 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. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeVirtualMachineSnapshots implements VirtualMachineSnapshotInterface +type FakeVirtualMachineSnapshots struct { + Fake *FakeVirtualizationV1alpha2 + ns string +} + +var virtualmachinesnapshotsResource = v1alpha2.SchemeGroupVersion.WithResource("virtualmachinesnapshots") + +var virtualmachinesnapshotsKind = v1alpha2.SchemeGroupVersion.WithKind("VirtualMachineSnapshot") + +// Get takes name of the virtualMachineSnapshot, and returns the corresponding virtualMachineSnapshot object, and an error if there is any. +func (c *FakeVirtualMachineSnapshots) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(virtualmachinesnapshotsResource, c.ns, name), &v1alpha2.VirtualMachineSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualMachineSnapshot), err +} + +// List takes label and field selectors, and returns the list of VirtualMachineSnapshots that match those selectors. +func (c *FakeVirtualMachineSnapshots) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VirtualMachineSnapshotList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(virtualmachinesnapshotsResource, virtualmachinesnapshotsKind, c.ns, opts), &v1alpha2.VirtualMachineSnapshotList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.VirtualMachineSnapshotList{ListMeta: obj.(*v1alpha2.VirtualMachineSnapshotList).ListMeta} + for _, item := range obj.(*v1alpha2.VirtualMachineSnapshotList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested virtualMachineSnapshots. +func (c *FakeVirtualMachineSnapshots) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(virtualmachinesnapshotsResource, c.ns, opts)) + +} + +// Create takes the representation of a virtualMachineSnapshot and creates it. Returns the server's representation of the virtualMachineSnapshot, and an error, if there is any. +func (c *FakeVirtualMachineSnapshots) Create(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.CreateOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(virtualmachinesnapshotsResource, c.ns, virtualMachineSnapshot), &v1alpha2.VirtualMachineSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualMachineSnapshot), err +} + +// Update takes the representation of a virtualMachineSnapshot and updates it. Returns the server's representation of the virtualMachineSnapshot, and an error, if there is any. +func (c *FakeVirtualMachineSnapshots) Update(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(virtualmachinesnapshotsResource, c.ns, virtualMachineSnapshot), &v1alpha2.VirtualMachineSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualMachineSnapshot), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeVirtualMachineSnapshots) UpdateStatus(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualMachineSnapshot, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(virtualmachinesnapshotsResource, "status", c.ns, virtualMachineSnapshot), &v1alpha2.VirtualMachineSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualMachineSnapshot), err +} + +// Delete takes name of the virtualMachineSnapshot and deletes it. Returns an error if one occurs. +func (c *FakeVirtualMachineSnapshots) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(virtualmachinesnapshotsResource, c.ns, name, opts), &v1alpha2.VirtualMachineSnapshot{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeVirtualMachineSnapshots) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(virtualmachinesnapshotsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.VirtualMachineSnapshotList{}) + return err +} + +// Patch applies the patch and returns the patched virtualMachineSnapshot. +func (c *FakeVirtualMachineSnapshots) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VirtualMachineSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(virtualmachinesnapshotsResource, c.ns, name, pt, data, subresources...), &v1alpha2.VirtualMachineSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualMachineSnapshot), err +} diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/generated_expansion.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/generated_expansion.go index 6e1144d6f..fcf59a09b 100644 --- a/api/client/generated/clientset/versioned/typed/core/v1alpha2/generated_expansion.go +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/generated_expansion.go @@ -37,3 +37,5 @@ type VirtualMachineIPAddressExpansion interface{} type VirtualMachineIPAddressLeaseExpansion interface{} type VirtualMachineOperationExpansion interface{} + +type VirtualMachineSnapshotExpansion interface{} diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachinesnapshot.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachinesnapshot.go new file mode 100644 index 000000000..f6e599cbc --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachinesnapshot.go @@ -0,0 +1,194 @@ +/* +Copyright 2022 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. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + "time" + + scheme "github.com/deckhouse/virtualization/api/client/generated/clientset/versioned/scheme" + v1alpha2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// VirtualMachineSnapshotsGetter has a method to return a VirtualMachineSnapshotInterface. +// A group's client should implement this interface. +type VirtualMachineSnapshotsGetter interface { + VirtualMachineSnapshots(namespace string) VirtualMachineSnapshotInterface +} + +// VirtualMachineSnapshotInterface has methods to work with VirtualMachineSnapshot resources. +type VirtualMachineSnapshotInterface interface { + Create(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.CreateOptions) (*v1alpha2.VirtualMachineSnapshot, error) + Update(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualMachineSnapshot, error) + UpdateStatus(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualMachineSnapshot, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha2.VirtualMachineSnapshot, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.VirtualMachineSnapshotList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VirtualMachineSnapshot, err error) + VirtualMachineSnapshotExpansion +} + +// virtualMachineSnapshots implements VirtualMachineSnapshotInterface +type virtualMachineSnapshots struct { + client rest.Interface + ns string +} + +// newVirtualMachineSnapshots returns a VirtualMachineSnapshots +func newVirtualMachineSnapshots(c *VirtualizationV1alpha2Client, namespace string) *virtualMachineSnapshots { + return &virtualMachineSnapshots{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the virtualMachineSnapshot, and returns the corresponding virtualMachineSnapshot object, and an error if there is any. +func (c *virtualMachineSnapshots) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + result = &v1alpha2.VirtualMachineSnapshot{} + err = c.client.Get(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of VirtualMachineSnapshots that match those selectors. +func (c *virtualMachineSnapshots) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VirtualMachineSnapshotList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.VirtualMachineSnapshotList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested virtualMachineSnapshots. +func (c *virtualMachineSnapshots) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a virtualMachineSnapshot and creates it. Returns the server's representation of the virtualMachineSnapshot, and an error, if there is any. +func (c *virtualMachineSnapshots) Create(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.CreateOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + result = &v1alpha2.VirtualMachineSnapshot{} + err = c.client.Post(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualMachineSnapshot). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a virtualMachineSnapshot and updates it. Returns the server's representation of the virtualMachineSnapshot, and an error, if there is any. +func (c *virtualMachineSnapshots) Update(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + result = &v1alpha2.VirtualMachineSnapshot{} + err = c.client.Put(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + Name(virtualMachineSnapshot.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualMachineSnapshot). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *virtualMachineSnapshots) UpdateStatus(ctx context.Context, virtualMachineSnapshot *v1alpha2.VirtualMachineSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualMachineSnapshot, err error) { + result = &v1alpha2.VirtualMachineSnapshot{} + err = c.client.Put(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + Name(virtualMachineSnapshot.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualMachineSnapshot). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the virtualMachineSnapshot and deletes it. Returns an error if one occurs. +func (c *virtualMachineSnapshots) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *virtualMachineSnapshots) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched virtualMachineSnapshot. +func (c *virtualMachineSnapshots) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VirtualMachineSnapshot, err error) { + result = &v1alpha2.VirtualMachineSnapshot{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("virtualmachinesnapshots"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/api/client/generated/informers/externalversions/core/v1alpha2/interface.go b/api/client/generated/informers/externalversions/core/v1alpha2/interface.go index fcbb5eed4..9178544d2 100644 --- a/api/client/generated/informers/externalversions/core/v1alpha2/interface.go +++ b/api/client/generated/informers/externalversions/core/v1alpha2/interface.go @@ -44,6 +44,8 @@ type Interface interface { VirtualMachineIPAddressLeases() VirtualMachineIPAddressLeaseInformer // VirtualMachineOperations returns a VirtualMachineOperationInformer. VirtualMachineOperations() VirtualMachineOperationInformer + // VirtualMachineSnapshots returns a VirtualMachineSnapshotInformer. + VirtualMachineSnapshots() VirtualMachineSnapshotInformer } type version struct { @@ -106,3 +108,8 @@ func (v *version) VirtualMachineIPAddressLeases() VirtualMachineIPAddressLeaseIn func (v *version) VirtualMachineOperations() VirtualMachineOperationInformer { return &virtualMachineOperationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// VirtualMachineSnapshots returns a VirtualMachineSnapshotInformer. +func (v *version) VirtualMachineSnapshots() VirtualMachineSnapshotInformer { + return &virtualMachineSnapshotInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/api/client/generated/informers/externalversions/core/v1alpha2/virtualmachinesnapshot.go b/api/client/generated/informers/externalversions/core/v1alpha2/virtualmachinesnapshot.go new file mode 100644 index 000000000..4e85ca84d --- /dev/null +++ b/api/client/generated/informers/externalversions/core/v1alpha2/virtualmachinesnapshot.go @@ -0,0 +1,89 @@ +/* +Copyright 2022 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. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + time "time" + + versioned "github.com/deckhouse/virtualization/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/virtualization/api/client/generated/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/deckhouse/virtualization/api/client/generated/listers/core/v1alpha2" + corev1alpha2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// VirtualMachineSnapshotInformer provides access to a shared informer and lister for +// VirtualMachineSnapshots. +type VirtualMachineSnapshotInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.VirtualMachineSnapshotLister +} + +type virtualMachineSnapshotInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewVirtualMachineSnapshotInformer constructs a new informer for VirtualMachineSnapshot type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewVirtualMachineSnapshotInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredVirtualMachineSnapshotInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredVirtualMachineSnapshotInformer constructs a new informer for VirtualMachineSnapshot type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredVirtualMachineSnapshotInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.VirtualizationV1alpha2().VirtualMachineSnapshots(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.VirtualizationV1alpha2().VirtualMachineSnapshots(namespace).Watch(context.TODO(), options) + }, + }, + &corev1alpha2.VirtualMachineSnapshot{}, + resyncPeriod, + indexers, + ) +} + +func (f *virtualMachineSnapshotInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredVirtualMachineSnapshotInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *virtualMachineSnapshotInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&corev1alpha2.VirtualMachineSnapshot{}, f.defaultInformer) +} + +func (f *virtualMachineSnapshotInformer) Lister() v1alpha2.VirtualMachineSnapshotLister { + return v1alpha2.NewVirtualMachineSnapshotLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/generic.go b/api/client/generated/informers/externalversions/generic.go index 5dbc8dfe0..dd169446c 100644 --- a/api/client/generated/informers/externalversions/generic.go +++ b/api/client/generated/informers/externalversions/generic.go @@ -73,6 +73,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualMachineIPAddressLeases().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("virtualmachineoperations"): return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualMachineOperations().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("virtualmachinesnapshots"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualMachineSnapshots().Informer()}, nil } diff --git a/api/client/generated/listers/core/v1alpha2/expansion_generated.go b/api/client/generated/listers/core/v1alpha2/expansion_generated.go index 71132e318..b504fbe43 100644 --- a/api/client/generated/listers/core/v1alpha2/expansion_generated.go +++ b/api/client/generated/listers/core/v1alpha2/expansion_generated.go @@ -85,3 +85,11 @@ type VirtualMachineOperationListerExpansion interface{} // VirtualMachineOperationNamespaceListerExpansion allows custom methods to be added to // VirtualMachineOperationNamespaceLister. type VirtualMachineOperationNamespaceListerExpansion interface{} + +// VirtualMachineSnapshotListerExpansion allows custom methods to be added to +// VirtualMachineSnapshotLister. +type VirtualMachineSnapshotListerExpansion interface{} + +// VirtualMachineSnapshotNamespaceListerExpansion allows custom methods to be added to +// VirtualMachineSnapshotNamespaceLister. +type VirtualMachineSnapshotNamespaceListerExpansion interface{} diff --git a/api/client/generated/listers/core/v1alpha2/virtualmachinesnapshot.go b/api/client/generated/listers/core/v1alpha2/virtualmachinesnapshot.go new file mode 100644 index 000000000..074597a91 --- /dev/null +++ b/api/client/generated/listers/core/v1alpha2/virtualmachinesnapshot.go @@ -0,0 +1,98 @@ +/* +Copyright 2022 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. +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// VirtualMachineSnapshotLister helps list VirtualMachineSnapshots. +// All objects returned here must be treated as read-only. +type VirtualMachineSnapshotLister interface { + // List lists all VirtualMachineSnapshots in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VirtualMachineSnapshot, err error) + // VirtualMachineSnapshots returns an object that can list and get VirtualMachineSnapshots. + VirtualMachineSnapshots(namespace string) VirtualMachineSnapshotNamespaceLister + VirtualMachineSnapshotListerExpansion +} + +// virtualMachineSnapshotLister implements the VirtualMachineSnapshotLister interface. +type virtualMachineSnapshotLister struct { + indexer cache.Indexer +} + +// NewVirtualMachineSnapshotLister returns a new VirtualMachineSnapshotLister. +func NewVirtualMachineSnapshotLister(indexer cache.Indexer) VirtualMachineSnapshotLister { + return &virtualMachineSnapshotLister{indexer: indexer} +} + +// List lists all VirtualMachineSnapshots in the indexer. +func (s *virtualMachineSnapshotLister) List(selector labels.Selector) (ret []*v1alpha2.VirtualMachineSnapshot, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VirtualMachineSnapshot)) + }) + return ret, err +} + +// VirtualMachineSnapshots returns an object that can list and get VirtualMachineSnapshots. +func (s *virtualMachineSnapshotLister) VirtualMachineSnapshots(namespace string) VirtualMachineSnapshotNamespaceLister { + return virtualMachineSnapshotNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// VirtualMachineSnapshotNamespaceLister helps list and get VirtualMachineSnapshots. +// All objects returned here must be treated as read-only. +type VirtualMachineSnapshotNamespaceLister interface { + // List lists all VirtualMachineSnapshots in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VirtualMachineSnapshot, err error) + // Get retrieves the VirtualMachineSnapshot from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha2.VirtualMachineSnapshot, error) + VirtualMachineSnapshotNamespaceListerExpansion +} + +// virtualMachineSnapshotNamespaceLister implements the VirtualMachineSnapshotNamespaceLister +// interface. +type virtualMachineSnapshotNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all VirtualMachineSnapshots in the indexer for a given namespace. +func (s virtualMachineSnapshotNamespaceLister) List(selector labels.Selector) (ret []*v1alpha2.VirtualMachineSnapshot, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VirtualMachineSnapshot)) + }) + return ret, err +} + +// Get retrieves the VirtualMachineSnapshot from the indexer for a given namespace and name. +func (s virtualMachineSnapshotNamespaceLister) Get(name string) (*v1alpha2.VirtualMachineSnapshot, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("virtualmachinesnapshot"), name) + } + return obj.(*v1alpha2.VirtualMachineSnapshot), nil +} diff --git a/api/core/v1alpha2/finalizers.go b/api/core/v1alpha2/finalizers.go index 06980afd8..ab7fcbbce 100644 --- a/api/core/v1alpha2/finalizers.go +++ b/api/core/v1alpha2/finalizers.go @@ -24,6 +24,7 @@ const ( FinalizerIPAddressProtection = "virtualization.deckhouse.io/vmip-protection" FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" FinalizerVDSnapshotProtection = "virtualization.deckhouse.io/vdsnapshot-protection" + FinalizerVMSnapshotProtection = "virtualization.deckhouse.io/vmsnapshot-protection" FinalizerCVICleanup = "virtualization.deckhouse.io/cvi-cleanup" FinalizerVDCleanup = "virtualization.deckhouse.io/vd-cleanup" @@ -32,6 +33,7 @@ const ( FinalizerIPAddressCleanup = "virtualization.deckhouse.io/vmip-cleanup" FinalizerIPAddressLeaseCleanup = "virtualization.deckhouse.io/vmipl-cleanup" FinalizerVDSnapshotCleanup = "virtualization.deckhouse.io/vdsnapshot-cleanup" + FinalizerVMSnapshotCleanup = "virtualization.deckhouse.io/vmsnapshot-cleanup" FinalizerVMOPCleanup = "virtualization.deckhouse.io/vmop-cleanup" FinalizerVMClassCleanup = "virtualization.deckhouse.io/vmclass-cleanup" FinalizerVMBDACleanup = "virtualization.deckhouse.io/vmbda-cleanup" diff --git a/api/core/v1alpha2/register.go b/api/core/v1alpha2/register.go index 62b5c908e..362ff5afc 100644 --- a/api/core/v1alpha2/register.go +++ b/api/core/v1alpha2/register.go @@ -82,6 +82,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineOperationList{}, &VirtualDiskSnapshot{}, &VirtualDiskSnapshotList{}, + &VirtualMachineSnapshot{}, + &VirtualMachineSnapshotList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/api/core/v1alpha2/virtual_machine_snapshot.go b/api/core/v1alpha2/virtual_machine_snapshot.go new file mode 100644 index 000000000..d0ad91ba6 --- /dev/null +++ b/api/core/v1alpha2/virtual_machine_snapshot.go @@ -0,0 +1,131 @@ +/* +Copyright 2024 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. +*/ + +// +kubebuilder:object:generate=true +// +groupName=virtualization.deckhouse.io +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + VirtualMachineSnapshotKind = "VirtualMachineSnapshot" + VirtualMachineSnapshotResource = "virtualmachinesnapshots" +) + +// VirtualMachineSnapshot provides a resource for creating snapshots of virtual machines. +// +// +kubebuilder:object:root=true +// +kubebuilder:metadata:labels={heritage=deckhouse,module=virtualization} +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories=virtualization,scope=Namespaced,shortName={vmsnapshot,vmsnapshots},singular=virtualmachinesnapshot +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="VirtualMachineSnapshot phase." +// +kubebuilder:printcolumn:name="Consistent",type="boolean",JSONPath=".status.consistent",description="VirtualMachineSnapshot consistency." +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="VirtualMachineSnapshot age." +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VirtualMachineSnapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VirtualMachineSnapshotSpec `json:"spec"` + Status VirtualMachineSnapshotStatus `json:"status,omitempty"` +} + +// VirtualMachineSnapshotList contains a list of `VirtualMachineSnapshot` +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VirtualMachineSnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []VirtualMachineSnapshot `json:"items"` +} + +type VirtualMachineSnapshotSpec struct { + // The name of virtual machine to take snapshot. + // + // +required + VirtualMachineName string `json:"virtualMachineName"` + // Create a snapshot of a virtual machine only if it is possible to freeze the machine through the agent. + // + // If value is true, the snapshot of the virtual machine will be taken only in the following scenarios: + // - the virtual machine is powered off. + // - the virtual machine with an agent, and the freeze operation was successful. + // + // +kubebuilder:default:=true + RequiredConsistency bool `json:"requiredConsistency"` + // +kubebuilder:default:="Always" + KeepIPAddress KeepIPAddress `json:"keepIPAddress"` + // +optional + VolumeSnapshotClassNames []VolumeSnapshotClassName `json:"volumeSnapshotClassNames"` +} + +type VirtualMachineSnapshotStatus struct { + Phase VirtualMachineSnapshotPhase `json:"phase"` + // The virtual machine snapshot is consistent. + Consistent *bool `json:"consistent,omitempty"` + // The name of underlying `Secret`, created for virtual machine snapshotting. + VirtualMachineSnapshotSecretName string `json:"virtualMachineSnapshotSecretName,omitempty"` + // The list of `VirtualDiskSnapshot` names that associated with the virtual machine and taken for snapshotting. + VirtualDiskSnapshotNames []string `json:"virtualDiskSnapshotNames,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + // The generation last processed by the controller. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// VolumeSnapshotClassName defines `StorageClass` and `VolumeSnapshotClass` binding. +type VolumeSnapshotClassName struct { + // The `StorageClass` name associated with `VolumeSnapshotClass`. + // + // +required + StorageClassName string `json:"storageClassName"` + // The name of `VolumeSnapshotClass` to use for virtual disk snapshotting. + // + // +required + VolumeSnapshotClassName string `json:"volumeSnapshotClassName"` +} + +// KeepIPAddress defines whether to save the IP address of the virtual machine or not: +// +// * Always - when creating a snapshot, the virtual machine's IP address will be converted from `Auto` to `Static` and saved. +// * Never - when creating a snapshot, the virtual machine's IP address will not be converted. +// +// +kubebuilder:validation:Enum={Always,Never} +type KeepIPAddress string + +const ( + KeepIPAddressAlways VirtualMachineSnapshotPhase = "Always" + KeepIPAddressNever VirtualMachineSnapshotPhase = "Never" +) + +// VirtualMachineSnapshotPhase defines current status of resource: +// +// * Pending - the resource has been created and is on a waiting queue. +// * InProgress - the process of creating the snapshot is currently underway. +// * Ready - the snapshot creation has successfully completed, and the virtual machine snapshot is now available. +// * Failed - an error occurred during the snapshotting process. +// * Terminating - the resource is in the process of being deleted. +// +// +kubebuilder:validation:Enum={Pending,InProgress,Ready,Failed,Terminating} +type VirtualMachineSnapshotPhase string + +const ( + VirtualMachineSnapshotPhasePending VirtualMachineSnapshotPhase = "Pending" + VirtualMachineSnapshotPhaseInProgress VirtualMachineSnapshotPhase = "InProgress" + VirtualMachineSnapshotPhaseReady VirtualMachineSnapshotPhase = "Ready" + VirtualMachineSnapshotPhaseFailed VirtualMachineSnapshotPhase = "Failed" + VirtualMachineSnapshotPhaseTerminating VirtualMachineSnapshotPhase = "Terminating" +) diff --git a/api/core/v1alpha2/vmcondition/condition.go b/api/core/v1alpha2/vmcondition/condition.go index 5b7a024f9..f9922d351 100644 --- a/api/core/v1alpha2/vmcondition/condition.go +++ b/api/core/v1alpha2/vmcondition/condition.go @@ -37,6 +37,7 @@ const ( TypeAwaitingRestartToApplyConfiguration Type = "AwaitingRestartToApplyConfiguration" TypeFilesystemReady Type = "FilesystemReady" TypeSizingPolicyMatched Type = "SizingPolicyMatched" + TypeSnapshotting Type = "Snapshotting" ) type Reason string @@ -88,6 +89,9 @@ const ( ReasonFilesystemFrozen Reason = "Frozen" ReasonFilesystemNotReady Reason = "NotReady" + ReasonChosenForSnapshotting Reason = "WaitingForTheSnapshotting" + ReasonSnapshottingInProgress Reason = "SnapshottingInProgress" + ReasonSizingPolicyMatched Reason = "SizingPolicyMatched" ReasonSizingPolicyNotMatched Reason = "SizingPolicyNotMatched" ReasonVirtualMachineClassTerminating Reason = "VirtualMachineClassTerminating" diff --git a/api/core/v1alpha2/vmscondition/condition.go b/api/core/v1alpha2/vmscondition/condition.go new file mode 100644 index 000000000..1d257672e --- /dev/null +++ b/api/core/v1alpha2/vmscondition/condition.go @@ -0,0 +1,66 @@ +/* +Copyright 2024 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 vmscondition + +// Type represents the various condition types for the `VirtualMachineSnapshot`. +type Type = string + +const ( + // VirtualMachineReadyType indicates that the `VirtualMachine` is ready for snapshotting. + VirtualMachineReadyType Type = "VirtualMachineReady" + // VirtualMachineSnapshotReadyType indicates that the virtual machine snapshot has been successfully taken and is ready for restore. + VirtualMachineSnapshotReadyType Type = "VirtualMachineSnapshotReady" +) + +type ( + // VirtualMachineReadyReason represents the various reasons for the `VirtualMachineReady` condition type. + VirtualMachineReadyReason = string + // VirtualMachineSnapshotReadyReason represents the various reasons for the `VirtualMachineSnapshotReady` condition type. + VirtualMachineSnapshotReadyReason = string +) + +const ( + // VirtualMachineUnknown signifies that the state of virtual machine is unknown, preventing the snapshot process from starting. + VirtualMachineUnknown VirtualMachineReadyReason = "Unknown" + // VirtualMachineReady signifies that the virtual machine is ready for snapshotting, allowing the snapshot process to begin. + VirtualMachineReady VirtualMachineReadyReason = "VirtualMachineReady" + // VirtualMachineNotReadyForSnapshotting signifies that the virtual machine is not ready for snapshotting, preventing the snapshot process from starting. + VirtualMachineNotReadyForSnapshotting VirtualMachineReadyReason = "VirtualMachineNotReadyForSnapshotting" + + // WaitingForTheVirtualMachine signifies that the snapshot process is waiting for the virtual machine to become ready for snapshotting. + WaitingForTheVirtualMachine VirtualMachineSnapshotReadyReason = "WaitingForTheVirtualMachine" + // RestartAwaitingChanges signifies that the snapshotting process cannot begin because the virtual machine has awaiting changes. + RestartAwaitingChanges VirtualMachineSnapshotReadyReason = "RestartAwaitingChanges" + // BlockDevicesNotReady signifies that the snapshotting process cannot begin because the block devices of the virtual machine are not ready. + BlockDevicesNotReady VirtualMachineSnapshotReadyReason = "BlockDevicesNotReady" + // PotentiallyInconsistent signifies that the snapshotting process cannot begin because creating a snapshot of the running virtual machine might result in an inconsistent snapshot. + PotentiallyInconsistent VirtualMachineSnapshotReadyReason = "PotentiallyInconsistent" + // VirtualMachineSnapshotSecretLost signifies that the underling `Secret` is lost: cannot restore the virtual machine using this snapshot. + VirtualMachineSnapshotSecretLost VirtualMachineSnapshotReadyReason = "VirtualMachineSnapshotSecretLost" + // VirtualDiskSnapshotLost signifies that the underling `VirtualDiskSnapshot` is lost: cannot restore the virtual machine using this snapshot. + VirtualDiskSnapshotLost VirtualMachineSnapshotReadyReason = "VirtualDiskSnapshotLost" + // FileSystemFreezing signifies that the `VirtualMachineSnapshot` resource is in the process of freezing the filesystem of the virtual machine. + FileSystemFreezing VirtualMachineSnapshotReadyReason = "FileSystemFreezing" + // Snapshotting signifies that the `VirtualMachineSnapshot` resource is in the process of taking a snapshot of the virtual machine. + Snapshotting VirtualMachineSnapshotReadyReason = "Snapshotting" + // VirtualMachineSnapshotReady signifies that the snapshot process is complete and the `VirtualMachineSnapshot` is ready for use. + VirtualMachineSnapshotReady VirtualMachineSnapshotReadyReason = "VirtualMachineSnapshotReady" + // VirtualMachineSnapshotUnknown signifies that the state of snapshot process cannot be detected. + VirtualMachineSnapshotUnknown VirtualMachineSnapshotReadyReason = "Unknown" + // VirtualMachineSnapshotFailed signifies that the snapshot process has failed. + VirtualMachineSnapshotFailed VirtualMachineSnapshotReadyReason = "VirtualMachineSnapshotFailed" +) diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index 50ae04cd5..a96a93fbc 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -2132,6 +2132,121 @@ func (in *VirtualMachinePod) DeepCopy() *VirtualMachinePod { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineSnapshot) DeepCopyInto(out *VirtualMachineSnapshot) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineSnapshot. +func (in *VirtualMachineSnapshot) DeepCopy() *VirtualMachineSnapshot { + if in == nil { + return nil + } + out := new(VirtualMachineSnapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineSnapshot) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineSnapshotList) DeepCopyInto(out *VirtualMachineSnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VirtualMachineSnapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineSnapshotList. +func (in *VirtualMachineSnapshotList) DeepCopy() *VirtualMachineSnapshotList { + if in == nil { + return nil + } + out := new(VirtualMachineSnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineSnapshotList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineSnapshotSpec) DeepCopyInto(out *VirtualMachineSnapshotSpec) { + *out = *in + if in.VolumeSnapshotClassNames != nil { + in, out := &in.VolumeSnapshotClassNames, &out.VolumeSnapshotClassNames + *out = make([]VolumeSnapshotClassName, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineSnapshotSpec. +func (in *VirtualMachineSnapshotSpec) DeepCopy() *VirtualMachineSnapshotSpec { + if in == nil { + return nil + } + out := new(VirtualMachineSnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineSnapshotStatus) DeepCopyInto(out *VirtualMachineSnapshotStatus) { + *out = *in + if in.Consistent != nil { + in, out := &in.Consistent, &out.Consistent + *out = new(bool) + **out = **in + } + if in.VirtualDiskSnapshotNames != nil { + in, out := &in.VirtualDiskSnapshotNames, &out.VirtualDiskSnapshotNames + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineSnapshotStatus. +func (in *VirtualMachineSnapshotStatus) DeepCopy() *VirtualMachineSnapshotStatus { + if in == nil { + return nil + } + out := new(VirtualMachineSnapshotStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineSpec) DeepCopyInto(out *VirtualMachineSpec) { *out = *in @@ -2272,6 +2387,22 @@ func (in *VirtualMachineStatus) DeepCopy() *VirtualMachineStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeSnapshotClassName) DeepCopyInto(out *VolumeSnapshotClassName) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeSnapshotClassName. +func (in *VolumeSnapshotClassName) DeepCopy() *VolumeSnapshotClassName { + if in == nil { + return nil + } + out := new(VolumeSnapshotClassName) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WeightedVirtualMachineAndPodAffinityTerm) DeepCopyInto(out *WeightedVirtualMachineAndPodAffinityTerm) { *out = *in diff --git a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 3658802ca..f0217065e 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -122,9 +122,14 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineOperationStatus": schema_virtualization_api_core_v1alpha2_VirtualMachineOperationStatus(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachinePhaseTransitionTimestamp": schema_virtualization_api_core_v1alpha2_VirtualMachinePhaseTransitionTimestamp(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachinePod": schema_virtualization_api_core_v1alpha2_VirtualMachinePod(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshot": schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshot(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotList": schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotList(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotSpec": schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotSpec(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotStatus": schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotStatus(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSpec": schema_virtualization_api_core_v1alpha2_VirtualMachineSpec(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineStats": schema_virtualization_api_core_v1alpha2_VirtualMachineStats(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineStatus": schema_virtualization_api_core_v1alpha2_VirtualMachineStatus(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VolumeSnapshotClassName": schema_virtualization_api_core_v1alpha2_VolumeSnapshotClassName(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.WeightedVirtualMachineAndPodAffinityTerm": schema_virtualization_api_core_v1alpha2_WeightedVirtualMachineAndPodAffinityTerm(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineAddVolume": schema_virtualization_api_subresources_v1alpha2_VirtualMachineAddVolume(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineConsole": schema_virtualization_api_subresources_v1alpha2_VirtualMachineConsole(ref), @@ -4163,6 +4168,225 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachinePod(ref common.Refere } } +func schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VirtualMachineSnapshot provides a resource for creating snapshots of virtual machines.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotSpec", "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshotStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VirtualMachineSnapshotList contains a list of `VirtualMachineSnapshot`", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshot"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualMachineSnapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "virtualMachineName": { + SchemaProps: spec.SchemaProps{ + Description: "The name of virtual machine to take snapshot.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "requiredConsistency": { + SchemaProps: spec.SchemaProps{ + Description: "Create a snapshot of a virtual machine only if it is possible to freeze the machine through the agent.\n\nIf value is true, the snapshot of the virtual machine will be taken only in the following scenarios: - the virtual machine is powered off. - the virtual machine with an agent, and the freeze operation was successful.", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "keepIPAddress": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "volumeSnapshotClassNames": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.VolumeSnapshotClassName"), + }, + }, + }, + }, + }, + }, + Required: []string{"virtualMachineName", "requiredConsistency", "keepIPAddress"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.VolumeSnapshotClassName"}, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualMachineSnapshotStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "phase": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "consistent": { + SchemaProps: spec.SchemaProps{ + Description: "The virtual machine snapshot is consistent.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "virtualMachineSnapshotSecretName": { + SchemaProps: spec.SchemaProps{ + Description: "The name of underlying `Secret`, created for virtual machine snapshotting.", + Type: []string{"string"}, + Format: "", + }, + }, + "virtualDiskSnapshotNames": { + SchemaProps: spec.SchemaProps{ + Description: "The list of `VirtualDiskSnapshot` names that associated with the virtual machine and taken for snapshotting.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "conditions": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "The generation last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + Required: []string{"phase"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, + } +} + func schema_virtualization_api_core_v1alpha2_VirtualMachineSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4483,6 +4707,36 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineStatus(ref common.Ref } } +func schema_virtualization_api_core_v1alpha2_VolumeSnapshotClassName(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VolumeSnapshotClassName defines `StorageClass` and `VolumeSnapshotClass` binding.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "storageClassName": { + SchemaProps: spec.SchemaProps{ + Description: "The `StorageClass` name associated with `VolumeSnapshotClass`.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "volumeSnapshotClassName": { + SchemaProps: spec.SchemaProps{ + Description: "The name of `VolumeSnapshotClass` to use for virtual disk snapshotting.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"storageClassName", "volumeSnapshotClassName"}, + }, + }, + } +} + func schema_virtualization_api_core_v1alpha2_WeightedVirtualMachineAndPodAffinityTerm(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/crds/doc-ru-virtualmachinesnapshots.yaml b/crds/doc-ru-virtualmachinesnapshots.yaml new file mode 100644 index 000000000..e2e925437 --- /dev/null +++ b/crds/doc-ru-virtualmachinesnapshots.yaml @@ -0,0 +1,70 @@ +spec: + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + `VirtualMachineSnapshot` предоставляет ресурс для создания снимков виртуальных машин. + properties: + spec: + properties: + keepIPAddress: + description: |- + Сохранить ip адрес виртуальной машины или нет: + + * Always - при создании снимка адреса виртуальный машины будет конвертирован из `Auto` в `Static` и сохранен. + * Never - при создании снимка адрес виртуальной машины не будет конвертирован. + requiredConsistency: + description: |- + Создавать снимок виртуальной машины только в том случае, если возможно заморозить её через агента. + + Если значение установлено в true, снимок виртуальной машины будет создан только в следующих случаях: + - виртуальная машина выключена; + - виртуальная машина с агентом, операция заморозки прошла успешно. + virtualMachineName: + description: Имя виртуальной машины для создания снимка. + volumeSnapshotClassNames: + items: + description: Устанавливает соответствие между `StorageClass` и `VolumeSnapshotClass`. + properties: + storageClassName: + description: Имя ресурса `StorageClass`, соответствующее указанному ресурсу `VolumeSnapshotClass`. + volumeSnapshotClassName: + description: Имя ресурса `VolumeSnapshotClass`, который будет использован для создания снимков виртуальных дисков с соответствующим классом хранения. + status: + properties: + conditions: + description: | + Последнее подтвержденное состояние данного ресурса. + items: + properties: + lastProbeTime: + description: Время проверки условия. + lastTransitionTime: + description: Время перехода условия из одного состояния в другое. + message: + description: Удобочитаемое сообщение с подробной информацией о последнем переходе. + reason: + description: Краткая причина последнего перехода состояния. + status: + description: | + Статус условия. Возможные значения: `True`, `False`, `Unknown`. + type: + description: Тип условия. + consistent: + description: Снимок виртуальной машины консистентен. + observedGeneration: + description: Поколение ресурса, которое в последний раз обрабатывалось контроллером. + phase: + description: |- + Текущее состояние ресурса `VirtualMachineSnapshot`: + + * `Pending` — ресурс был создан и находится в очереди ожидания. + * `InProgress` — идет процесс создания снимка виртуальной машины. + * `Ready` — создание снимка успешно завершено, и снимок виртуальной машины доступен для использования. + * `Failed` — произошла ошибка во время процесса создания снимка виртуальной машины. + * `Terminating` — ресурс находится в процессе удаления. + virtualDiskSnapshotNames: + description: Имена созданных снимков виртаульных дисков. + virtualMachineSnapshotSecretName: + description: Имя созданного секрета с информацией о снимке виртуальной машины. diff --git a/crds/virtualmachinesnapshots.yaml b/crds/virtualmachinesnapshots.yaml new file mode 100644 index 000000000..469dbcb06 --- /dev/null +++ b/crds/virtualmachinesnapshots.yaml @@ -0,0 +1,230 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + labels: + heritage: deckhouse + module: virtualization + name: virtualmachinesnapshots.virtualization.deckhouse.io +spec: + group: virtualization.deckhouse.io + names: + categories: + - virtualization + kind: VirtualMachineSnapshot + listKind: VirtualMachineSnapshotList + plural: virtualmachinesnapshots + shortNames: + - vmsnapshot + - vmsnapshots + singular: virtualmachinesnapshot + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: VirtualMachineSnapshot phase. + jsonPath: .status.phase + name: Phase + type: string + - description: VirtualMachineSnapshot consistency. + jsonPath: .status.consistent + name: Consistent + type: boolean + - description: VirtualMachineSnapshot age. + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: + VirtualMachineSnapshot provides a resource for creating snapshots + of virtual machines. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + keepIPAddress: + default: Always + description: |- + KeepIPAddress defines whether to save the IP address of the virtual machine or not: + + + * Always - when creating a snapshot, the virtual machine's IP address will be converted from `Auto` to `Static` and saved. + * Never - when creating a snapshot, the virtual machine's IP address will not be converted. + enum: + - Always + - Never + type: string + requiredConsistency: + default: true + description: |- + Create a snapshot of a virtual machine only if it is possible to freeze the machine through the agent. + + + If value is true, the snapshot of the virtual machine will be taken only in the following scenarios: + - the virtual machine is powered off. + - the virtual machine with an agent, and the freeze operation was successful. + type: boolean + virtualMachineName: + description: The name of virtual machine to take snapshot. + type: string + volumeSnapshotClassNames: + items: + description: + VolumeSnapshotClassName defines `StorageClass` and + `VolumeSnapshotClass` binding. + properties: + storageClassName: + description: The `StorageClass` name associated with `VolumeSnapshotClass`. + type: string + volumeSnapshotClassName: + description: + The name of `VolumeSnapshotClass` to use for virtual + disk snapshotting. + type: string + required: + - storageClassName + - volumeSnapshotClassName + type: object + type: array + required: + - keepIPAddress + - requiredConsistency + - virtualMachineName + type: object + status: + properties: + conditions: + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + consistent: + description: The virtual machine snapshot is consistent. + type: boolean + observedGeneration: + description: The generation last processed by the controller. + format: int64 + type: integer + phase: + description: |- + VirtualMachineSnapshotPhase defines current status of resource: + + + * Pending - the resource has been created and is on a waiting queue. + * InProgress - the process of creating the snapshot is currently underway. + * Ready - the snapshot creation has successfully completed, and the virtual machine snapshot is now available. + * Failed - an error occurred during the snapshotting process. + * Terminating - the resource is in the process of being deleted. + enum: + - Pending + - InProgress + - Ready + - Failed + - Terminating + type: string + virtualDiskSnapshotNames: + description: + The list of `VirtualDiskSnapshot` names that associated + with the virtual machine and taken for snapshotting. + items: + type: string + type: array + virtualMachineSnapshotSecretName: + description: + The name of underlying `Secret`, created for virtual + machine snapshotting. + type: string + required: + - phase + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index 088c9fb7a..d8a6be21e 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -48,6 +48,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/vmip" "github.com/deckhouse/virtualization-controller/pkg/controller/vmiplease" "github.com/deckhouse/virtualization-controller/pkg/controller/vmop" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmsnapshot" "github.com/deckhouse/virtualization-controller/pkg/logger" "github.com/deckhouse/virtualization/api/client/kubeclient" virtv2alpha1 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -261,6 +262,11 @@ func main() { os.Exit(1) } + if _, err = vmsnapshot.NewController(ctx, mgr, log, virtClient); err != nil { + log.Error(err.Error()) + os.Exit(1) + } + if err = vmop.SetupController(ctx, mgr, log); err != nil { log.Error(err.Error()) os.Exit(1) diff --git a/images/virtualization-artifact/pkg/controller/service/snapshot_service.go b/images/virtualization-artifact/pkg/controller/service/snapshot_service.go index cdcdfa9a3..6a9f7beb1 100644 --- a/images/virtualization-artifact/pkg/controller/service/snapshot_service.go +++ b/images/virtualization-artifact/pkg/controller/service/snapshot_service.go @@ -19,7 +19,6 @@ package service import ( "context" "fmt" - "strings" vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" @@ -85,7 +84,7 @@ func (s *SnapshotService) CanUnfreeze(ctx context.Context, vdSnapshotName string vdByName := make(map[string]struct{}) for _, bdr := range vm.Status.BlockDeviceRefs { - if bdr.Kind != virtv2.DiskDevice { + if bdr.Kind == virtv2.DiskDevice { vdByName[bdr.Name] = struct{}{} } } @@ -109,6 +108,20 @@ func (s *SnapshotService) CanUnfreeze(ctx context.Context, vdSnapshotName string } } + var vmSnapshots virtv2.VirtualMachineSnapshotList + err = s.client.List(ctx, &vmSnapshots, &client.ListOptions{ + Namespace: vm.Namespace, + }) + if err != nil { + return false, err + } + + for _, vmSnapshot := range vmSnapshots.Items { + if vmSnapshot.Spec.VirtualMachineName == vm.Name { + return false, nil + } + } + return true, nil } @@ -121,46 +134,18 @@ func (s *SnapshotService) Unfreeze(ctx context.Context, name, namespace string) return nil } -func (s *SnapshotService) CreateVolumeSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { - anno := make(map[string]string) - if pvc.Spec.StorageClassName != nil && *pvc.Spec.StorageClassName != "" { - anno["storageClass"] = *pvc.Spec.StorageClassName - accessModes := make([]string, 0, len(pvc.Status.AccessModes)) - for _, accessMode := range pvc.Status.AccessModes { - accessModes = append(accessModes, string(accessMode)) - } - - anno["accessModes"] = strings.Join(accessModes, ",") - } - - volumeSnapshot := vsv1.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: anno, - Name: vdSnapshot.Name, - Namespace: vdSnapshot.Namespace, - OwnerReferences: []metav1.OwnerReference{ - MakeOwnerReference(vdSnapshot), - }, - }, - Spec: vsv1.VolumeSnapshotSpec{ - Source: vsv1.VolumeSnapshotSource{ - PersistentVolumeClaimName: &pvc.Name, - }, - VolumeSnapshotClassName: &vdSnapshot.Spec.VolumeSnapshotClassName, - }, - } - - err := s.client.Create(ctx, &volumeSnapshot) +func (s *SnapshotService) CreateVolumeSnapshot(ctx context.Context, vs *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) { + err := s.client.Create(ctx, vs) if err != nil && !k8serrors.IsAlreadyExists(err) { return nil, err } - err = s.protection.AddProtection(ctx, &volumeSnapshot) + err = s.protection.AddProtection(ctx, vs) if err != nil { return nil, err } - return &volumeSnapshot, nil + return vs, nil } func (s *SnapshotService) DeleteVolumeSnapshot(ctx context.Context, vs *vsv1.VolumeSnapshot) error { @@ -177,12 +162,16 @@ func (s *SnapshotService) DeleteVolumeSnapshot(ctx context.Context, vs *vsv1.Vol return nil } +func (s *SnapshotService) GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualDisk{}) +} + func (s *SnapshotService) GetPersistentVolumeClaim(ctx context.Context, name, namespace string) (*corev1.PersistentVolumeClaim, error) { return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &corev1.PersistentVolumeClaim{}) } -func (s *SnapshotService) GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) { - return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualDisk{}) +func (s *SnapshotService) GetVirtualDiskSnapshot(ctx context.Context, name, namespace string) (*virtv2.VirtualDiskSnapshot, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualDiskSnapshot{}) } func (s *SnapshotService) GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) { @@ -192,3 +181,25 @@ func (s *SnapshotService) GetVirtualMachine(ctx context.Context, name, namespace func (s *SnapshotService) GetVolumeSnapshot(ctx context.Context, name, namespace string) (*vsv1.VolumeSnapshot, error) { return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &vsv1.VolumeSnapshot{}) } + +func (s *SnapshotService) GetSecret(ctx context.Context, name, namespace string) (*corev1.Secret, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &corev1.Secret{}) +} + +func (s *SnapshotService) CreateVirtualDiskSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (*virtv2.VirtualDiskSnapshot, error) { + err := s.client.Create(ctx, vdSnapshot) + if err != nil { + return nil, err + } + + return vdSnapshot, nil +} + +func (s *SnapshotService) CreateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { + err := s.client.Create(ctx, secret) + if err != nil { + return nil, err + } + + return secret, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/blank.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/blank.go index 925c58dd2..65dc552fd 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/blank.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/blank.go @@ -67,7 +67,7 @@ func (ds BlankDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) (boo switch { case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, &condition, &vd.Status.Phase, supgen) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go index e21d1daaa..95d508e92 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/http.go @@ -84,7 +84,7 @@ func (ds HTTPDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) (bool switch { case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, &condition, &vd.Status.Phase, supgen) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go index 32c726f93..c5c495725 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref.go @@ -102,7 +102,7 @@ func (ds ObjectRefDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) switch { case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, &condition, &vd.Status.Phase, supgen) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go index d61a688bd..8a6094601 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go @@ -60,7 +60,7 @@ func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, vd *virtv2.Virt switch { case isDiskProvisioningFinished(*condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, condition, &vd.Status.Phase, supgen) diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go index 630339eed..90add1b2a 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/registry.go @@ -90,7 +90,7 @@ func (ds RegistryDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) ( switch { case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, &condition, &vd.Status.Phase, supgen) 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 d8af1faa7..fc361257c 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/upload.go @@ -92,7 +92,7 @@ func (ds UploadDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) (bo switch { case isDiskProvisioningFinished(condition): - log.Info("Disk provisioning finished: clean up") + log.Debug("Disk provisioning finished: clean up") setPhaseConditionForFinishedDisk(pvc, &condition, &vd.Status.Phase, supgen) diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go index 0332e0b8b..580b08e62 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go @@ -37,7 +37,7 @@ type LifeCycleSnapshotter interface { CanFreeze(vm *virtv2.VirtualMachine) bool CanUnfreeze(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) Unfreeze(ctx context.Context, name, namespace string) error - CreateVolumeSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) + CreateVolumeSnapshot(ctx context.Context, vs *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) GetPersistentVolumeClaim(ctx context.Context, name, namespace string) (*corev1.PersistentVolumeClaim, error) GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go index d2bd10695..83e5eec58 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go @@ -19,7 +19,9 @@ package internal import ( "context" "fmt" + "strings" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -100,6 +102,8 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.Virtual return reconcile.Result{}, nil } + log.Debug("Process the virtual disk snapshot") + virtualDiskReadyCondition, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) if vd == nil || virtualDiskReadyCondition.Status != metav1.ConditionTrue { vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhasePending @@ -164,7 +168,35 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.Virtual log.Debug("The corresponding volume snapshot not found: create the new one") - vs, err = h.snapshotter.CreateVolumeSnapshot(ctx, vdSnapshot, pvc) + anno := make(map[string]string) + if pvc.Spec.StorageClassName != nil && *pvc.Spec.StorageClassName != "" { + anno["storageClass"] = *pvc.Spec.StorageClassName + accessModes := make([]string, 0, len(pvc.Status.AccessModes)) + for _, accessMode := range pvc.Status.AccessModes { + accessModes = append(accessModes, string(accessMode)) + } + + anno["accessModes"] = strings.Join(accessModes, ",") + } + + vs = &vsv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: anno, + Name: vdSnapshot.Name, + Namespace: vdSnapshot.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vdSnapshot), + }, + }, + Spec: vsv1.VolumeSnapshotSpec{ + Source: vsv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvc.Name, + }, + VolumeSnapshotClassName: &vdSnapshot.Spec.VolumeSnapshotClassName, + }, + } + + vs, err = h.snapshotter.CreateVolumeSnapshot(ctx, vs) if err != nil { setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) return reconcile.Result{}, err diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle_test.go index 3e8cd27bb..56bd06717 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle_test.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle_test.go @@ -73,7 +73,7 @@ var _ = Describe("LifeCycle handler", func() { } snapshotter = &LifeCycleSnapshotterMock{ - CreateVolumeSnapshotFunc: func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { + CreateVolumeSnapshotFunc: func(_ context.Context, _ *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) { return vs, nil }, GetPersistentVolumeClaimFunc: func(_ context.Context, _, _ string) (*corev1.PersistentVolumeClaim, error) { diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go index d8ab7b734..0b39d6b23 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go @@ -105,7 +105,7 @@ var _ LifeCycleSnapshotter = &LifeCycleSnapshotterMock{} // CanUnfreezeFunc: func(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) { // panic("mock out the CanUnfreeze method") // }, -// CreateVolumeSnapshotFunc: func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { +// CreateVolumeSnapshotFunc: func(ctx context.Context, vs *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) { // panic("mock out the CreateVolumeSnapshot method") // }, // FreezeFunc: func(ctx context.Context, name string, namespace string) error { @@ -143,7 +143,7 @@ type LifeCycleSnapshotterMock struct { CanUnfreezeFunc func(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) // CreateVolumeSnapshotFunc mocks the CreateVolumeSnapshot method. - CreateVolumeSnapshotFunc func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) + CreateVolumeSnapshotFunc func(ctx context.Context, vs *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) // FreezeFunc mocks the Freeze method. FreezeFunc func(ctx context.Context, name string, namespace string) error @@ -186,10 +186,8 @@ type LifeCycleSnapshotterMock struct { CreateVolumeSnapshot []struct { // Ctx is the ctx argument value. Ctx context.Context - // VdSnapshot is the vdSnapshot argument value. - VdSnapshot *virtv2.VirtualDiskSnapshot - // Pvc is the pvc argument value. - Pvc *corev1.PersistentVolumeClaim + // Vs is the vs argument value. + Vs *vsv1.VolumeSnapshot } // Freeze holds details about calls to the Freeze method. Freeze []struct { @@ -336,23 +334,21 @@ func (mock *LifeCycleSnapshotterMock) CanUnfreezeCalls() []struct { } // CreateVolumeSnapshot calls CreateVolumeSnapshotFunc. -func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { +func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshot(ctx context.Context, vs *vsv1.VolumeSnapshot) (*vsv1.VolumeSnapshot, error) { if mock.CreateVolumeSnapshotFunc == nil { panic("LifeCycleSnapshotterMock.CreateVolumeSnapshotFunc: method is nil but LifeCycleSnapshotter.CreateVolumeSnapshot was just called") } callInfo := struct { - Ctx context.Context - VdSnapshot *virtv2.VirtualDiskSnapshot - Pvc *corev1.PersistentVolumeClaim + Ctx context.Context + Vs *vsv1.VolumeSnapshot }{ - Ctx: ctx, - VdSnapshot: vdSnapshot, - Pvc: pvc, + Ctx: ctx, + Vs: vs, } mock.lockCreateVolumeSnapshot.Lock() mock.calls.CreateVolumeSnapshot = append(mock.calls.CreateVolumeSnapshot, callInfo) mock.lockCreateVolumeSnapshot.Unlock() - return mock.CreateVolumeSnapshotFunc(ctx, vdSnapshot, pvc) + return mock.CreateVolumeSnapshotFunc(ctx, vs) } // CreateVolumeSnapshotCalls gets all the calls that were made to CreateVolumeSnapshot. @@ -360,14 +356,12 @@ func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshot(ctx context.Context, // // len(mockedLifeCycleSnapshotter.CreateVolumeSnapshotCalls()) func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshotCalls() []struct { - Ctx context.Context - VdSnapshot *virtv2.VirtualDiskSnapshot - Pvc *corev1.PersistentVolumeClaim + Ctx context.Context + Vs *vsv1.VolumeSnapshot } { var calls []struct { - Ctx context.Context - VdSnapshot *virtv2.VirtualDiskSnapshot - Pvc *corev1.PersistentVolumeClaim + Ctx context.Context + Vs *vsv1.VolumeSnapshot } mock.lockCreateVolumeSnapshot.RLock() calls = mock.calls.CreateVolumeSnapshot diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go index 0752ecc42..1250e5d30 100644 --- a/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go @@ -34,7 +34,7 @@ import ( ) type Handler interface { - Handle(ctx context.Context, vdsnapshot *virtv2.VirtualDiskSnapshot) (reconcile.Result, error) + Handle(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (reconcile.Result, error) } type Watcher interface { @@ -56,14 +56,14 @@ func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := logger.FromContext(ctx) - vdsnapshot := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + vdSnapshot := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) - err := vdsnapshot.Fetch(ctx) + err := vdSnapshot.Fetch(ctx) if err != nil { return reconcile.Result{}, err } - if vdsnapshot.IsEmpty() { + if vdSnapshot.IsEmpty() { return reconcile.Result{}, nil } @@ -72,18 +72,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco for _, h := range r.handlers { var res reconcile.Result - res, err = h.Handle(ctx, vdsnapshot.Changed()) + res, err = h.Handle(ctx, vdSnapshot.Changed()) if err != nil { - log.Error("Failed to handle vdsnapshot", logger.SlogErr(err), logger.SlogHandler(reflect.TypeOf(h).Elem().Name())) + log.Error("Failed to handle vdSnapshot", logger.SlogErr(err), logger.SlogHandler(reflect.TypeOf(h).Elem().Name())) handlerErrs = append(handlerErrs, err) } result = service.MergeResults(result, res) } - vdsnapshot.Changed().Status.ObservedGeneration = vdsnapshot.Changed().Generation + vdSnapshot.Changed().Status.ObservedGeneration = vdSnapshot.Changed().Generation - err = vdsnapshot.Update(ctx) + err = vdSnapshot.Update(ctx) if err != nil { return reconcile.Result{}, err } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/snapshotting.go b/images/virtualization-artifact/pkg/controller/vm/internal/snapshotting.go new file mode 100644 index 000000000..705bb1b7a --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/snapshotting.go @@ -0,0 +1,95 @@ +/* +Copyright 2024 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" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/controller/vm/internal/state" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +const nameSnapshottingHandler = "snapshotting" + +func NewSnapshottingHandler(client client.Client) *SnapshottingHandler { + return &SnapshottingHandler{client: client} +} + +type SnapshottingHandler struct { + client client.Client +} + +func (h *SnapshottingHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) { + if s.VirtualMachine().IsEmpty() { + return reconcile.Result{}, nil + } + + vm := s.VirtualMachine().Changed() + + if update := addAllUnknown(vm, vmcondition.TypeSnapshotting); update { + return reconcile.Result{Requeue: true}, nil + } + + if isDeletion(vm) { + return reconcile.Result{}, nil + } + + var vmSnapshots virtv2.VirtualMachineSnapshotList + err := h.client.List(ctx, &vmSnapshots, client.InNamespace(vm.Namespace)) + if err != nil { + return reconcile.Result{}, err + } + + cb := conditions.NewConditionBuilder(vmcondition.TypeSnapshotting).Generation(vm.GetGeneration()) + + defer func() { conditions.SetCondition(cb, &vm.Status.Conditions) }() + + cb.Status(metav1.ConditionUnknown) + + for _, vmSnapshot := range vmSnapshots.Items { + if vmSnapshot.Spec.VirtualMachineName != vm.Name { + continue + } + + switch vmSnapshot.Status.Phase { + case virtv2.VirtualMachineSnapshotPhaseReady, virtv2.VirtualMachineSnapshotPhaseTerminating: + continue + case virtv2.VirtualMachineSnapshotPhaseInProgress: + cb.Status(metav1.ConditionTrue). + Message("The virtual machine is the process of snapshotting."). + Reason(vmcondition.ReasonSnapshottingInProgress) + return reconcile.Result{}, nil + default: + cb.Status(metav1.ConditionTrue). + Message("The virtual machine is selected for taking a snapshot."). + Reason(vmcondition.ReasonChosenForSnapshotting) + continue + } + } + + return reconcile.Result{}, nil +} + +func (h *SnapshottingHandler) Name() string { + return nameSnapshottingHandler +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 51be2b9ce..dae2f396a 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -123,6 +123,10 @@ func (h *SyncKvvmHandler) isWaiting(vm *virtv2.VirtualMachine) bool { if c.Status != metav1.ConditionTrue && c.Reason != vmcondition.ReasonWaitingForProvisioningToPVC.String() { return true } + case vmcondition.TypeSnapshotting: + if c.Status == metav1.ConditionTrue && c.Reason == vmcondition.ReasonSnapshottingInProgress.String() { + return true + } case vmcondition.TypeIPAddressReady, vmcondition.TypeProvisioningReady, vmcondition.TypeClassReady: diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/watchers/vmsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vm/internal/watchers/vmsnapshot_watcher.go new file mode 100644 index 000000000..b71fbb880 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/watchers/vmsnapshot_watcher.go @@ -0,0 +1,104 @@ +/* +Copyright 2024 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 watcher + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineSnapshotWatcher struct { + client client.Client +} + +func NewVirtualMachineSnapshotWatcher(client client.Client) *VirtualMachineSnapshotWatcher { + return &VirtualMachineSnapshotWatcher{ + client: client, + } +} + +func (w VirtualMachineSnapshotWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineSnapshot{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualMachineSnapshotWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vmSnapshot, ok := obj.(*virtv2.VirtualMachineSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a VirtualMachineSnapshot but got a %T", obj)) + return nil + } + + var vms virtv2.VirtualMachineList + // TODO use index. + err := w.client.List(ctx, &vms, &client.ListOptions{ + Namespace: vmSnapshot.Namespace, + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual machines: %s", err)) + return + } + + for _, vm := range vms.Items { + if vmSnapshot.Spec.VirtualMachineName == vm.Name { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vm.Name, + Namespace: vm.Namespace, + }, + }) + } + } + + return +} + +func (w VirtualMachineSnapshotWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVMSnapshot, ok := e.ObjectOld.(*virtv2.VirtualMachineSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an old VirtualMachineSnapshot but got a %T", e.ObjectOld)) + return false + } + + newVMSnapshot, ok := e.ObjectNew.(*virtv2.VirtualMachineSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a new VirtualMachineSnapshot but got a %T", e.ObjectNew)) + return false + } + + return oldVMSnapshot.Status.Phase != newVMSnapshot.Status.Phase +} diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go index 62e6cf38b..507cbe9e1 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go @@ -56,6 +56,7 @@ func SetupController( internal.NewProvisioningHandler(client), internal.NewAgentHandler(), internal.NewFilesystemHandler(), + internal.NewSnapshottingHandler(client), internal.NewPodHandler(client), internal.NewSyncKvvmHandler(dvcrSettings, client, recorder), internal.NewSyncMetadataHandler(client), diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index eeab5680d..2c7795e28 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -38,6 +38,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/service" "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/watcher" + watcher "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/watchers" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -245,6 +246,12 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr return fmt.Errorf("error setting watch on VirtualMachineClass SizePolicy: %w", err) } + w := watcher.NewVirtualMachineSnapshotWatcher(mgr.GetClient()) + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("faield to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) + } + return nil } diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/deletion.go new file mode 100644 index 000000000..5f77735cb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/deletion.go @@ -0,0 +1,42 @@ +/* +Copyright 2024 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" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type DeletionHandler struct{} + +func NewDeletionHandler() *DeletionHandler { + return &DeletionHandler{} +} + +func (h DeletionHandler) Handle(_ context.Context, vmSnapshot *virtv2.VirtualMachineSnapshot) (reconcile.Result, error) { + if vmSnapshot.DeletionTimestamp == nil { + controllerutil.AddFinalizer(vmSnapshot, virtv2.FinalizerVMSnapshotCleanup) + return reconcile.Result{}, nil + } + + controllerutil.RemoveFinalizer(vmSnapshot, virtv2.FinalizerVMSnapshotCleanup) + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/handler_test.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/handler_test.go new file mode 100644 index 000000000..f4f370fc1 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/handler_test.go @@ -0,0 +1,37 @@ +/* +Copyright 2024 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" + "log/slog" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/virtualization-controller/pkg/logger" +) + +func TestHandlers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Handlers") +} + +func testContext() context.Context { + return logger.ToContext(context.Background(), slog.Default()) +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/interfaces.go new file mode 100644 index 000000000..885ab7515 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/interfaces.go @@ -0,0 +1,46 @@ +/* +Copyright 2024 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" + + corev1 "k8s.io/api/core/v1" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +//go:generate moq -rm -out mock.go . SecretBuilderSnapshotter LifeCycleSnapshotter + +type SecretBuilderSnapshotter interface { + Build(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) (*corev1.Secret, error) +} + +type LifeCycleSnapshotter interface { + GetSecret(ctx context.Context, name, namespace string) (*corev1.Secret, error) + CreateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) + GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) + GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) + GetPersistentVolumeClaim(ctx context.Context, name, namespace string) (*corev1.PersistentVolumeClaim, error) + GetVirtualDiskSnapshot(ctx context.Context, name, namespace string) (*virtv2.VirtualDiskSnapshot, error) + CreateVirtualDiskSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (*virtv2.VirtualDiskSnapshot, error) + Freeze(ctx context.Context, name, namespace string) error + Unfreeze(ctx context.Context, name, namespace string) error + IsFrozen(vm *virtv2.VirtualMachine) bool + CanFreeze(vm *virtv2.VirtualMachine) bool + CanUnfreeze(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go new file mode 100644 index 000000000..6b30745e8 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go @@ -0,0 +1,490 @@ +/* +Copyright 2024 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" + "errors" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmscondition" +) + +type LifeCycleHandler struct { + snapshotter LifeCycleSnapshotter + builder SecretBuilderSnapshotter +} + +func NewLifeCycleHandler(snapshotter LifeCycleSnapshotter, builder SecretBuilderSnapshotter) *LifeCycleHandler { + return &LifeCycleHandler{ + snapshotter: snapshotter, + builder: builder, + } +} + +func (h LifeCycleHandler) Handle(ctx context.Context, vmSnapshot *virtv2.VirtualMachineSnapshot) (reconcile.Result, error) { + log := logger.FromContext(ctx).With(logger.SlogHandler("lifecycle")) + + condition, ok := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vmscondition.VirtualMachineSnapshotReadyType, + Status: metav1.ConditionUnknown, + Reason: vmscondition.VirtualMachineSnapshotUnknown, + } + } + + defer func() { service.SetCondition(condition, &vmSnapshot.Status.Conditions) }() + + if vmSnapshot.DeletionTimestamp != nil { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseTerminating + condition.Status = metav1.ConditionUnknown + condition.Reason = vmscondition.VirtualMachineSnapshotUnknown + condition.Message = "" + + return reconcile.Result{}, nil + } + + switch vmSnapshot.Status.Phase { + case "": + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + case virtv2.VirtualMachineSnapshotPhaseReady: + // Ensure vd snapshots aren't lost. + var lostVDSnapshots []string + for _, vdSnapshotName := range vmSnapshot.Status.VirtualDiskSnapshotNames { + vdSnapshot, err := h.snapshotter.GetVirtualDiskSnapshot(ctx, vdSnapshotName, vmSnapshot.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + switch { + case vdSnapshot == nil: + lostVDSnapshots = append(lostVDSnapshots, vdSnapshotName) + case vdSnapshot.Status.Phase != virtv2.VirtualDiskSnapshotPhaseReady: + log.Error("expected virtual disk snapshot to be ready, please report a bug", "vdSnapshotPhase", vdSnapshot.Status.Phase) + } + } + + if len(lostVDSnapshots) > 0 { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseFailed + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.VirtualDiskSnapshotLost + if len(lostVDSnapshots) == 1 { + condition.Message = fmt.Sprintf("The underlieng virtual disk snapshot (%s) is lost.", lostVDSnapshots[0]) + } else { + condition.Message = fmt.Sprintf("The underlieng virtual disk snapshots (%s) are lost.", strings.Join(lostVDSnapshots, ", ")) + } + return reconcile.Result{}, nil + } + + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseReady + condition.Status = metav1.ConditionTrue + condition.Reason = vmscondition.VirtualMachineSnapshotReady + condition.Message = "" + return reconcile.Result{}, nil + } + + log.Debug("Process the virtual machine snapshot") + + vm, err := h.snapshotter.GetVirtualMachine(ctx, vmSnapshot.Spec.VirtualMachineName, vmSnapshot.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + virtualMachineReadyCondition, _ := service.GetCondition(vmscondition.VirtualMachineReadyType, vmSnapshot.Status.Conditions) + if vm == nil || virtualMachineReadyCondition.Status != metav1.ConditionTrue { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.WaitingForTheVirtualMachine + condition.Message = fmt.Sprintf("Waiting for the virtual machine %q to be ready for snapshotting.", vmSnapshot.Spec.VirtualMachineName) + return reconcile.Result{}, nil + } + + // 1. Ensure the block devices are Ready for snapshotting. + err = h.ensureBlockDeviceConsistency(ctx, vm) + switch { + case err == nil: + case errors.Is(err, ErrBlockDevicesNotReady), errors.Is(err, ErrVirtualDiskNotReady), errors.Is(err, ErrVirtualDiskResizing): + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.BlockDevicesNotReady + condition.Message = service.CapitalizeFirstLetter(err.Error()) + "." + return reconcile.Result{}, nil + default: + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + // 2. Ensure there are no RestartAwaitingChanges. + if len(vm.Status.RestartAwaitingChanges) > 0 { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.RestartAwaitingChanges + condition.Message = fmt.Sprintf( + "Waiting for the restart and approval of changes to virtual machine %q before taking the snapshot.", + vm.Name, + ) + return reconcile.Result{}, nil + } + + // 3. Ensure the virtual machine is consistent for snapshotting. + hasFrozen, err := h.freezeVirtualMachineIfCan(ctx, vm) + switch { + case err == nil: + case errors.Is(err, ErrPotentiallyInconsistent): + if vmSnapshot.Spec.RequiredConsistency { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.PotentiallyInconsistent + condition.Message = fmt.Sprintf( + "The snapshotting of virtual machine %q might result in an inconsistent snapshot: "+ + "waiting for the virtual machine to be %s", + vm.Name, virtv2.MachineStopped, + ) + return reconcile.Result{}, nil + } + default: + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + if hasFrozen { + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.FileSystemFreezing + condition.Message = fmt.Sprintf("The virtual machine %q is in the process of being frozen for taking a snapshot.", vm.Name) + return reconcile.Result{}, nil + } + + // 4. Create secret. + err = h.ensureSecret(ctx, vm, vmSnapshot) + if err != nil { + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + // 5. Fill status.VirtualDiskSnapshotNames. + h.fillStatusVirtualDiskSnapshotNames(vmSnapshot, vm) + + // 6. Get or Create VirtualDiskSnapshots. + vdSnapshots, err := h.ensureVirtualDiskSnapshots(ctx, vmSnapshot) + switch { + case err == nil: + case errors.Is(err, ErrVolumeSnapshotClassNotFound): + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.BlockDevicesNotReady + condition.Message = service.CapitalizeFirstLetter(err.Error()) + "." + return reconcile.Result{}, nil + default: + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + // 7. Wait for VirtualDiskSnapshots to be Ready. + readyCount := h.countReadyVirtualDiskSnapshots(vdSnapshots) + + if readyCount != len(vdSnapshots) { + log.Debug("Waiting for the virtual disk snapshots to be taken for the block devices of the virtual machine") + + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.Snapshotting + condition.Message = fmt.Sprintf( + "Waiting for the virtual disk snapshots to be taken for the block devices of the virtual machine %q (%d/%d).", + vm.Name, readyCount, len(vdSnapshots), + ) + return reconcile.Result{}, nil + } + + if vm.Status.Phase == virtv2.MachineStopped || h.snapshotter.IsFrozen(vm) { + vmSnapshot.Status.Consistent = ptr.To(true) + } + + // 8. Unfreeze VirtualMachine if can. + unfrozen, err := h.unfreezeVirtualMachineIfCan(ctx, vm) + if err != nil { + setPhaseConditionToFailed(&condition, &vmSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + // 9. Move to Ready phase. + log.Debug("The virtual disk snapshots are taken: the virtual machine snapshot is Ready now", "unfrozen", unfrozen) + + vmSnapshot.Status.Phase = virtv2.VirtualMachineSnapshotPhaseReady + condition.Status = metav1.ConditionTrue + condition.Reason = vmscondition.VirtualMachineReady + condition.Message = "" + + return reconcile.Result{}, nil +} + +func setPhaseConditionToFailed(cond *metav1.Condition, phase *virtv2.VirtualMachineSnapshotPhase, err error) { + *phase = virtv2.VirtualMachineSnapshotPhaseFailed + cond.Status = metav1.ConditionFalse + cond.Reason = vmscondition.VirtualMachineSnapshotFailed + cond.Message = service.CapitalizeFirstLetter(err.Error()) +} + +func (h LifeCycleHandler) fillStatusVirtualDiskSnapshotNames(vmSnapshot *virtv2.VirtualMachineSnapshot, vm *virtv2.VirtualMachine) { + vmSnapshot.Status.VirtualDiskSnapshotNames = nil + + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice { + continue + } + + vmSnapshot.Status.VirtualDiskSnapshotNames = append( + vmSnapshot.Status.VirtualDiskSnapshotNames, + getVDSnapshotName(bdr.Name, vmSnapshot), + ) + } +} + +func (h LifeCycleHandler) ensureVirtualDiskSnapshots(ctx context.Context, vmSnapshot *virtv2.VirtualMachineSnapshot) ([]*virtv2.VirtualDiskSnapshot, error) { + vdSnapshots := make([]*virtv2.VirtualDiskSnapshot, 0, len(vmSnapshot.Status.VirtualDiskSnapshotNames)) + + for _, vdSnapshotName := range vmSnapshot.Status.VirtualDiskSnapshotNames { + vdSnapshot, err := h.snapshotter.GetVirtualDiskSnapshot(ctx, vdSnapshotName, vmSnapshot.Namespace) + if err != nil { + return nil, err + } + + if vdSnapshot == nil { + vdName, ok := getVDName(vdSnapshotName, vmSnapshot) + if !ok { + return nil, fmt.Errorf("failed to get VirtualDisk's name from VirtualDiskSnapshot's name %q", vdSnapshotName) + } + + var vd *virtv2.VirtualDisk + vd, err = h.snapshotter.GetVirtualDisk(ctx, vdName, vmSnapshot.Namespace) + if err != nil { + return nil, err + } + + if vd == nil { + return nil, fmt.Errorf("the virtual disk %q not found", vdName) + } + + var pvc *corev1.PersistentVolumeClaim + pvc, err = h.snapshotter.GetPersistentVolumeClaim(ctx, vd.Status.Target.PersistentVolumeClaim, vd.Namespace) + if err != nil { + return nil, err + } + + if pvc == nil { + return nil, fmt.Errorf("the persistent volume claim %q not found for the virtual disk %q", vd.Status.Target.PersistentVolumeClaim, vd.Name) + } + + if pvc.Spec.StorageClassName == nil || *pvc.Spec.StorageClassName == "" { + return nil, fmt.Errorf("the persistent volume claim %q doesn't have the storage class name", pvc.Name) + } + + var vsClass string + vsClass, err = h.getVolumeSnapshotClassByStorageClass(*pvc.Spec.StorageClassName, vmSnapshot.Spec.VolumeSnapshotClassNames) + if err != nil { + return nil, err + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + TypeMeta: metav1.TypeMeta{ + Kind: virtv2.VirtualDiskSnapshotKind, + APIVersion: virtv2.Version, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: vdSnapshotName, + Namespace: vmSnapshot.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vmSnapshot), + }, + }, + Spec: virtv2.VirtualDiskSnapshotSpec{ + VirtualDiskName: vdName, + VolumeSnapshotClassName: vsClass, + RequiredConsistency: vmSnapshot.Spec.RequiredConsistency, + }, + } + + vdSnapshot, err = h.snapshotter.CreateVirtualDiskSnapshot(ctx, vdSnapshot) + if err != nil { + return nil, err + } + } + + vdSnapshots = append(vdSnapshots, vdSnapshot) + } + + return vdSnapshots, nil +} + +func (h LifeCycleHandler) countReadyVirtualDiskSnapshots(vdSnapshots []*virtv2.VirtualDiskSnapshot) int { + var readyCount int + for _, vdSnapshot := range vdSnapshots { + if vdSnapshot.Status.Phase == virtv2.VirtualDiskSnapshotPhaseReady { + readyCount++ + } + } + + return readyCount +} + +var ErrPotentiallyInconsistent = errors.New("potentially inconsistent") + +func (h LifeCycleHandler) freezeVirtualMachineIfCan(ctx context.Context, vm *virtv2.VirtualMachine) (bool, error) { + switch vm.Status.Phase { + case virtv2.MachineStopped: + return false, nil + case virtv2.MachineRunning: + default: + return false, errors.New("cannot freeze not Running virtual machine") + } + + if h.snapshotter.IsFrozen(vm) { + return false, nil + } + + if !h.snapshotter.CanFreeze(vm) { + return false, ErrPotentiallyInconsistent + } + + err := h.snapshotter.Freeze(ctx, vm.Name, vm.Namespace) + if err != nil { + return false, err + } + + return true, nil +} + +func (h LifeCycleHandler) unfreezeVirtualMachineIfCan(ctx context.Context, vm *virtv2.VirtualMachine) (bool, error) { + if vm.Status.Phase != virtv2.MachineRunning || !h.snapshotter.IsFrozen(vm) { + return false, nil + } + + canUnfreeze, err := h.snapshotter.CanUnfreeze(ctx, "", vm) + if err != nil { + return false, err + } + + if !canUnfreeze { + return false, nil + } + + err = h.snapshotter.Unfreeze(ctx, vm.Name, vm.Namespace) + if err != nil { + return false, err + } + + return true, nil +} + +var ( + ErrBlockDevicesNotReady = errors.New("block devices not ready") + ErrVirtualDiskNotReady = errors.New("virtual disk not ready") + ErrVirtualDiskResizing = errors.New("virtual disk is in the process of resizing") +) + +func (h LifeCycleHandler) ensureBlockDeviceConsistency(ctx context.Context, vm *virtv2.VirtualMachine) error { + bdReady, _ := service.GetCondition(vmcondition.TypeBlockDevicesReady.String(), vm.Status.Conditions) + if bdReady.Status != metav1.ConditionTrue { + return fmt.Errorf("%w: waiting for the block devices of the virtual machine %q to be ready", ErrBlockDevicesNotReady, vm.Name) + } + + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice { + continue + } + + vd, err := h.snapshotter.GetVirtualDisk(ctx, bdr.Name, vm.Namespace) + if err != nil { + return err + } + + if vd.Status.Phase != virtv2.DiskReady { + return fmt.Errorf("%w: waiting for the virtual disk %q to be %s", ErrVirtualDiskNotReady, vd.Name, virtv2.DiskReady) + } + + ready, _ := service.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if ready.Status != metav1.ConditionTrue { + return fmt.Errorf("%w: waiting for the Ready condition of the virtual disk %q to be True", ErrVirtualDiskResizing, vd.Name) + } + + resizingReady, _ := service.GetCondition(vdcondition.ResizedType, vd.Status.Conditions) + if resizingReady.Reason == vdcondition.InProgress { + return fmt.Errorf("%w: waiting for the virtual disk %q to be resized", ErrVirtualDiskResizing, vd.Name) + } + } + + return nil +} + +func (h LifeCycleHandler) ensureSecret(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) error { + if vmSnapshot.Status.VirtualMachineSnapshotSecretName == "" { + vmSnapshot.Status.VirtualMachineSnapshotSecretName = vmSnapshot.Name + } + + secret, err := h.snapshotter.GetSecret(ctx, vmSnapshot.Status.VirtualMachineSnapshotSecretName, vmSnapshot.Namespace) + if err != nil { + return err + } + + if secret == nil { + secret, err = h.builder.Build(ctx, vm, vmSnapshot) + if err != nil { + return err + } + + _, err = h.snapshotter.CreateSecret(ctx, secret) + if err != nil { + return err + } + } + + return nil +} + +var ErrVolumeSnapshotClassNotFound = errors.New("the volume snapshot class not found") + +func (h LifeCycleHandler) getVolumeSnapshotClassByStorageClass(storageClassName string, classes []virtv2.VolumeSnapshotClassName) (string, error) { + for _, class := range classes { + if class.StorageClassName == storageClassName { + return class.VolumeSnapshotClassName, nil + } + } + + return "", fmt.Errorf("%w: please define the volume snapshot class for the storage class %q", ErrVolumeSnapshotClassNotFound, storageClassName) +} + +func getVDName(vdSnapshotName string, vmSnapshot *virtv2.VirtualMachineSnapshot) (string, bool) { + return strings.CutSuffix(vdSnapshotName, "-"+string(vmSnapshot.UID)) +} + +func getVDSnapshotName(vdName string, vmSnapshot *virtv2.VirtualMachineSnapshot) string { + return fmt.Sprintf("%s-%s", vdName, vmSnapshot.UID) +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle_test.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle_test.go new file mode 100644 index 000000000..6e475fa07 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle_test.go @@ -0,0 +1,329 @@ +/* +Copyright 2024 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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmscondition" +) + +var _ = Describe("LifeCycle handler", func() { + var snapshotter *LifeCycleSnapshotterMock + var secretBuilder *SecretBuilderSnapshotterMock + var vd *virtv2.VirtualDisk + var vm *virtv2.VirtualMachine + var secret *corev1.Secret + var vdSnapshot *virtv2.VirtualDiskSnapshot + var vmSnapshot *virtv2.VirtualMachineSnapshot + + BeforeEach(func() { + vd = &virtv2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{Name: "vd-bar"}, + Status: virtv2.VirtualDiskStatus{ + Phase: virtv2.DiskReady, + Conditions: []metav1.Condition{ + { + Type: vdcondition.Ready, + Status: metav1.ConditionTrue, + }, + { + Type: vdcondition.Resized, + Status: metav1.ConditionTrue, + Reason: vdcondition.Resized, + }, + }, + }, + } + + vm = &virtv2.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{Name: "vm"}, + Spec: virtv2.VirtualMachineSpec{ + BlockDeviceRefs: []virtv2.BlockDeviceSpecRef{ + { + Kind: virtv2.DiskDevice, + Name: vd.Name, + }, + }, + }, + Status: virtv2.VirtualMachineStatus{ + Phase: virtv2.MachineRunning, + BlockDeviceRefs: []virtv2.BlockDeviceStatusRef{ + { + Kind: virtv2.DiskDevice, + Name: vd.Name, + }, + }, + Conditions: []metav1.Condition{ + { + Type: vmcondition.TypeBlockDevicesReady.String(), + Status: metav1.ConditionTrue, + }, + }, + }, + } + + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: vm.Name}, + } + + vmSnapshot = &virtv2.VirtualMachineSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vm-snapshot"}, + Spec: virtv2.VirtualMachineSnapshotSpec{ + VirtualMachineName: vm.Name, + RequiredConsistency: true, + }, + Status: virtv2.VirtualMachineSnapshotStatus{ + Conditions: []metav1.Condition{ + { + Type: vmscondition.VirtualMachineReadyType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: getVDSnapshotName(vd.Name, vmSnapshot)}, + Status: virtv2.VirtualDiskSnapshotStatus{ + Phase: virtv2.VirtualDiskSnapshotPhaseReady, + }, + } + + snapshotter = &LifeCycleSnapshotterMock{ + GetVirtualDiskFunc: func(_ context.Context, name, namespace string) (*virtv2.VirtualDisk, error) { + return vd, nil + }, + GetVirtualMachineFunc: func(_ context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + return vm, nil + }, + IsFrozenFunc: func(_ *virtv2.VirtualMachine) bool { + return true + }, + CanUnfreezeFunc: func(_ context.Context, _ string, _ *virtv2.VirtualMachine) (bool, error) { + return true, nil + }, + UnfreezeFunc: func(ctx context.Context, _, _ string) error { + return nil + }, + GetSecretFunc: func(_ context.Context, _, _ string) (*corev1.Secret, error) { + return secret, nil + }, + GetVirtualDiskSnapshotFunc: func(_ context.Context, _, _ string) (*virtv2.VirtualDiskSnapshot, error) { + return vdSnapshot, nil + }, + } + }) + + Context("The block devices of the virtual machine are not in the consistent state", func() { + It("The BlockDevicesReady condition of the virtual machine isn't True", func() { + snapshotter.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + service.SetCondition(metav1.Condition{ + Type: vmcondition.TypeBlockDevicesReady.String(), + Status: metav1.ConditionFalse, + }, &vm.Status.Conditions) + return vm, nil + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.BlockDevicesNotReady)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is Pending", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + vd.Status.Phase = virtv2.DiskPending + return vd, nil + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.BlockDevicesNotReady)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is not Ready", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + service.SetCondition(metav1.Condition{ + Type: vdcondition.Ready, + Status: metav1.ConditionFalse, + }, &vd.Status.Conditions) + return vd, nil + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.BlockDevicesNotReady)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is the process of Resizing", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + service.SetCondition(metav1.Condition{ + Type: vdcondition.Resized, + Status: metav1.ConditionFalse, + Reason: vdcondition.InProgress, + }, &vd.Status.Conditions) + return vd, nil + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.BlockDevicesNotReady)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + }) + + Context("Ensure the virtual machine consistency", func() { + It("The virtual machine has RestartAwaitingChanges", func() { + snapshotter.GetVirtualMachineFunc = func(ctx context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + vm.Status.RestartAwaitingChanges = []apiextensionsv1.JSON{{}, {}} + return vm, nil + } + + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.RestartAwaitingChanges)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual machine is potentially inconsistent", func() { + snapshotter.IsFrozenFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhasePending)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.PotentiallyInconsistent)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual machine has frozen", func() { + snapshotter.IsFrozenFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return true + } + snapshotter.FreezeFunc = func(_ context.Context, _, _ string) error { + return nil + } + + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vmscondition.FileSystemFreezing)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + }) + + Context("The virtual machine snapshot is Ready", func() { + It("The snapshot of virtual machine is Ready", func() { + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Phase).To(Equal(virtv2.VirtualMachineSnapshotPhaseReady)) + ready, _ := service.GetCondition(vmscondition.VirtualMachineSnapshotReadyType, vmSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionTrue)) + Expect(ready.Reason).To(Equal(vmscondition.VirtualMachineReady)) + Expect(ready.Message).To(BeEmpty()) + + Expect(vmSnapshot.Status.VirtualDiskSnapshotNames[0]).To(Equal(vdSnapshot.Name)) + }) + + It("The snapshot of running virtual machine is consistent", func() { + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Consistent).To(Equal(ptr.To(true))) + }) + + It("The snapshot of stopped virtual machine is consistent", func() { + snapshotter.GetVirtualMachineFunc = func(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) { + vm.Status.Phase = virtv2.MachineStopped + return vm, nil + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Consistent).To(Equal(ptr.To(true))) + }) + + It("The virtual machine snapshot is potentially inconsistent", func() { + vmSnapshot.Spec.RequiredConsistency = false + snapshotter.IsFrozenFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + h := NewLifeCycleHandler(snapshotter, secretBuilder) + + _, err := h.Handle(testContext(), vmSnapshot) + Expect(err).To(BeNil()) + Expect(vmSnapshot.Status.Consistent).To(BeNil()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/mock.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/mock.go new file mode 100644 index 000000000..f9028760c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/mock.go @@ -0,0 +1,747 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package internal + +import ( + "context" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + corev1 "k8s.io/api/core/v1" + "sync" +) + +// Ensure, that SecretBuilderSnapshotterMock does implement SecretBuilderSnapshotter. +// If this is not the case, regenerate this file with moq. +var _ SecretBuilderSnapshotter = &SecretBuilderSnapshotterMock{} + +// SecretBuilderSnapshotterMock is a mock implementation of SecretBuilderSnapshotter. +// +// func TestSomethingThatUsesSecretBuilderSnapshotter(t *testing.T) { +// +// // make and configure a mocked SecretBuilderSnapshotter +// mockedSecretBuilderSnapshotter := &SecretBuilderSnapshotterMock{ +// BuildFunc: func(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) (*corev1.Secret, error) { +// panic("mock out the Build method") +// }, +// } +// +// // use mockedSecretBuilderSnapshotter in code that requires SecretBuilderSnapshotter +// // and then make assertions. +// +// } +type SecretBuilderSnapshotterMock struct { + // BuildFunc mocks the Build method. + BuildFunc func(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) (*corev1.Secret, error) + + // calls tracks calls to the methods. + calls struct { + // Build holds details about calls to the Build method. + Build []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // VM is the vm argument value. + VM *virtv2.VirtualMachine + // VmSnapshot is the vmSnapshot argument value. + VmSnapshot *virtv2.VirtualMachineSnapshot + } + } + lockBuild sync.RWMutex +} + +// Build calls BuildFunc. +func (mock *SecretBuilderSnapshotterMock) Build(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) (*corev1.Secret, error) { + if mock.BuildFunc == nil { + panic("SecretBuilderSnapshotterMock.BuildFunc: method is nil but SecretBuilderSnapshotter.Build was just called") + } + callInfo := struct { + Ctx context.Context + VM *virtv2.VirtualMachine + VmSnapshot *virtv2.VirtualMachineSnapshot + }{ + Ctx: ctx, + VM: vm, + VmSnapshot: vmSnapshot, + } + mock.lockBuild.Lock() + mock.calls.Build = append(mock.calls.Build, callInfo) + mock.lockBuild.Unlock() + return mock.BuildFunc(ctx, vm, vmSnapshot) +} + +// BuildCalls gets all the calls that were made to Build. +// Check the length with: +// +// len(mockedSecretBuilderSnapshotter.BuildCalls()) +func (mock *SecretBuilderSnapshotterMock) BuildCalls() []struct { + Ctx context.Context + VM *virtv2.VirtualMachine + VmSnapshot *virtv2.VirtualMachineSnapshot +} { + var calls []struct { + Ctx context.Context + VM *virtv2.VirtualMachine + VmSnapshot *virtv2.VirtualMachineSnapshot + } + mock.lockBuild.RLock() + calls = mock.calls.Build + mock.lockBuild.RUnlock() + return calls +} + +// Ensure, that LifeCycleSnapshotterMock does implement LifeCycleSnapshotter. +// If this is not the case, regenerate this file with moq. +var _ LifeCycleSnapshotter = &LifeCycleSnapshotterMock{} + +// LifeCycleSnapshotterMock is a mock implementation of LifeCycleSnapshotter. +// +// func TestSomethingThatUsesLifeCycleSnapshotter(t *testing.T) { +// +// // make and configure a mocked LifeCycleSnapshotter +// mockedLifeCycleSnapshotter := &LifeCycleSnapshotterMock{ +// CanFreezeFunc: func(vm *virtv2.VirtualMachine) bool { +// panic("mock out the CanFreeze method") +// }, +// CanUnfreezeFunc: func(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) { +// panic("mock out the CanUnfreeze method") +// }, +// CreateSecretFunc: func(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { +// panic("mock out the CreateSecret method") +// }, +// CreateVirtualDiskSnapshotFunc: func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (*virtv2.VirtualDiskSnapshot, error) { +// panic("mock out the CreateVirtualDiskSnapshot method") +// }, +// FreezeFunc: func(ctx context.Context, name string, namespace string) error { +// panic("mock out the Freeze method") +// }, +// GetPersistentVolumeClaimFunc: func(ctx context.Context, name string, namespace string) (*corev1.PersistentVolumeClaim, error) { +// panic("mock out the GetPersistentVolumeClaim method") +// }, +// GetSecretFunc: func(ctx context.Context, name string, namespace string) (*corev1.Secret, error) { +// panic("mock out the GetSecret method") +// }, +// GetVirtualDiskFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) { +// panic("mock out the GetVirtualDisk method") +// }, +// GetVirtualDiskSnapshotFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDiskSnapshot, error) { +// panic("mock out the GetVirtualDiskSnapshot method") +// }, +// GetVirtualMachineFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualMachine, error) { +// panic("mock out the GetVirtualMachine method") +// }, +// IsFrozenFunc: func(vm *virtv2.VirtualMachine) bool { +// panic("mock out the IsFrozen method") +// }, +// UnfreezeFunc: func(ctx context.Context, name string, namespace string) error { +// panic("mock out the Unfreeze method") +// }, +// } +// +// // use mockedLifeCycleSnapshotter in code that requires LifeCycleSnapshotter +// // and then make assertions. +// +// } +type LifeCycleSnapshotterMock struct { + // CanFreezeFunc mocks the CanFreeze method. + CanFreezeFunc func(vm *virtv2.VirtualMachine) bool + + // CanUnfreezeFunc mocks the CanUnfreeze method. + CanUnfreezeFunc func(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) + + // CreateSecretFunc mocks the CreateSecret method. + CreateSecretFunc func(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) + + // CreateVirtualDiskSnapshotFunc mocks the CreateVirtualDiskSnapshot method. + CreateVirtualDiskSnapshotFunc func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (*virtv2.VirtualDiskSnapshot, error) + + // FreezeFunc mocks the Freeze method. + FreezeFunc func(ctx context.Context, name string, namespace string) error + + // GetPersistentVolumeClaimFunc mocks the GetPersistentVolumeClaim method. + GetPersistentVolumeClaimFunc func(ctx context.Context, name string, namespace string) (*corev1.PersistentVolumeClaim, error) + + // GetSecretFunc mocks the GetSecret method. + GetSecretFunc func(ctx context.Context, name string, namespace string) (*corev1.Secret, error) + + // GetVirtualDiskFunc mocks the GetVirtualDisk method. + GetVirtualDiskFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) + + // GetVirtualDiskSnapshotFunc mocks the GetVirtualDiskSnapshot method. + GetVirtualDiskSnapshotFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDiskSnapshot, error) + + // GetVirtualMachineFunc mocks the GetVirtualMachine method. + GetVirtualMachineFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualMachine, error) + + // IsFrozenFunc mocks the IsFrozen method. + IsFrozenFunc func(vm *virtv2.VirtualMachine) bool + + // UnfreezeFunc mocks the Unfreeze method. + UnfreezeFunc func(ctx context.Context, name string, namespace string) error + + // calls tracks calls to the methods. + calls struct { + // CanFreeze holds details about calls to the CanFreeze method. + CanFreeze []struct { + // VM is the vm argument value. + VM *virtv2.VirtualMachine + } + // CanUnfreeze holds details about calls to the CanUnfreeze method. + CanUnfreeze []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // VdSnapshotName is the vdSnapshotName argument value. + VdSnapshotName string + // VM is the vm argument value. + VM *virtv2.VirtualMachine + } + // CreateSecret holds details about calls to the CreateSecret method. + CreateSecret []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Secret is the secret argument value. + Secret *corev1.Secret + } + // CreateVirtualDiskSnapshot holds details about calls to the CreateVirtualDiskSnapshot method. + CreateVirtualDiskSnapshot []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // VdSnapshot is the vdSnapshot argument value. + VdSnapshot *virtv2.VirtualDiskSnapshot + } + // Freeze holds details about calls to the Freeze method. + Freeze []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // GetPersistentVolumeClaim holds details about calls to the GetPersistentVolumeClaim method. + GetPersistentVolumeClaim []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // GetSecret holds details about calls to the GetSecret method. + GetSecret []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // GetVirtualDisk holds details about calls to the GetVirtualDisk method. + GetVirtualDisk []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // GetVirtualDiskSnapshot holds details about calls to the GetVirtualDiskSnapshot method. + GetVirtualDiskSnapshot []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // GetVirtualMachine holds details about calls to the GetVirtualMachine method. + GetVirtualMachine []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + // IsFrozen holds details about calls to the IsFrozen method. + IsFrozen []struct { + // VM is the vm argument value. + VM *virtv2.VirtualMachine + } + // Unfreeze holds details about calls to the Unfreeze method. + Unfreeze []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Name is the name argument value. + Name string + // Namespace is the namespace argument value. + Namespace string + } + } + lockCanFreeze sync.RWMutex + lockCanUnfreeze sync.RWMutex + lockCreateSecret sync.RWMutex + lockCreateVirtualDiskSnapshot sync.RWMutex + lockFreeze sync.RWMutex + lockGetPersistentVolumeClaim sync.RWMutex + lockGetSecret sync.RWMutex + lockGetVirtualDisk sync.RWMutex + lockGetVirtualDiskSnapshot sync.RWMutex + lockGetVirtualMachine sync.RWMutex + lockIsFrozen sync.RWMutex + lockUnfreeze sync.RWMutex +} + +// CanFreeze calls CanFreezeFunc. +func (mock *LifeCycleSnapshotterMock) CanFreeze(vm *virtv2.VirtualMachine) bool { + if mock.CanFreezeFunc == nil { + panic("LifeCycleSnapshotterMock.CanFreezeFunc: method is nil but LifeCycleSnapshotter.CanFreeze was just called") + } + callInfo := struct { + VM *virtv2.VirtualMachine + }{ + VM: vm, + } + mock.lockCanFreeze.Lock() + mock.calls.CanFreeze = append(mock.calls.CanFreeze, callInfo) + mock.lockCanFreeze.Unlock() + return mock.CanFreezeFunc(vm) +} + +// CanFreezeCalls gets all the calls that were made to CanFreeze. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.CanFreezeCalls()) +func (mock *LifeCycleSnapshotterMock) CanFreezeCalls() []struct { + VM *virtv2.VirtualMachine +} { + var calls []struct { + VM *virtv2.VirtualMachine + } + mock.lockCanFreeze.RLock() + calls = mock.calls.CanFreeze + mock.lockCanFreeze.RUnlock() + return calls +} + +// CanUnfreeze calls CanUnfreezeFunc. +func (mock *LifeCycleSnapshotterMock) CanUnfreeze(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) { + if mock.CanUnfreezeFunc == nil { + panic("LifeCycleSnapshotterMock.CanUnfreezeFunc: method is nil but LifeCycleSnapshotter.CanUnfreeze was just called") + } + callInfo := struct { + Ctx context.Context + VdSnapshotName string + VM *virtv2.VirtualMachine + }{ + Ctx: ctx, + VdSnapshotName: vdSnapshotName, + VM: vm, + } + mock.lockCanUnfreeze.Lock() + mock.calls.CanUnfreeze = append(mock.calls.CanUnfreeze, callInfo) + mock.lockCanUnfreeze.Unlock() + return mock.CanUnfreezeFunc(ctx, vdSnapshotName, vm) +} + +// CanUnfreezeCalls gets all the calls that were made to CanUnfreeze. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.CanUnfreezeCalls()) +func (mock *LifeCycleSnapshotterMock) CanUnfreezeCalls() []struct { + Ctx context.Context + VdSnapshotName string + VM *virtv2.VirtualMachine +} { + var calls []struct { + Ctx context.Context + VdSnapshotName string + VM *virtv2.VirtualMachine + } + mock.lockCanUnfreeze.RLock() + calls = mock.calls.CanUnfreeze + mock.lockCanUnfreeze.RUnlock() + return calls +} + +// CreateSecret calls CreateSecretFunc. +func (mock *LifeCycleSnapshotterMock) CreateSecret(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { + if mock.CreateSecretFunc == nil { + panic("LifeCycleSnapshotterMock.CreateSecretFunc: method is nil but LifeCycleSnapshotter.CreateSecret was just called") + } + callInfo := struct { + Ctx context.Context + Secret *corev1.Secret + }{ + Ctx: ctx, + Secret: secret, + } + mock.lockCreateSecret.Lock() + mock.calls.CreateSecret = append(mock.calls.CreateSecret, callInfo) + mock.lockCreateSecret.Unlock() + return mock.CreateSecretFunc(ctx, secret) +} + +// CreateSecretCalls gets all the calls that were made to CreateSecret. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.CreateSecretCalls()) +func (mock *LifeCycleSnapshotterMock) CreateSecretCalls() []struct { + Ctx context.Context + Secret *corev1.Secret +} { + var calls []struct { + Ctx context.Context + Secret *corev1.Secret + } + mock.lockCreateSecret.RLock() + calls = mock.calls.CreateSecret + mock.lockCreateSecret.RUnlock() + return calls +} + +// CreateVirtualDiskSnapshot calls CreateVirtualDiskSnapshotFunc. +func (mock *LifeCycleSnapshotterMock) CreateVirtualDiskSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (*virtv2.VirtualDiskSnapshot, error) { + if mock.CreateVirtualDiskSnapshotFunc == nil { + panic("LifeCycleSnapshotterMock.CreateVirtualDiskSnapshotFunc: method is nil but LifeCycleSnapshotter.CreateVirtualDiskSnapshot was just called") + } + callInfo := struct { + Ctx context.Context + VdSnapshot *virtv2.VirtualDiskSnapshot + }{ + Ctx: ctx, + VdSnapshot: vdSnapshot, + } + mock.lockCreateVirtualDiskSnapshot.Lock() + mock.calls.CreateVirtualDiskSnapshot = append(mock.calls.CreateVirtualDiskSnapshot, callInfo) + mock.lockCreateVirtualDiskSnapshot.Unlock() + return mock.CreateVirtualDiskSnapshotFunc(ctx, vdSnapshot) +} + +// CreateVirtualDiskSnapshotCalls gets all the calls that were made to CreateVirtualDiskSnapshot. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.CreateVirtualDiskSnapshotCalls()) +func (mock *LifeCycleSnapshotterMock) CreateVirtualDiskSnapshotCalls() []struct { + Ctx context.Context + VdSnapshot *virtv2.VirtualDiskSnapshot +} { + var calls []struct { + Ctx context.Context + VdSnapshot *virtv2.VirtualDiskSnapshot + } + mock.lockCreateVirtualDiskSnapshot.RLock() + calls = mock.calls.CreateVirtualDiskSnapshot + mock.lockCreateVirtualDiskSnapshot.RUnlock() + return calls +} + +// Freeze calls FreezeFunc. +func (mock *LifeCycleSnapshotterMock) Freeze(ctx context.Context, name string, namespace string) error { + if mock.FreezeFunc == nil { + panic("LifeCycleSnapshotterMock.FreezeFunc: method is nil but LifeCycleSnapshotter.Freeze was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockFreeze.Lock() + mock.calls.Freeze = append(mock.calls.Freeze, callInfo) + mock.lockFreeze.Unlock() + return mock.FreezeFunc(ctx, name, namespace) +} + +// FreezeCalls gets all the calls that were made to Freeze. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.FreezeCalls()) +func (mock *LifeCycleSnapshotterMock) FreezeCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockFreeze.RLock() + calls = mock.calls.Freeze + mock.lockFreeze.RUnlock() + return calls +} + +// GetPersistentVolumeClaim calls GetPersistentVolumeClaimFunc. +func (mock *LifeCycleSnapshotterMock) GetPersistentVolumeClaim(ctx context.Context, name string, namespace string) (*corev1.PersistentVolumeClaim, error) { + if mock.GetPersistentVolumeClaimFunc == nil { + panic("LifeCycleSnapshotterMock.GetPersistentVolumeClaimFunc: method is nil but LifeCycleSnapshotter.GetPersistentVolumeClaim was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetPersistentVolumeClaim.Lock() + mock.calls.GetPersistentVolumeClaim = append(mock.calls.GetPersistentVolumeClaim, callInfo) + mock.lockGetPersistentVolumeClaim.Unlock() + return mock.GetPersistentVolumeClaimFunc(ctx, name, namespace) +} + +// GetPersistentVolumeClaimCalls gets all the calls that were made to GetPersistentVolumeClaim. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetPersistentVolumeClaimCalls()) +func (mock *LifeCycleSnapshotterMock) GetPersistentVolumeClaimCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetPersistentVolumeClaim.RLock() + calls = mock.calls.GetPersistentVolumeClaim + mock.lockGetPersistentVolumeClaim.RUnlock() + return calls +} + +// GetSecret calls GetSecretFunc. +func (mock *LifeCycleSnapshotterMock) GetSecret(ctx context.Context, name string, namespace string) (*corev1.Secret, error) { + if mock.GetSecretFunc == nil { + panic("LifeCycleSnapshotterMock.GetSecretFunc: method is nil but LifeCycleSnapshotter.GetSecret was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetSecret.Lock() + mock.calls.GetSecret = append(mock.calls.GetSecret, callInfo) + mock.lockGetSecret.Unlock() + return mock.GetSecretFunc(ctx, name, namespace) +} + +// GetSecretCalls gets all the calls that were made to GetSecret. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetSecretCalls()) +func (mock *LifeCycleSnapshotterMock) GetSecretCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetSecret.RLock() + calls = mock.calls.GetSecret + mock.lockGetSecret.RUnlock() + return calls +} + +// GetVirtualDisk calls GetVirtualDiskFunc. +func (mock *LifeCycleSnapshotterMock) GetVirtualDisk(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) { + if mock.GetVirtualDiskFunc == nil { + panic("LifeCycleSnapshotterMock.GetVirtualDiskFunc: method is nil but LifeCycleSnapshotter.GetVirtualDisk was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetVirtualDisk.Lock() + mock.calls.GetVirtualDisk = append(mock.calls.GetVirtualDisk, callInfo) + mock.lockGetVirtualDisk.Unlock() + return mock.GetVirtualDiskFunc(ctx, name, namespace) +} + +// GetVirtualDiskCalls gets all the calls that were made to GetVirtualDisk. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetVirtualDiskCalls()) +func (mock *LifeCycleSnapshotterMock) GetVirtualDiskCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetVirtualDisk.RLock() + calls = mock.calls.GetVirtualDisk + mock.lockGetVirtualDisk.RUnlock() + return calls +} + +// GetVirtualDiskSnapshot calls GetVirtualDiskSnapshotFunc. +func (mock *LifeCycleSnapshotterMock) GetVirtualDiskSnapshot(ctx context.Context, name string, namespace string) (*virtv2.VirtualDiskSnapshot, error) { + if mock.GetVirtualDiskSnapshotFunc == nil { + panic("LifeCycleSnapshotterMock.GetVirtualDiskSnapshotFunc: method is nil but LifeCycleSnapshotter.GetVirtualDiskSnapshot was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetVirtualDiskSnapshot.Lock() + mock.calls.GetVirtualDiskSnapshot = append(mock.calls.GetVirtualDiskSnapshot, callInfo) + mock.lockGetVirtualDiskSnapshot.Unlock() + return mock.GetVirtualDiskSnapshotFunc(ctx, name, namespace) +} + +// GetVirtualDiskSnapshotCalls gets all the calls that were made to GetVirtualDiskSnapshot. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetVirtualDiskSnapshotCalls()) +func (mock *LifeCycleSnapshotterMock) GetVirtualDiskSnapshotCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetVirtualDiskSnapshot.RLock() + calls = mock.calls.GetVirtualDiskSnapshot + mock.lockGetVirtualDiskSnapshot.RUnlock() + return calls +} + +// GetVirtualMachine calls GetVirtualMachineFunc. +func (mock *LifeCycleSnapshotterMock) GetVirtualMachine(ctx context.Context, name string, namespace string) (*virtv2.VirtualMachine, error) { + if mock.GetVirtualMachineFunc == nil { + panic("LifeCycleSnapshotterMock.GetVirtualMachineFunc: method is nil but LifeCycleSnapshotter.GetVirtualMachine was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetVirtualMachine.Lock() + mock.calls.GetVirtualMachine = append(mock.calls.GetVirtualMachine, callInfo) + mock.lockGetVirtualMachine.Unlock() + return mock.GetVirtualMachineFunc(ctx, name, namespace) +} + +// GetVirtualMachineCalls gets all the calls that were made to GetVirtualMachine. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetVirtualMachineCalls()) +func (mock *LifeCycleSnapshotterMock) GetVirtualMachineCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetVirtualMachine.RLock() + calls = mock.calls.GetVirtualMachine + mock.lockGetVirtualMachine.RUnlock() + return calls +} + +// IsFrozen calls IsFrozenFunc. +func (mock *LifeCycleSnapshotterMock) IsFrozen(vm *virtv2.VirtualMachine) bool { + if mock.IsFrozenFunc == nil { + panic("LifeCycleSnapshotterMock.IsFrozenFunc: method is nil but LifeCycleSnapshotter.IsFrozen was just called") + } + callInfo := struct { + VM *virtv2.VirtualMachine + }{ + VM: vm, + } + mock.lockIsFrozen.Lock() + mock.calls.IsFrozen = append(mock.calls.IsFrozen, callInfo) + mock.lockIsFrozen.Unlock() + return mock.IsFrozenFunc(vm) +} + +// IsFrozenCalls gets all the calls that were made to IsFrozen. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.IsFrozenCalls()) +func (mock *LifeCycleSnapshotterMock) IsFrozenCalls() []struct { + VM *virtv2.VirtualMachine +} { + var calls []struct { + VM *virtv2.VirtualMachine + } + mock.lockIsFrozen.RLock() + calls = mock.calls.IsFrozen + mock.lockIsFrozen.RUnlock() + return calls +} + +// Unfreeze calls UnfreezeFunc. +func (mock *LifeCycleSnapshotterMock) Unfreeze(ctx context.Context, name string, namespace string) error { + if mock.UnfreezeFunc == nil { + panic("LifeCycleSnapshotterMock.UnfreezeFunc: method is nil but LifeCycleSnapshotter.Unfreeze was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockUnfreeze.Lock() + mock.calls.Unfreeze = append(mock.calls.Unfreeze, callInfo) + mock.lockUnfreeze.Unlock() + return mock.UnfreezeFunc(ctx, name, namespace) +} + +// UnfreezeCalls gets all the calls that were made to Unfreeze. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.UnfreezeCalls()) +func (mock *LifeCycleSnapshotterMock) UnfreezeCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockUnfreeze.RLock() + calls = mock.calls.Unfreeze + mock.lockUnfreeze.RUnlock() + return calls +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/secret_builder.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/secret_builder.go new file mode 100644 index 000000000..f2795b013 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/secret_builder.go @@ -0,0 +1,235 @@ +/* +Copyright 2024 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" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + + 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" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type SecretBuilder struct { + client client.Client +} + +func NewSecretBuilder(client client.Client) *SecretBuilder { + return &SecretBuilder{ + client: client, + } +} + +func (b SecretBuilder) Build(ctx context.Context, vm *virtv2.VirtualMachine, vmSnapshot *virtv2.VirtualMachineSnapshot) (*corev1.Secret, error) { + secret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: vmSnapshot.Status.VirtualMachineSnapshotSecretName, + Namespace: vmSnapshot.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vmSnapshot), + }, + }, + Data: make(map[string][]byte), + Type: "virtualmachine.virtualization.deckhouse.io/snapshot", + } + + err := b.setVirtualMachine(&secret, vm) + if err != nil { + return nil, err + } + + err = b.setVirtualDisks(ctx, &secret, vm) + if err != nil { + return nil, err + } + + err = b.setVirtualMachineBlockDeviceAttachments(ctx, &secret, vm) + if err != nil { + return nil, err + } + + err = b.setVirtualMachineIPAddress(ctx, &secret, vm) + if err != nil { + return nil, err + } + + err = b.setProvisioning(ctx, &secret, vm) + if err != nil { + return nil, err + } + + return &secret, nil +} + +func (b SecretBuilder) setVirtualMachine(secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + err := b.push(secret, "vm", vm) + if err != nil { + return err + } + + return nil +} + +func (b SecretBuilder) setVirtualDisks(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice { + continue + } + + vd, err := helper.FetchObject(ctx, types.NamespacedName{ + Name: bdr.Name, + Namespace: vm.Namespace, + }, b.client, &virtv2.VirtualDisk{}) + if err != nil { + return err + } + + if vd == nil { + return fmt.Errorf("the virtual disk %q not found", bdr.Name) + } + + err = b.push(secret, "vd-"+vd.Name, vd) + if err != nil { + return err + } + } + + return nil +} + +func (b SecretBuilder) setVirtualMachineBlockDeviceAttachments(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice || !bdr.Hotplugged { + continue + } + + vmbda, err := helper.FetchObject(ctx, types.NamespacedName{ + Name: bdr.VirtualMachineBlockDeviceAttachmentName, + Namespace: vm.Namespace, + }, b.client, &virtv2.VirtualMachineBlockDeviceAttachment{}) + if err != nil { + return err + } + + if vmbda == nil { + return fmt.Errorf("the virtual machine block device attachment %q not found", bdr.VirtualMachineBlockDeviceAttachmentName) + } + + err = b.push(secret, "vmbda-"+vmbda.Name, vmbda) + if err != nil { + return err + } + } + + return nil +} + +func (b SecretBuilder) setVirtualMachineIPAddress(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + vmip, err := helper.FetchObject(ctx, types.NamespacedName{ + Namespace: vm.Namespace, + Name: vm.Status.VirtualMachineIPAddress, + }, b.client, &virtv2.VirtualMachineIPAddress{}) + if err != nil { + return err + } + + if vmip == nil { + return fmt.Errorf("the virtual machine ip address %q not found", vm.Status.VirtualMachineIPAddress) + } + + err = b.push(secret, "vmip-"+vmip.Name, vmip) + if err != nil { + return err + } + + return nil +} + +func (b SecretBuilder) setProvisioning(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + var secretName string + + switch vm.Spec.Provisioning.Type { + case virtv2.ProvisioningTypeSysprepRef: + if vm.Spec.Provisioning.SysprepRef == nil { + return errors.New("the virtual machine sysprep ref provisioning is nil") + } + + switch vm.Spec.Provisioning.SysprepRef.Kind { + case virtv2.SysprepRefKindSecret: + secretName = vm.Spec.Provisioning.SysprepRef.Name + default: + return fmt.Errorf("unknown sysprep ref kind %s", vm.Spec.Provisioning.SysprepRef.Kind) + } + case virtv2.ProvisioningTypeUserDataRef: + if vm.Spec.Provisioning.UserDataRef == nil { + return errors.New("the virtual machine user data ref provisioning is nil") + } + + switch vm.Spec.Provisioning.UserDataRef.Kind { + case virtv2.UserDataRefKindSecret: + secretName = vm.Spec.Provisioning.UserDataRef.Name + default: + return fmt.Errorf("unknown user data ref kind %s", vm.Spec.Provisioning.UserDataRef.Kind) + } + default: + return nil + } + + ref, err := helper.FetchObject(ctx, types.NamespacedName{ + Name: secretName, + Namespace: vm.Namespace, + }, b.client, &corev1.Secret{}) + if err != nil { + return err + } + + if ref == nil { + return fmt.Errorf("the virtual machine provisioning secret %q not found", secretName) + } + + err = b.push(secret, strings.ToLower(string(vm.Spec.Provisioning.Type))+"-"+ref.Name, ref) + if err != nil { + return err + } + + return nil +} + +func (b SecretBuilder) push(secret *corev1.Secret, key string, value client.Object) error { + JSON, err := json.Marshal(value) + if err != nil { + return err + } + + secret.Data[key] = []byte(base64.StdEncoding.EncodeToString(JSON)) + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/virtual_machine_ready.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/virtual_machine_ready.go new file mode 100644 index 000000000..ae518dbe8 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/virtual_machine_ready.go @@ -0,0 +1,116 @@ +/* +Copyright 2024 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" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmscondition" +) + +type VirtualMachineReadySnapshotter interface { + GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) +} + +type VirtualMachineReadyHandler struct { + snapshotter VirtualMachineReadySnapshotter +} + +func NewVirtualMachineReadyHandler(snapshotter VirtualMachineReadySnapshotter) *VirtualMachineReadyHandler { + return &VirtualMachineReadyHandler{ + snapshotter: snapshotter, + } +} + +func (h VirtualMachineReadyHandler) Handle(ctx context.Context, vmSnapshot *virtv2.VirtualMachineSnapshot) (reconcile.Result, error) { + condition, ok := service.GetCondition(vmscondition.VirtualMachineReadyType, vmSnapshot.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vmscondition.VirtualMachineReadyType, + Status: metav1.ConditionUnknown, + Reason: vmscondition.VirtualMachineUnknown, + } + } + + defer func() { service.SetCondition(condition, &vmSnapshot.Status.Conditions) }() + + if vmSnapshot.DeletionTimestamp != nil { + condition.Status = metav1.ConditionUnknown + condition.Reason = vmscondition.VirtualMachineUnknown + condition.Message = "" + return reconcile.Result{}, nil + } + + if vmSnapshot.Status.Phase == virtv2.VirtualMachineSnapshotPhaseReady { + condition.Status = metav1.ConditionTrue + condition.Reason = vmscondition.VirtualMachineReady + condition.Message = "" + return reconcile.Result{}, nil + } + + vm, err := h.snapshotter.GetVirtualMachine(ctx, vmSnapshot.Spec.VirtualMachineName, vmSnapshot.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + if vm == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.VirtualMachineNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual machine %q not found.", vmSnapshot.Spec.VirtualMachineName) + return reconcile.Result{}, nil + } + + if vm.GetDeletionTimestamp() != nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.VirtualMachineNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual machine %q is in process of deletion.", vm.Name) + return reconcile.Result{}, nil + } + + switch vm.Status.Phase { + case virtv2.MachineRunning, virtv2.MachineStopped: + snapshotting, _ := service.GetCondition(vmcondition.TypeSnapshotting.String(), vm.Status.Conditions) + if snapshotting.Status != metav1.ConditionTrue { + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.VirtualMachineNotReadyForSnapshotting + if snapshotting.Message == "" { + condition.Message = "The VirtualMachineSnapshot resource has not been detected for the virtual machine yet." + } else { + condition.Message = snapshotting.Message + } + + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionTrue + condition.Reason = vmscondition.VirtualMachineReady + condition.Message = "" + return reconcile.Result{}, nil + default: + condition.Status = metav1.ConditionFalse + condition.Reason = vmscondition.VirtualMachineNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual machine %q is in the %q phase: waiting for it to reach the Running or Stopped phase.", vm.Name, vm.Status.Phase) + return reconcile.Result{}, nil + } +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vd_watcher.go new file mode 100644 index 000000000..f6f2b4b02 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vd_watcher.go @@ -0,0 +1,113 @@ +/* +Copyright 2024 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 watcher + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type VirtualDiskWatcher struct { + client client.Client +} + +func NewVirtualDiskWatcher(client client.Client) *VirtualDiskWatcher { + return &VirtualDiskWatcher{ + client: client, + } +} + +func (w VirtualDiskWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDisk{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var vmSnapshots virtv2.VirtualMachineSnapshotList + // TODO use index. + err := w.client.List(ctx, &vmSnapshots, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual machine snapshots: %s", err)) + return + } + + for _, vmSnapshot := range vmSnapshots.Items { + for _, vdSnapshotName := range vmSnapshot.Status.VirtualDiskSnapshotNames { + vdName, ok := strings.CutSuffix(vdSnapshotName, "-"+string(vmSnapshot.UID)) + if !ok { + slog.Default().Error("Failed to get virtual disk name from virtual disk snapshot name, please report a bug") + continue + } + + if vdName == obj.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmSnapshot.Name, + Namespace: vmSnapshot.Namespace, + }, + }) + break + } + } + } + + return +} + +func (w VirtualDiskWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVD, ok := e.ObjectOld.(*virtv2.VirtualDisk) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an old VirtualDisk but got a %T", e.ObjectOld)) + return false + } + + newVD, ok := e.ObjectNew.(*virtv2.VirtualDisk) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a new VirtualDisk but got a %T", e.ObjectNew)) + return false + } + + oldResized, _ := service.GetCondition(vdcondition.ResizedType, oldVD.Status.Conditions) + newResized, _ := service.GetCondition(vdcondition.ResizedType, newVD.Status.Conditions) + + return oldResized.Status != newResized.Status || oldResized.Reason != newResized.Reason +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vdsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vdsnapshot_watcher.go new file mode 100644 index 000000000..0bf8944c6 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vdsnapshot_watcher.go @@ -0,0 +1,102 @@ +/* +Copyright 2024 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 watcher + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualDiskSnapshotWatcher struct { + client client.Client +} + +func NewVirtualDiskSnapshotWatcher(client client.Client) *VirtualDiskSnapshotWatcher { + return &VirtualDiskSnapshotWatcher{ + client: client, + } +} + +func (w VirtualDiskSnapshotWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualDiskSnapshot{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskSnapshotWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var vmSnapshots virtv2.VirtualMachineSnapshotList + // TODO use index. + err := w.client.List(ctx, &vmSnapshots, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual machine snapshots: %s", err)) + return + } + + for _, vmSnapshot := range vmSnapshots.Items { + for _, vdSnapshotName := range vmSnapshot.Status.VirtualDiskSnapshotNames { + if vdSnapshotName == obj.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmSnapshot.Name, + Namespace: vmSnapshot.Namespace, + }, + }) + + break + } + } + } + + return +} + +func (w VirtualDiskSnapshotWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVDSnapshot, ok := e.ObjectOld.(*virtv2.VirtualDiskSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an old VirtualDiskSnapshot but got a %T", e.ObjectOld)) + return false + } + + newVDSnapshot, ok := e.ObjectNew.(*virtv2.VirtualDiskSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a new VirtualDiskSnapshot but got a %T", e.ObjectNew)) + return false + } + + return oldVDSnapshot.Status.Phase != newVDSnapshot.Status.Phase +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vm_watcher.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vm_watcher.go new file mode 100644 index 000000000..66befde09 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vm_watcher.go @@ -0,0 +1,110 @@ +/* +Copyright 2024 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 watcher + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +type VirtualMachineWatcher struct { + client client.Client +} + +func NewVirtualMachineWatcher(client client.Client) *VirtualMachineWatcher { + return &VirtualMachineWatcher{ + client: client, + } +} + +func (w VirtualMachineWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachine{}), + handler.EnqueueRequestsFromMapFunc(w.enqueueRequests), + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualMachineWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var vmSnapshots virtv2.VirtualMachineSnapshotList + // TODO use index. + err := w.client.List(ctx, &vmSnapshots, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual machine snapshots: %s", err)) + return + } + + for _, vmSnapshot := range vmSnapshots.Items { + if vmSnapshot.Spec.VirtualMachineName == obj.GetName() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vmSnapshot.Name, + Namespace: vmSnapshot.Namespace, + }, + }) + } + } + + return +} + +func (w VirtualMachineWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldVM, ok := e.ObjectOld.(*virtv2.VirtualMachine) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an old VirtualMachine but got a %T", e.ObjectOld)) + return false + } + + newVM, ok := e.ObjectNew.(*virtv2.VirtualMachine) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a new VirtualMachine but got a %T", e.ObjectNew)) + return false + } + + oldFSReady, _ := service.GetCondition(vmcondition.TypeFilesystemReady.String(), oldVM.Status.Conditions) + newFSReady, _ := service.GetCondition(vmcondition.TypeFilesystemReady.String(), newVM.Status.Conditions) + + if oldFSReady.Status != newFSReady.Status { + return true + } + + oldSnapshotting, _ := service.GetCondition(vmcondition.TypeSnapshotting.String(), oldVM.Status.Conditions) + newSnapshotting, _ := service.GetCondition(vmcondition.TypeSnapshotting.String(), newVM.Status.Conditions) + + return oldSnapshotting.Status != newSnapshotting.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vmsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vmsnapshot_watcher.go new file mode 100644 index 000000000..534b052d7 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/watcher/vmsnapshot_watcher.go @@ -0,0 +1,53 @@ +/* +Copyright 2024 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 watcher + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineSnapshotWatcher struct { + client client.Client +} + +func NewVirtualMachineSnapshotWatcher(client client.Client) *VirtualMachineSnapshotWatcher { + return &VirtualMachineSnapshotWatcher{ + client: client, + } +} + +func (w VirtualMachineSnapshotWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv2.VirtualMachineSnapshot{}), + &handler.EnqueueRequestForObject{}, + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, + }, + ) +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_controller.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_controller.go new file mode 100644 index 000000000..5d1b70a3e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_controller.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 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 vmsnapshot + +import ( + "context" + "log/slog" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmsnapshot/internal" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization/api/client/kubeclient" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ControllerName = "vmsnapshot-controller" + +func NewController( + ctx context.Context, + mgr manager.Manager, + log *slog.Logger, + virtClient kubeclient.Client, +) (controller.Controller, error) { + log = log.With(logger.SlogController(ControllerName)) + + protection := service.NewProtectionService(mgr.GetClient(), virtv2.FinalizerVMSnapshotProtection) + snapshotter := service.NewSnapshotService(virtClient, mgr.GetClient(), protection) + secretBuilder := internal.NewSecretBuilder(mgr.GetClient()) + + reconciler := NewReconciler( + mgr.GetClient(), + internal.NewVirtualMachineReadyHandler(snapshotter), + internal.NewLifeCycleHandler(snapshotter, secretBuilder), + internal.NewDeletionHandler(), + ) + + vmSnapshotController, err := controller.New(ControllerName, mgr, controller.Options{ + Reconciler: reconciler, + RecoverPanic: ptr.To(true), + LogConstructor: logger.NewConstructor(log), + }) + if err != nil { + return nil, err + } + + err = reconciler.SetupController(ctx, mgr, vmSnapshotController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&virtv2.VirtualMachineSnapshot{}). + WithValidator(NewValidator(log)). + Complete(); err != nil { + return nil, err + } + + log.Info("Initialized VirtualMachineSnapshot controller") + + return vmSnapshotController, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_reconciler.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_reconciler.go new file mode 100644 index 000000000..647091807 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_reconciler.go @@ -0,0 +1,121 @@ +/* +Copyright 2024 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 vmsnapshot + +import ( + "context" + "errors" + "fmt" + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vmsnapshot/internal/watcher" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Handler interface { + Handle(ctx context.Context, vmSnapshot *virtv2.VirtualMachineSnapshot) (reconcile.Result, error) +} + +type Watcher interface { + Watch(mgr manager.Manager, ctr controller.Controller) error +} + +type Reconciler struct { + handlers []Handler + client client.Client +} + +func NewReconciler(client client.Client, handlers ...Handler) *Reconciler { + return &Reconciler{ + client: client, + handlers: handlers, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := logger.FromContext(ctx) + + vmSnapshot := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vmSnapshot.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vmSnapshot.IsEmpty() { + return reconcile.Result{}, nil + } + + var result reconcile.Result + var handlerErrs []error + + for _, h := range r.handlers { + var res reconcile.Result + res, err = h.Handle(ctx, vmSnapshot.Changed()) + if err != nil { + log.Error("Failed to handle vmSnapshot", logger.SlogErr(err), logger.SlogHandler(reflect.TypeOf(h).Elem().Name())) + handlerErrs = append(handlerErrs, err) + } + + result = service.MergeResults(result, res) + } + + vmSnapshot.Changed().Status.ObservedGeneration = vmSnapshot.Changed().Generation + + err = vmSnapshot.Update(ctx) + if err != nil { + return reconcile.Result{}, err + } + + err = errors.Join(handlerErrs...) + if err != nil { + return reconcile.Result{}, err + } + + return result, nil +} + +func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr controller.Controller) error { + for _, w := range []Watcher{ + watcher.NewVirtualMachineSnapshotWatcher(mgr.GetClient()), + watcher.NewVirtualDiskSnapshotWatcher(mgr.GetClient()), + watcher.NewVirtualMachineWatcher(mgr.GetClient()), + watcher.NewVirtualDiskWatcher(mgr.GetClient()), + } { + err := w.Watch(mgr, ctr) + if err != nil { + return fmt.Errorf("faield to run watcher %s: %w", reflect.TypeOf(w).Elem().Name(), err) + } + } + + return nil +} + +func (r *Reconciler) factory() *virtv2.VirtualMachineSnapshot { + return &virtv2.VirtualMachineSnapshot{} +} + +func (r *Reconciler) statusGetter(obj *virtv2.VirtualMachineSnapshot) virtv2.VirtualMachineSnapshotStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_webhook.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_webhook.go new file mode 100644 index 000000000..b50baa469 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/vmsnapshot_webhook.go @@ -0,0 +1,77 @@ +/* +Copyright 2024 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 vmsnapshot + +import ( + "context" + "fmt" + "log/slog" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Validator struct { + logger *slog.Logger +} + +func NewValidator(logger *slog.Logger) *Validator { + return &Validator{ + logger: logger.With("webhook", "validator"), + } +} + +func (v *Validator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + vmSnapshot, ok := obj.(*virtv2.VirtualMachineSnapshot) + if !ok { + return nil, fmt.Errorf("expected a VirtualMachineSnapshot but got a %T", obj) + } + + if vmSnapshot.Spec.VirtualMachineName == "" { + return nil, fmt.Errorf("virtual machine name cannot be empty") + } + + return nil, nil +} + +func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldVMSnapshot, ok := oldObj.(*virtv2.VirtualMachineSnapshot) + if !ok { + return nil, fmt.Errorf("expected an old VirtualMachineSnapshot but got a %T", newObj) + } + + newVMSnapshot, ok := newObj.(*virtv2.VirtualMachineSnapshot) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachineSnapshot but got a %T", newObj) + } + + v.logger.Info("Validating VirtualMachineSnapshot") + + if oldVMSnapshot.Generation != newVMSnapshot.Generation { + return nil, fmt.Errorf("VirtualMachineSnapshot is an idempotent resource: specification changes are not available") + } + + return nil, nil +} + +func (v *Validator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + err := fmt.Errorf("misconfigured webhook rules: delete operation not implemented") + v.logger.Error("Ensure the correctness of ValidatingWebhookConfiguration", "err", err) + return nil, nil +} diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index 987765767..30c99bf3d 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -172,7 +172,6 @@ rules: - virtualization.deckhouse.io resources: - virtualdisks - - virtualmachinedisksnapshots - virtualimages - virtualmachineipaddressleases - virtualmachineipaddresses @@ -182,6 +181,7 @@ rules: - virtualmachineoperations - virtualmachineclasses - virtualdisksnapshots + - virtualmachinesnapshots verbs: - create - delete @@ -194,7 +194,6 @@ rules: - virtualization.deckhouse.io resources: - virtualdisks/finalizers - - virtualmachinedisksnapshots/finalizers - virtualimages/finalizers - virtualmachineblockdeviceattachments/finalizers - virtualmachines/finalizers @@ -204,10 +203,10 @@ rules: - virtualmachineoperations/finalizers - virtualmachineclasses/finalizers - virtualdisksnapshots/finalizers + - virtualmachinesnapshots/finalizers - virtualmachineipaddresses/status - virtualmachineipaddressleases/status - virtualdisks/status - - virtualmachinedisksnapshots/status - virtualimages/status - virtualmachineblockdeviceattachments/status - virtualmachines/status @@ -215,6 +214,7 @@ rules: - virtualmachineoperations/status - virtualmachineclasses/status - virtualdisksnapshots/status + - virtualmachinesnapshots/status verbs: - patch - update