diff --git a/.gitignore b/.gitignore index 6a9b69df9..295845058 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ tmp/* tmp # Kubernetes .kube +local.Dockerfile # direnv .envrc @@ -52,4 +53,4 @@ logs/ inventory/ # generated directories -retry/ \ No newline at end of file +retry/ 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 44c441691..85a392296 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 @@ -29,6 +29,7 @@ type VirtualizationV1alpha2Interface interface { RESTClient() rest.Interface ClusterVirtualImagesGetter VirtualDisksGetter + VirtualDiskSnapshotsGetter VirtualImagesGetter VirtualMachinesGetter VirtualMachineBlockDeviceAttachmentsGetter @@ -51,6 +52,10 @@ func (c *VirtualizationV1alpha2Client) VirtualDisks(namespace string) VirtualDis return newVirtualDisks(c, namespace) } +func (c *VirtualizationV1alpha2Client) VirtualDiskSnapshots(namespace string) VirtualDiskSnapshotInterface { + return newVirtualDiskSnapshots(c, namespace) +} + func (c *VirtualizationV1alpha2Client) VirtualImages(namespace string) VirtualImageInterface { return newVirtualImages(c, namespace) } 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 eca3d2eca..a0a9c895e 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 @@ -35,6 +35,10 @@ func (c *FakeVirtualizationV1alpha2) VirtualDisks(namespace string) v1alpha2.Vir return &FakeVirtualDisks{c, namespace} } +func (c *FakeVirtualizationV1alpha2) VirtualDiskSnapshots(namespace string) v1alpha2.VirtualDiskSnapshotInterface { + return &FakeVirtualDiskSnapshots{c, namespace} +} + func (c *FakeVirtualizationV1alpha2) VirtualImages(namespace string) v1alpha2.VirtualImageInterface { return &FakeVirtualImages{c, namespace} } diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualdisksnapshot.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualdisksnapshot.go new file mode 100644 index 000000000..ea813e7cd --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualdisksnapshot.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" +) + +// FakeVirtualDiskSnapshots implements VirtualDiskSnapshotInterface +type FakeVirtualDiskSnapshots struct { + Fake *FakeVirtualizationV1alpha2 + ns string +} + +var virtualdisksnapshotsResource = v1alpha2.SchemeGroupVersion.WithResource("virtualdisksnapshots") + +var virtualdisksnapshotsKind = v1alpha2.SchemeGroupVersion.WithKind("VirtualDiskSnapshot") + +// Get takes name of the virtualDiskSnapshot, and returns the corresponding virtualDiskSnapshot object, and an error if there is any. +func (c *FakeVirtualDiskSnapshots) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(virtualdisksnapshotsResource, c.ns, name), &v1alpha2.VirtualDiskSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualDiskSnapshot), err +} + +// List takes label and field selectors, and returns the list of VirtualDiskSnapshots that match those selectors. +func (c *FakeVirtualDiskSnapshots) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VirtualDiskSnapshotList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(virtualdisksnapshotsResource, virtualdisksnapshotsKind, c.ns, opts), &v1alpha2.VirtualDiskSnapshotList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.VirtualDiskSnapshotList{ListMeta: obj.(*v1alpha2.VirtualDiskSnapshotList).ListMeta} + for _, item := range obj.(*v1alpha2.VirtualDiskSnapshotList).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 virtualDiskSnapshots. +func (c *FakeVirtualDiskSnapshots) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(virtualdisksnapshotsResource, c.ns, opts)) + +} + +// Create takes the representation of a virtualDiskSnapshot and creates it. Returns the server's representation of the virtualDiskSnapshot, and an error, if there is any. +func (c *FakeVirtualDiskSnapshots) Create(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.CreateOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(virtualdisksnapshotsResource, c.ns, virtualDiskSnapshot), &v1alpha2.VirtualDiskSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualDiskSnapshot), err +} + +// Update takes the representation of a virtualDiskSnapshot and updates it. Returns the server's representation of the virtualDiskSnapshot, and an error, if there is any. +func (c *FakeVirtualDiskSnapshots) Update(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(virtualdisksnapshotsResource, c.ns, virtualDiskSnapshot), &v1alpha2.VirtualDiskSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualDiskSnapshot), 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 *FakeVirtualDiskSnapshots) UpdateStatus(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualDiskSnapshot, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(virtualdisksnapshotsResource, "status", c.ns, virtualDiskSnapshot), &v1alpha2.VirtualDiskSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualDiskSnapshot), err +} + +// Delete takes name of the virtualDiskSnapshot and deletes it. Returns an error if one occurs. +func (c *FakeVirtualDiskSnapshots) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(virtualdisksnapshotsResource, c.ns, name, opts), &v1alpha2.VirtualDiskSnapshot{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeVirtualDiskSnapshots) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(virtualdisksnapshotsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.VirtualDiskSnapshotList{}) + return err +} + +// Patch applies the patch and returns the patched virtualDiskSnapshot. +func (c *FakeVirtualDiskSnapshots) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VirtualDiskSnapshot, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(virtualdisksnapshotsResource, c.ns, name, pt, data, subresources...), &v1alpha2.VirtualDiskSnapshot{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VirtualDiskSnapshot), 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 d0cf841a8..3eb448f15 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 @@ -21,6 +21,8 @@ type ClusterVirtualImageExpansion interface{} type VirtualDiskExpansion interface{} +type VirtualDiskSnapshotExpansion interface{} + type VirtualImageExpansion interface{} type VirtualMachineExpansion interface{} diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualdisksnapshot.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualdisksnapshot.go new file mode 100644 index 000000000..17a647690 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualdisksnapshot.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" +) + +// VirtualDiskSnapshotsGetter has a method to return a VirtualDiskSnapshotInterface. +// A group's client should implement this interface. +type VirtualDiskSnapshotsGetter interface { + VirtualDiskSnapshots(namespace string) VirtualDiskSnapshotInterface +} + +// VirtualDiskSnapshotInterface has methods to work with VirtualDiskSnapshot resources. +type VirtualDiskSnapshotInterface interface { + Create(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.CreateOptions) (*v1alpha2.VirtualDiskSnapshot, error) + Update(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualDiskSnapshot, error) + UpdateStatus(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (*v1alpha2.VirtualDiskSnapshot, 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.VirtualDiskSnapshot, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.VirtualDiskSnapshotList, 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.VirtualDiskSnapshot, err error) + VirtualDiskSnapshotExpansion +} + +// virtualDiskSnapshots implements VirtualDiskSnapshotInterface +type virtualDiskSnapshots struct { + client rest.Interface + ns string +} + +// newVirtualDiskSnapshots returns a VirtualDiskSnapshots +func newVirtualDiskSnapshots(c *VirtualizationV1alpha2Client, namespace string) *virtualDiskSnapshots { + return &virtualDiskSnapshots{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the virtualDiskSnapshot, and returns the corresponding virtualDiskSnapshot object, and an error if there is any. +func (c *virtualDiskSnapshots) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + result = &v1alpha2.VirtualDiskSnapshot{} + err = c.client.Get(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of VirtualDiskSnapshots that match those selectors. +func (c *virtualDiskSnapshots) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VirtualDiskSnapshotList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.VirtualDiskSnapshotList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested virtualDiskSnapshots. +func (c *virtualDiskSnapshots) 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("virtualdisksnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a virtualDiskSnapshot and creates it. Returns the server's representation of the virtualDiskSnapshot, and an error, if there is any. +func (c *virtualDiskSnapshots) Create(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.CreateOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + result = &v1alpha2.VirtualDiskSnapshot{} + err = c.client.Post(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualDiskSnapshot). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a virtualDiskSnapshot and updates it. Returns the server's representation of the virtualDiskSnapshot, and an error, if there is any. +func (c *virtualDiskSnapshots) Update(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + result = &v1alpha2.VirtualDiskSnapshot{} + err = c.client.Put(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + Name(virtualDiskSnapshot.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualDiskSnapshot). + 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 *virtualDiskSnapshots) UpdateStatus(ctx context.Context, virtualDiskSnapshot *v1alpha2.VirtualDiskSnapshot, opts v1.UpdateOptions) (result *v1alpha2.VirtualDiskSnapshot, err error) { + result = &v1alpha2.VirtualDiskSnapshot{} + err = c.client.Put(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + Name(virtualDiskSnapshot.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(virtualDiskSnapshot). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the virtualDiskSnapshot and deletes it. Returns an error if one occurs. +func (c *virtualDiskSnapshots) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *virtualDiskSnapshots) 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("virtualdisksnapshots"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched virtualDiskSnapshot. +func (c *virtualDiskSnapshots) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VirtualDiskSnapshot, err error) { + result = &v1alpha2.VirtualDiskSnapshot{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("virtualdisksnapshots"). + 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 1fd4e0df5..e4ad41bdc 100644 --- a/api/client/generated/informers/externalversions/core/v1alpha2/interface.go +++ b/api/client/generated/informers/externalversions/core/v1alpha2/interface.go @@ -27,6 +27,8 @@ type Interface interface { ClusterVirtualImages() ClusterVirtualImageInformer // VirtualDisks returns a VirtualDiskInformer. VirtualDisks() VirtualDiskInformer + // VirtualDiskSnapshots returns a VirtualDiskSnapshotInformer. + VirtualDiskSnapshots() VirtualDiskSnapshotInformer // VirtualImages returns a VirtualImageInformer. VirtualImages() VirtualImageInformer // VirtualMachines returns a VirtualMachineInformer. @@ -64,6 +66,11 @@ func (v *version) VirtualDisks() VirtualDiskInformer { return &virtualDiskInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// VirtualDiskSnapshots returns a VirtualDiskSnapshotInformer. +func (v *version) VirtualDiskSnapshots() VirtualDiskSnapshotInformer { + return &virtualDiskSnapshotInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // VirtualImages returns a VirtualImageInformer. func (v *version) VirtualImages() VirtualImageInformer { return &virtualImageInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/api/client/generated/informers/externalversions/core/v1alpha2/virtualdisksnapshot.go b/api/client/generated/informers/externalversions/core/v1alpha2/virtualdisksnapshot.go new file mode 100644 index 000000000..b6aa52b63 --- /dev/null +++ b/api/client/generated/informers/externalversions/core/v1alpha2/virtualdisksnapshot.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" +) + +// VirtualDiskSnapshotInformer provides access to a shared informer and lister for +// VirtualDiskSnapshots. +type VirtualDiskSnapshotInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.VirtualDiskSnapshotLister +} + +type virtualDiskSnapshotInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewVirtualDiskSnapshotInformer constructs a new informer for VirtualDiskSnapshot 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 NewVirtualDiskSnapshotInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredVirtualDiskSnapshotInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredVirtualDiskSnapshotInformer constructs a new informer for VirtualDiskSnapshot 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 NewFilteredVirtualDiskSnapshotInformer(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().VirtualDiskSnapshots(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.VirtualizationV1alpha2().VirtualDiskSnapshots(namespace).Watch(context.TODO(), options) + }, + }, + &corev1alpha2.VirtualDiskSnapshot{}, + resyncPeriod, + indexers, + ) +} + +func (f *virtualDiskSnapshotInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredVirtualDiskSnapshotInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *virtualDiskSnapshotInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&corev1alpha2.VirtualDiskSnapshot{}, f.defaultInformer) +} + +func (f *virtualDiskSnapshotInformer) Lister() v1alpha2.VirtualDiskSnapshotLister { + return v1alpha2.NewVirtualDiskSnapshotLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/generic.go b/api/client/generated/informers/externalversions/generic.go index ff7e015d9..715f77b05 100644 --- a/api/client/generated/informers/externalversions/generic.go +++ b/api/client/generated/informers/externalversions/generic.go @@ -56,6 +56,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().ClusterVirtualImages().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("virtualdisks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualDisks().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("virtualdisksnapshots"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualDiskSnapshots().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("virtualimages"): return &genericInformer{resource: resource.GroupResource(), informer: f.Virtualization().V1alpha2().VirtualImages().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("virtualmachines"): diff --git a/api/client/generated/listers/core/v1alpha2/expansion_generated.go b/api/client/generated/listers/core/v1alpha2/expansion_generated.go index cbdf66178..85471e5df 100644 --- a/api/client/generated/listers/core/v1alpha2/expansion_generated.go +++ b/api/client/generated/listers/core/v1alpha2/expansion_generated.go @@ -29,6 +29,14 @@ type VirtualDiskListerExpansion interface{} // VirtualDiskNamespaceLister. type VirtualDiskNamespaceListerExpansion interface{} +// VirtualDiskSnapshotListerExpansion allows custom methods to be added to +// VirtualDiskSnapshotLister. +type VirtualDiskSnapshotListerExpansion interface{} + +// VirtualDiskSnapshotNamespaceListerExpansion allows custom methods to be added to +// VirtualDiskSnapshotNamespaceLister. +type VirtualDiskSnapshotNamespaceListerExpansion interface{} + // VirtualImageListerExpansion allows custom methods to be added to // VirtualImageLister. type VirtualImageListerExpansion interface{} diff --git a/api/client/generated/listers/core/v1alpha2/virtualdisksnapshot.go b/api/client/generated/listers/core/v1alpha2/virtualdisksnapshot.go new file mode 100644 index 000000000..ac2e910fd --- /dev/null +++ b/api/client/generated/listers/core/v1alpha2/virtualdisksnapshot.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" +) + +// VirtualDiskSnapshotLister helps list VirtualDiskSnapshots. +// All objects returned here must be treated as read-only. +type VirtualDiskSnapshotLister interface { + // List lists all VirtualDiskSnapshots in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VirtualDiskSnapshot, err error) + // VirtualDiskSnapshots returns an object that can list and get VirtualDiskSnapshots. + VirtualDiskSnapshots(namespace string) VirtualDiskSnapshotNamespaceLister + VirtualDiskSnapshotListerExpansion +} + +// virtualDiskSnapshotLister implements the VirtualDiskSnapshotLister interface. +type virtualDiskSnapshotLister struct { + indexer cache.Indexer +} + +// NewVirtualDiskSnapshotLister returns a new VirtualDiskSnapshotLister. +func NewVirtualDiskSnapshotLister(indexer cache.Indexer) VirtualDiskSnapshotLister { + return &virtualDiskSnapshotLister{indexer: indexer} +} + +// List lists all VirtualDiskSnapshots in the indexer. +func (s *virtualDiskSnapshotLister) List(selector labels.Selector) (ret []*v1alpha2.VirtualDiskSnapshot, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VirtualDiskSnapshot)) + }) + return ret, err +} + +// VirtualDiskSnapshots returns an object that can list and get VirtualDiskSnapshots. +func (s *virtualDiskSnapshotLister) VirtualDiskSnapshots(namespace string) VirtualDiskSnapshotNamespaceLister { + return virtualDiskSnapshotNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// VirtualDiskSnapshotNamespaceLister helps list and get VirtualDiskSnapshots. +// All objects returned here must be treated as read-only. +type VirtualDiskSnapshotNamespaceLister interface { + // List lists all VirtualDiskSnapshots in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VirtualDiskSnapshot, err error) + // Get retrieves the VirtualDiskSnapshot from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha2.VirtualDiskSnapshot, error) + VirtualDiskSnapshotNamespaceListerExpansion +} + +// virtualDiskSnapshotNamespaceLister implements the VirtualDiskSnapshotNamespaceLister +// interface. +type virtualDiskSnapshotNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all VirtualDiskSnapshots in the indexer for a given namespace. +func (s virtualDiskSnapshotNamespaceLister) List(selector labels.Selector) (ret []*v1alpha2.VirtualDiskSnapshot, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VirtualDiskSnapshot)) + }) + return ret, err +} + +// Get retrieves the VirtualDiskSnapshot from the indexer for a given namespace and name. +func (s virtualDiskSnapshotNamespaceLister) Get(name string) (*v1alpha2.VirtualDiskSnapshot, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("virtualdisksnapshot"), name) + } + return obj.(*v1alpha2.VirtualDiskSnapshot), nil +} diff --git a/api/core/v1alpha2/finalizers.go b/api/core/v1alpha2/finalizers.go index 45bb8504a..555a2de05 100644 --- a/api/core/v1alpha2/finalizers.go +++ b/api/core/v1alpha2/finalizers.go @@ -17,14 +17,14 @@ limitations under the License. package v1alpha2 const ( - FinalizerCVIProtection = "virtualization.deckhouse.io/cvi-protection" - FinalizerVIProtection = "virtualization.deckhouse.io/vi-protection" - FinalizerVDProtection = "virtualization.deckhouse.io/vd-protection" - FinalizerKVVMProtection = "virtualization.deckhouse.io/kvvm-protection" - FinalizerVMOPProtection = "virtualization.deckhouse.io/vmop-protection" - FinalizerVMCPUProtection = "virtualization.deckhouse.io/vmcpu-protection" - FinalizerIPAddressProtection = "virtualization.deckhouse.io/vmip-protection" - FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" + FinalizerCVIProtection = "virtualization.deckhouse.io/cvi-protection" + FinalizerVIProtection = "virtualization.deckhouse.io/vi-protection" + FinalizerVDProtection = "virtualization.deckhouse.io/vd-protection" + FinalizerKVVMProtection = "virtualization.deckhouse.io/kvvm-protection" + FinalizerVMOPProtection = "virtualization.deckhouse.io/vmop-protection" + FinalizerIPAddressProtection = "virtualization.deckhouse.io/vmip-protection" + FinalizerPodProtection = "virtualization.deckhouse.io/pod-protection" + FinalizerVDSnapshotProtection = "virtualization.deckhouse.io/vdsnapshot-protection" FinalizerCVICleanup = "virtualization.deckhouse.io/cvi-cleanup" FinalizerVDCleanup = "virtualization.deckhouse.io/vd-cleanup" @@ -32,7 +32,8 @@ const ( FinalizerVMCleanup = "virtualization.deckhouse.io/vm-cleanup" FinalizerIPAddressCleanup = "virtualization.deckhouse.io/vmip-cleanup" FinalizerIPAddressLeaseCleanup = "virtualization.deckhouse.io/vmipl-cleanup" - FinalizerVMBDACleanup = "virtualization.deckhouse.io/vmbda-cleanup" + FinalizerVDSnapshotCleanup = "virtualization.deckhouse.io/vdsnapshot-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 d841323a0..62b5c908e 100644 --- a/api/core/v1alpha2/register.go +++ b/api/core/v1alpha2/register.go @@ -80,6 +80,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineIPAddressLeaseList{}, &VirtualMachineOperation{}, &VirtualMachineOperationList{}, + &VirtualDiskSnapshot{}, + &VirtualDiskSnapshotList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/api/core/v1alpha2/vdcondition/condition.go b/api/core/v1alpha2/vdcondition/condition.go index a381b2d9c..fd0b8fe43 100644 --- a/api/core/v1alpha2/vdcondition/condition.go +++ b/api/core/v1alpha2/vdcondition/condition.go @@ -26,6 +26,8 @@ const ( ReadyType Type = "Ready" // ResizedType indicates whether the disk resizing operation is completed. ResizedType Type = "Resized" + // SnapshottingType indicates whether the disk snapshotting operation is in progress. + SnapshottingType Type = "Snapshotting" ) type ( @@ -35,6 +37,8 @@ type ( ReadyReason = string // ResizedReason represents the various reasons for the Resized condition type. ResizedReason = string + // SnapshottingReason represents the various reasons for the Snapshotting condition type. + SnapshottingReason = string ) const ( @@ -46,6 +50,8 @@ const ( ImageNotReady DatasourceReadyReason = "ImageNotReady" // ClusterImageNotReady indicates that the `VirtualDisk` datasource is not ready, which prevents the import process from starting. ClusterImageNotReady DatasourceReadyReason = "ClusterImageNotReady" + // VirtualDiskSnapshotNotReady indicates that the `VirtualDiskSnapshot` datasource is not ready, which prevents the import process from starting. + VirtualDiskSnapshotNotReady DatasourceReadyReason = "VirtualDiskSnapshot" // WaitForUserUpload indicates that the `VirtualDisk` is waiting for the user to upload a datasource for the import process to continue. WaitForUserUpload ReadyReason = "WaitForUserUpload" @@ -62,10 +68,19 @@ const ( // Lost indicates that the underlying PersistentVolumeClaim has been lost and the `VirtualDisk` can no longer be used. Lost ReadyReason = "PVCLost" - // NotRequested indicates that the resize operation has not been requested yet. - NotRequested ResizedReason = "NotRequested" + // ResizingNotRequested indicates that the resize operation has not been requested yet. + ResizingNotRequested ResizedReason = "NotRequested" // InProgress indicates that the resize request has been detected and the operation is currently in progress. InProgress ResizedReason = "InProgress" // Resized indicates that the resize operation has been successfully completed. Resized ResizedReason = "Resized" + // ResizingNotAvailable indicates that the resize operation is not available for now. + ResizingNotAvailable SnapshottingReason = "NotAvailable" + + // SnapshottingNotRequested indicates that the snapshotting operation has been successfully started and is in progress now. + SnapshottingNotRequested SnapshottingReason = "NotRequested" + // Snapshotting indicates that the snapshotting operation has been successfully started and is in progress now. + Snapshotting SnapshottingReason = "Snapshotting" + // SnapshottingNotAvailable indicates that the snapshotting operation is not available for now. + SnapshottingNotAvailable SnapshottingReason = "NotAvailable" ) diff --git a/api/core/v1alpha2/vdscondition/condition.go b/api/core/v1alpha2/vdscondition/condition.go new file mode 100644 index 000000000..60842c3bc --- /dev/null +++ b/api/core/v1alpha2/vdscondition/condition.go @@ -0,0 +1,56 @@ +/* +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 vdscondition + +// Type represents the various condition types for the `VirtualDiskSnapshot`. +type Type = string + +const ( + // VirtualDiskReadyType indicates that the source `VirtualDisk` is ready for snapshotting. + VirtualDiskReadyType Type = "VirtualDiskReady" + // VirtualDiskSnapshotReadyType indicates that the virtual disk snapshot has been successfully taken and is ready for use. + VirtualDiskSnapshotReadyType Type = "VirtualDiskSnapshotReady" +) + +type ( + // VirtualDiskReadyReason represents the various reasons for the `VirtualDiskReady` condition type. + VirtualDiskReadyReason = string + // VirtualDiskSnapshotReadyReason represents the various reasons for the `VirtualDiskSnapshotReady` condition type. + VirtualDiskSnapshotReadyReason = string +) + +const ( + // VirtualDiskReady signifies that the source virtual disk is ready for snapshotting, allowing the snapshot process to begin. + VirtualDiskReady VirtualDiskReadyReason = "VirtualDiskReady" + // VirtualDiskNotReadyForSnapshotting signifies that the source virtual disk is not ready for snapshotting, preventing the snapshot process from starting. + VirtualDiskNotReadyForSnapshotting VirtualDiskReadyReason = "VirtualDiskNotReadyForSnapshotting" + + // WaitingForTheVirtualDisk signifies that the snapshot process is waiting for the virtual disk to become ready for snapshotting. + WaitingForTheVirtualDisk VirtualDiskSnapshotReadyReason = "WaitingForTheVirtualDisk" + // PotentiallyInconsistent signifies that the snapshotting process cannot begin because creating a snapshot of virtual disk attached to the running virtual machine might result in an inconsistent snapshot. + PotentiallyInconsistent VirtualDiskSnapshotReadyReason = "PotentiallyInconsistent" + // VolumeSnapshotLost signifies that the underling `VolumeSnapshot` is lost: cannot use the virtual disk snapshot as a data source. + VolumeSnapshotLost VirtualDiskSnapshotReadyReason = "Lost" + // FileSystemFreezing signifies that the `VirtualDiskSnapshot` resource is in the process of freezing the filesystem of the virtual machine associated with the source virtual disk. + FileSystemFreezing VirtualDiskSnapshotReadyReason = "FileSystemFreezing" + // Snapshotting signifies that the `VirtualDiskSnapshot` resource is in the process of taking a snapshot of the virtual disk. + Snapshotting VirtualDiskSnapshotReadyReason = "Snapshotting" + // VirtualDiskSnapshotReady signifies that the snapshot process is complete and the `VirtualDiskSnapshot` is ready for use. + VirtualDiskSnapshotReady VirtualDiskSnapshotReadyReason = "VirtualDiskSnapshotReady" + // VirtualDiskSnapshotFailed signifies that the snapshot process has failed. + VirtualDiskSnapshotFailed VirtualDiskSnapshotReadyReason = "VirtualDiskSnapshotFailed" +) diff --git a/api/core/v1alpha2/virtual_disk.go b/api/core/v1alpha2/virtual_disk.go index 83c6ccc7d..ac2f7f3a5 100644 --- a/api/core/v1alpha2/virtual_disk.go +++ b/api/core/v1alpha2/virtual_disk.go @@ -96,6 +96,7 @@ type VirtualDiskObjectRefKind string const ( VirtualDiskObjectRefKindVirtualImage VirtualDiskObjectRefKind = "VirtualImage" VirtualDiskObjectRefKindClusterVirtualImage VirtualDiskObjectRefKind = "ClusterVirtualImage" + VirtualDiskObjectRefKindVirtualDiskSnapshot VirtualDiskObjectRefKind = "VirtualDiskSnapshot" ) type DiskTarget struct { diff --git a/api/core/v1alpha2/virtual_disk_snapshot.go b/api/core/v1alpha2/virtual_disk_snapshot.go new file mode 100644 index 000000000..7ee21adae --- /dev/null +++ b/api/core/v1alpha2/virtual_disk_snapshot.go @@ -0,0 +1,68 @@ +/* +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 v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + VirtualDiskSnapshotKind = "VirtualDiskSnapshot" + VirtualDiskSnapshotResource = "virtualdisksnapshots" +) + +// VirtualDiskSnapshot +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VirtualDiskSnapshot struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VirtualDiskSnapshotSpec `json:"spec"` + Status VirtualDiskSnapshotStatus `json:"status,omitempty"` +} + +// VirtualDiskSnapshotList contains a list of VirtualDiskSnapshot +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VirtualDiskSnapshotList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []VirtualDiskSnapshot `json:"items"` +} + +type VirtualDiskSnapshotSpec struct { + VirtualDiskName string `json:"virtualDiskName"` + VolumeSnapshotClassName string `json:"volumeSnapshotClassName"` + AllowPotentiallyInconsistent bool `json:"allowPotentiallyInconsistent"` +} + +type VirtualDiskSnapshotStatus struct { + Phase VirtualDiskSnapshotPhase `json:"phase"` + VolumeSnapshotName string `json:"volumeSnapshotName,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +type VirtualDiskSnapshotPhase string + +const ( + VirtualDiskSnapshotPhasePending VirtualDiskSnapshotPhase = "Pending" + VirtualDiskSnapshotPhaseInProgress VirtualDiskSnapshotPhase = "InProgress" + VirtualDiskSnapshotPhaseReady VirtualDiskSnapshotPhase = "Ready" + VirtualDiskSnapshotPhaseFailed VirtualDiskSnapshotPhase = "Failed" + VirtualDiskSnapshotPhaseTerminating VirtualDiskSnapshotPhase = "Terminating" +) diff --git a/api/core/v1alpha2/vmcondition/condition.go b/api/core/v1alpha2/vmcondition/condition.go index c240de596..5129163d4 100644 --- a/api/core/v1alpha2/vmcondition/condition.go +++ b/api/core/v1alpha2/vmcondition/condition.go @@ -35,6 +35,7 @@ const ( TypeAgentVersionNotSupported Type = "AgentVersionNotSupported" TypeConfigurationApplied Type = "ConfigurationApplied" TypeAwaitingRestartToApplyConfiguration Type = "AwaitingRestartToApplyConfiguration" + TypeFilesystemReady Type = "FilesystemReady" ) type Reason string @@ -79,4 +80,8 @@ const ( ReasonVmIsNotRunning Reason = "VirtualMachineNotRunning" ReasonVmIsRunning Reason = "VirtualMachineRunning" ReasonInternalVirtualMachineError Reason = "InternalVirtualMachineError" + + ReasonFilesystemReady Reason = "Ready" + ReasonFilesystemFrozen Reason = "Frozen" + ReasonFilesystemNotReady Reason = "NotReady" ) diff --git a/api/core/v1alpha2/zz_generated.deepcopy.go b/api/core/v1alpha2/zz_generated.deepcopy.go index 106176653..4511a7e50 100644 --- a/api/core/v1alpha2/zz_generated.deepcopy.go +++ b/api/core/v1alpha2/zz_generated.deepcopy.go @@ -886,6 +886,106 @@ func (in *VirtualDiskPersistentVolumeClaim) DeepCopy() *VirtualDiskPersistentVol return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualDiskSnapshot) DeepCopyInto(out *VirtualDiskSnapshot) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualDiskSnapshot. +func (in *VirtualDiskSnapshot) DeepCopy() *VirtualDiskSnapshot { + if in == nil { + return nil + } + out := new(VirtualDiskSnapshot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualDiskSnapshot) 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 *VirtualDiskSnapshotList) DeepCopyInto(out *VirtualDiskSnapshotList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VirtualDiskSnapshot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualDiskSnapshotList. +func (in *VirtualDiskSnapshotList) DeepCopy() *VirtualDiskSnapshotList { + if in == nil { + return nil + } + out := new(VirtualDiskSnapshotList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualDiskSnapshotList) 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 *VirtualDiskSnapshotSpec) DeepCopyInto(out *VirtualDiskSnapshotSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualDiskSnapshotSpec. +func (in *VirtualDiskSnapshotSpec) DeepCopy() *VirtualDiskSnapshotSpec { + if in == nil { + return nil + } + out := new(VirtualDiskSnapshotSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualDiskSnapshotStatus) DeepCopyInto(out *VirtualDiskSnapshotStatus) { + *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 VirtualDiskSnapshotStatus. +func (in *VirtualDiskSnapshotStatus) DeepCopy() *VirtualDiskSnapshotStatus { + if in == nil { + return nil + } + out := new(VirtualDiskSnapshotStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualDiskSpec) DeepCopyInto(out *VirtualDiskSpec) { *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 c5c919753..2240ea68a 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -72,6 +72,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskList": schema_virtualization_api_core_v1alpha2_VirtualDiskList(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskObjectRef": schema_virtualization_api_core_v1alpha2_VirtualDiskObjectRef(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskPersistentVolumeClaim": schema_virtualization_api_core_v1alpha2_VirtualDiskPersistentVolumeClaim(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshot": schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshot(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotList": schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotList(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotSpec": schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotSpec(ref), + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotStatus": schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotStatus(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSpec": schema_virtualization_api_core_v1alpha2_VirtualDiskSpec(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskStats": schema_virtualization_api_core_v1alpha2_VirtualDiskStats(ref), "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskStatsCreationDuration": schema_virtualization_api_core_v1alpha2_VirtualDiskStatsCreationDuration(ref), @@ -119,8 +123,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "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), + "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineFreeze": schema_virtualization_api_subresources_v1alpha2_VirtualMachineFreeze(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachinePortForward": schema_virtualization_api_subresources_v1alpha2_VirtualMachinePortForward(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineRemoveVolume": schema_virtualization_api_subresources_v1alpha2_VirtualMachineRemoveVolume(ref), + "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineUnfreeze": schema_virtualization_api_subresources_v1alpha2_VirtualMachineUnfreeze(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineVNC": schema_virtualization_api_subresources_v1alpha2_VirtualMachineVNC(ref), "k8s.io/api/core/v1.AWSElasticBlockStoreVolumeSource": schema_k8sio_api_core_v1_AWSElasticBlockStoreVolumeSource(ref), "k8s.io/api/core/v1.Affinity": schema_k8sio_api_core_v1_Affinity(ref), @@ -1705,7 +1711,6 @@ func schema_virtualization_api_core_v1alpha2_SizingPolicyMemory(ref common.Refer }, }, }, - Required: []string{"step", "perCore"}, }, }, Dependencies: []string{ @@ -2063,6 +2068,184 @@ func schema_virtualization_api_core_v1alpha2_VirtualDiskPersistentVolumeClaim(re } } +func schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VirtualDiskSnapshot", + 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.VirtualDiskSnapshotSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotSpec", "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshotStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "VirtualDiskSnapshotList contains a list of VirtualDiskSnapshot", + 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.VirtualDiskSnapshot"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/virtualization/api/core/v1alpha2.VirtualDiskSnapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "virtualDiskName": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "volumeSnapshotClassName": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "allowPotentiallyInconsistent": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"virtualDiskName", "volumeSnapshotClassName", "allowPotentiallyInconsistent"}, + }, + }, + } +} + +func schema_virtualization_api_core_v1alpha2_VirtualDiskSnapshotStatus(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: "", + }, + }, + "volumeSnapshotName": { + SchemaProps: spec.SchemaProps{ + 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{ + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + Required: []string{"phase"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, + } +} + func schema_virtualization_api_core_v1alpha2_VirtualDiskSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4183,6 +4366,40 @@ func schema_virtualization_api_subresources_v1alpha2_VirtualMachineConsole(ref c } } +func schema_virtualization_api_subresources_v1alpha2_VirtualMachineFreeze(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + 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: "", + }, + }, + "unfreezeTimeout": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, + }, + Required: []string{"unfreezeTimeout"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"}, + } +} + func schema_virtualization_api_subresources_v1alpha2_VirtualMachinePortForward(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4250,6 +4467,32 @@ func schema_virtualization_api_subresources_v1alpha2_VirtualMachineRemoveVolume( } } +func schema_virtualization_api_subresources_v1alpha2_VirtualMachineUnfreeze(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + 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: "", + }, + }, + }, + }, + }, + } +} + func schema_virtualization_api_subresources_v1alpha2_VirtualMachineVNC(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/api/subresources/register.go b/api/subresources/register.go index 4b81d7f3a..1f714aff0 100644 --- a/api/subresources/register.go +++ b/api/subresources/register.go @@ -56,6 +56,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachinePortForward{}, &VirtualMachineAddVolume{}, &VirtualMachineRemoveVolume{}, + &VirtualMachineFreeze{}, + &VirtualMachineUnfreeze{}, &virtv2.VirtualMachine{}, &virtv2.VirtualMachineList{}, ) diff --git a/api/subresources/types.go b/api/subresources/types.go index 9b800f223..321c2e3d6 100644 --- a/api/subresources/types.go +++ b/api/subresources/types.go @@ -60,3 +60,21 @@ type VirtualMachineAddVolume struct { type VirtualMachineRemoveVolume struct { metav1.TypeMeta } + +// +genclient +// +genclient:readonly +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type VirtualMachineFreeze struct { + metav1.TypeMeta + + UnfreezeTimeout *metav1.Duration +} + +// +genclient +// +genclient:readonly +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type VirtualMachineUnfreeze struct { + metav1.TypeMeta +} diff --git a/api/subresources/v1alpha2/register.go b/api/subresources/v1alpha2/register.go index 8d5e381a7..87d8d39c6 100644 --- a/api/subresources/v1alpha2/register.go +++ b/api/subresources/v1alpha2/register.go @@ -56,6 +56,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachinePortForward{}, &VirtualMachineAddVolume{}, &VirtualMachineRemoveVolume{}, + &VirtualMachineFreeze{}, + &VirtualMachineUnfreeze{}, &virtv2.VirtualMachine{}, &virtv2.VirtualMachineList{}, ) diff --git a/api/subresources/v1alpha2/types.go b/api/subresources/v1alpha2/types.go index ccefa45d8..9e1437bc0 100644 --- a/api/subresources/v1alpha2/types.go +++ b/api/subresources/v1alpha2/types.go @@ -65,3 +65,23 @@ type VirtualMachineAddVolume struct { type VirtualMachineRemoveVolume struct { metav1.TypeMeta `json:",inline"` } + +// +genclient +// +genclient:readonly +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:conversion-gen:explicit-from=net/url.Values + +type VirtualMachineFreeze struct { + metav1.TypeMeta `json:",inline"` + + UnfreezeTimeout *metav1.Duration `json:"unfreezeTimeout"` +} + +// +genclient +// +genclient:readonly +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:conversion-gen:explicit-from=net/url.Values + +type VirtualMachineUnfreeze struct { + metav1.TypeMeta `json:",inline"` +} diff --git a/api/subresources/v1alpha2/zz_generated.conversion.go b/api/subresources/v1alpha2/zz_generated.conversion.go index e62c6817f..575db04a4 100644 --- a/api/subresources/v1alpha2/zz_generated.conversion.go +++ b/api/subresources/v1alpha2/zz_generated.conversion.go @@ -22,8 +22,10 @@ package v1alpha2 import ( url "net/url" + unsafe "unsafe" subresources "github.com/deckhouse/virtualization/api/subresources" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -55,6 +57,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*VirtualMachineFreeze)(nil), (*subresources.VirtualMachineFreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_VirtualMachineFreeze_To_subresources_VirtualMachineFreeze(a.(*VirtualMachineFreeze), b.(*subresources.VirtualMachineFreeze), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*subresources.VirtualMachineFreeze)(nil), (*VirtualMachineFreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_subresources_VirtualMachineFreeze_To_v1alpha2_VirtualMachineFreeze(a.(*subresources.VirtualMachineFreeze), b.(*VirtualMachineFreeze), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*VirtualMachinePortForward)(nil), (*subresources.VirtualMachinePortForward)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_VirtualMachinePortForward_To_subresources_VirtualMachinePortForward(a.(*VirtualMachinePortForward), b.(*subresources.VirtualMachinePortForward), scope) }); err != nil { @@ -75,6 +87,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*VirtualMachineUnfreeze)(nil), (*subresources.VirtualMachineUnfreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(a.(*VirtualMachineUnfreeze), b.(*subresources.VirtualMachineUnfreeze), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*subresources.VirtualMachineUnfreeze)(nil), (*VirtualMachineUnfreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_subresources_VirtualMachineUnfreeze_To_v1alpha2_VirtualMachineUnfreeze(a.(*subresources.VirtualMachineUnfreeze), b.(*VirtualMachineUnfreeze), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*VirtualMachineVNC)(nil), (*subresources.VirtualMachineVNC)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_VirtualMachineVNC_To_subresources_VirtualMachineVNC(a.(*VirtualMachineVNC), b.(*subresources.VirtualMachineVNC), scope) }); err != nil { @@ -95,6 +117,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineFreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v1alpha2_VirtualMachineFreeze(a.(*url.Values), b.(*VirtualMachineFreeze), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachinePortForward)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_url_Values_To_v1alpha2_VirtualMachinePortForward(a.(*url.Values), b.(*VirtualMachinePortForward), scope) }); err != nil { @@ -105,6 +132,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineUnfreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v1alpha2_VirtualMachineUnfreeze(a.(*url.Values), b.(*VirtualMachineUnfreeze), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineVNC)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_url_Values_To_v1alpha2_VirtualMachineVNC(a.(*url.Values), b.(*VirtualMachineVNC), scope) }); err != nil { @@ -171,6 +203,42 @@ func Convert_url_Values_To_v1alpha2_VirtualMachineConsole(in *url.Values, out *V return autoConvert_url_Values_To_v1alpha2_VirtualMachineConsole(in, out, s) } +func autoConvert_v1alpha2_VirtualMachineFreeze_To_subresources_VirtualMachineFreeze(in *VirtualMachineFreeze, out *subresources.VirtualMachineFreeze, s conversion.Scope) error { + out.UnfreezeTimeout = (*v1.Duration)(unsafe.Pointer(in.UnfreezeTimeout)) + return nil +} + +// Convert_v1alpha2_VirtualMachineFreeze_To_subresources_VirtualMachineFreeze is an autogenerated conversion function. +func Convert_v1alpha2_VirtualMachineFreeze_To_subresources_VirtualMachineFreeze(in *VirtualMachineFreeze, out *subresources.VirtualMachineFreeze, s conversion.Scope) error { + return autoConvert_v1alpha2_VirtualMachineFreeze_To_subresources_VirtualMachineFreeze(in, out, s) +} + +func autoConvert_subresources_VirtualMachineFreeze_To_v1alpha2_VirtualMachineFreeze(in *subresources.VirtualMachineFreeze, out *VirtualMachineFreeze, s conversion.Scope) error { + out.UnfreezeTimeout = (*v1.Duration)(unsafe.Pointer(in.UnfreezeTimeout)) + return nil +} + +// Convert_subresources_VirtualMachineFreeze_To_v1alpha2_VirtualMachineFreeze is an autogenerated conversion function. +func Convert_subresources_VirtualMachineFreeze_To_v1alpha2_VirtualMachineFreeze(in *subresources.VirtualMachineFreeze, out *VirtualMachineFreeze, s conversion.Scope) error { + return autoConvert_subresources_VirtualMachineFreeze_To_v1alpha2_VirtualMachineFreeze(in, out, s) +} + +func autoConvert_url_Values_To_v1alpha2_VirtualMachineFreeze(in *url.Values, out *VirtualMachineFreeze, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + if values, ok := map[string][]string(*in)["unfreezeTimeout"]; ok && len(values) > 0 { + // FIXME: out.UnfreezeTimeout is of not yet supported type and requires manual conversion + } else { + out.UnfreezeTimeout = nil + } + return nil +} + +// Convert_url_Values_To_v1alpha2_VirtualMachineFreeze is an autogenerated conversion function. +func Convert_url_Values_To_v1alpha2_VirtualMachineFreeze(in *url.Values, out *VirtualMachineFreeze, s conversion.Scope) error { + return autoConvert_url_Values_To_v1alpha2_VirtualMachineFreeze(in, out, s) +} + func autoConvert_v1alpha2_VirtualMachinePortForward_To_subresources_VirtualMachinePortForward(in *VirtualMachinePortForward, out *subresources.VirtualMachinePortForward, s conversion.Scope) error { out.Protocol = in.Protocol out.Port = in.Port @@ -247,6 +315,35 @@ func Convert_url_Values_To_v1alpha2_VirtualMachineRemoveVolume(in *url.Values, o return autoConvert_url_Values_To_v1alpha2_VirtualMachineRemoveVolume(in, out, s) } +func autoConvert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(in *VirtualMachineUnfreeze, out *subresources.VirtualMachineUnfreeze, s conversion.Scope) error { + return nil +} + +// Convert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze is an autogenerated conversion function. +func Convert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(in *VirtualMachineUnfreeze, out *subresources.VirtualMachineUnfreeze, s conversion.Scope) error { + return autoConvert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(in, out, s) +} + +func autoConvert_subresources_VirtualMachineUnfreeze_To_v1alpha2_VirtualMachineUnfreeze(in *subresources.VirtualMachineUnfreeze, out *VirtualMachineUnfreeze, s conversion.Scope) error { + return nil +} + +// Convert_subresources_VirtualMachineUnfreeze_To_v1alpha2_VirtualMachineUnfreeze is an autogenerated conversion function. +func Convert_subresources_VirtualMachineUnfreeze_To_v1alpha2_VirtualMachineUnfreeze(in *subresources.VirtualMachineUnfreeze, out *VirtualMachineUnfreeze, s conversion.Scope) error { + return autoConvert_subresources_VirtualMachineUnfreeze_To_v1alpha2_VirtualMachineUnfreeze(in, out, s) +} + +func autoConvert_url_Values_To_v1alpha2_VirtualMachineUnfreeze(in *url.Values, out *VirtualMachineUnfreeze, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + return nil +} + +// Convert_url_Values_To_v1alpha2_VirtualMachineUnfreeze is an autogenerated conversion function. +func Convert_url_Values_To_v1alpha2_VirtualMachineUnfreeze(in *url.Values, out *VirtualMachineUnfreeze, s conversion.Scope) error { + return autoConvert_url_Values_To_v1alpha2_VirtualMachineUnfreeze(in, out, s) +} + func autoConvert_v1alpha2_VirtualMachineVNC_To_subresources_VirtualMachineVNC(in *VirtualMachineVNC, out *subresources.VirtualMachineVNC, s conversion.Scope) error { return nil } diff --git a/api/subresources/v1alpha2/zz_generated.deepcopy.go b/api/subresources/v1alpha2/zz_generated.deepcopy.go index 89ba8a7b6..4c52d8567 100644 --- a/api/subresources/v1alpha2/zz_generated.deepcopy.go +++ b/api/subresources/v1alpha2/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha2 import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -74,6 +75,36 @@ func (in *VirtualMachineConsole) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineFreeze) DeepCopyInto(out *VirtualMachineFreeze) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.UnfreezeTimeout != nil { + in, out := &in.UnfreezeTimeout, &out.UnfreezeTimeout + *out = new(v1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineFreeze. +func (in *VirtualMachineFreeze) DeepCopy() *VirtualMachineFreeze { + if in == nil { + return nil + } + out := new(VirtualMachineFreeze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineFreeze) 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 *VirtualMachinePortForward) DeepCopyInto(out *VirtualMachinePortForward) { *out = *in @@ -124,6 +155,31 @@ func (in *VirtualMachineRemoveVolume) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineUnfreeze) DeepCopyInto(out *VirtualMachineUnfreeze) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineUnfreeze. +func (in *VirtualMachineUnfreeze) DeepCopy() *VirtualMachineUnfreeze { + if in == nil { + return nil + } + out := new(VirtualMachineUnfreeze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineUnfreeze) 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 *VirtualMachineVNC) DeepCopyInto(out *VirtualMachineVNC) { *out = *in diff --git a/api/subresources/zz_generated.deepcopy.go b/api/subresources/zz_generated.deepcopy.go index 02ab1e07f..1e9627216 100644 --- a/api/subresources/zz_generated.deepcopy.go +++ b/api/subresources/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package subresources import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -74,6 +75,36 @@ func (in *VirtualMachineConsole) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineFreeze) DeepCopyInto(out *VirtualMachineFreeze) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.UnfreezeTimeout != nil { + in, out := &in.UnfreezeTimeout, &out.UnfreezeTimeout + *out = new(v1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineFreeze. +func (in *VirtualMachineFreeze) DeepCopy() *VirtualMachineFreeze { + if in == nil { + return nil + } + out := new(VirtualMachineFreeze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineFreeze) 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 *VirtualMachinePortForward) DeepCopyInto(out *VirtualMachinePortForward) { *out = *in @@ -124,6 +155,31 @@ func (in *VirtualMachineRemoveVolume) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineUnfreeze) DeepCopyInto(out *VirtualMachineUnfreeze) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineUnfreeze. +func (in *VirtualMachineUnfreeze) DeepCopy() *VirtualMachineUnfreeze { + if in == nil { + return nil + } + out := new(VirtualMachineUnfreeze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineUnfreeze) 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 *VirtualMachineVNC) DeepCopyInto(out *VirtualMachineVNC) { *out = *in diff --git a/crds/doc-ru-virtualdisk.yaml b/crds/doc-ru-virtualdisk.yaml index a878efdfc..5a7e4faa2 100644 --- a/crds/doc-ru-virtualdisk.yaml +++ b/crds/doc-ru-virtualdisk.yaml @@ -69,14 +69,14 @@ spec: * xz. objectRef: description: | - Для создания образа использовать существующий `VirtualImage` или `ClusterVirtualImage`. + Для создания образа использовать существующий `VirtualImage`, `ClusterVirtualImage` или `VirtualDiskSnapshot`. properties: kind: description: | - Ссылка на существующий `VirtualImage` или `ClusterVirtualImage`. + Ссылка на существующий `VirtualImage`, `ClusterVirtualImage` или `VirtualDiskSnapshot`. name: description: | - Имя существующего `VirtualImage` или `ClusterVirtualImage`. + Имя существующего `VirtualImage`, `ClusterVirtualImage` или `VirtualDiskSnapshot`. type: description: | Тип источника, из которого будет создан диск: diff --git a/crds/doc-ru-virtualdisksnapshot.yaml b/crds/doc-ru-virtualdisksnapshot.yaml new file mode 100644 index 000000000..41dd5fe9e --- /dev/null +++ b/crds/doc-ru-virtualdisksnapshot.yaml @@ -0,0 +1,61 @@ +spec: + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: | + Предоставляет ресурс для создания снимков существующих виртуальных дисков, которые могут быть использованы в качестве источников данных для создания новых виртуальных дисков. + + Под капотом автоматически создается ресурс `VolumeSnapshot`. + properties: + spec: + properties: + virtualDiskName: + description: | + Имя виртуального диска, для которого будет создан снимок. + volumeSnapshotClassName: + description: | + Имя класса снимков томов, который будет использоваться при создании снимка виртуального диска. + allowPotentiallyInconsistent: + description: | + Разрешать ли создание снимка диска виртуальной машины, подключенного к работающей виртуальной машине без агента, которую нет возможности заморозить. + + Если значение установлено в false, снимок виртуального диска будет создан только в следующих случаях: + - виртуальный диск не подключен ни к одной виртуальной машине; + - виртуальный диск подключен к виртуальной машине, которая выключена; + - виртуальный диск подключен к виртуальной машине с агентом, и операция заморозки прошла успешно. + status: + properties: + conditions: + description: | + Последнее подтвержденное состояние данного ресурса. + items: + properties: + lastProbeTime: + description: Время проверки условия. + lastTransitionTime: + description: Время перехода условия из одного состояния в другое. + message: + description: Удобочитаемое сообщение с подробной информацией о последнем переходе. + reason: + description: Краткая причина последнего перехода состояния. + status: + description: | + Статус условия. Возможные значения: `True`, `False`, `Unknown`. + type: + description: Тип условия. + volumeSnapshotName: + description: | + Имя созданного ресурса `VolumeSnapshot`. + phase: + description: | + Текущее состояние ресурса `VirtualDiskSnapshot`: + + * Pending — ресурс был создан и находится в очереди ожидания. + * InProgress — идет процесс создания снимка виртуального диска. + * Ready — создание снимка успешно завершено, и снимок виртуального диска доступен для использования. + * Failed — произошла ошибка во время процесса создания снимка виртуального диска. + * Terminating — ресурс находится в процессе удаления. + observedGeneration: + description: | + Поколение ресурса, которое в последний раз обрабатывалось контроллером. diff --git a/crds/doc-ru-virtualmachineblockdeviceattachment.yaml b/crds/doc-ru-virtualmachineblockdeviceattachment.yaml index ded53f415..f3dd2e94a 100644 --- a/crds/doc-ru-virtualmachineblockdeviceattachment.yaml +++ b/crds/doc-ru-virtualmachineblockdeviceattachment.yaml @@ -51,7 +51,7 @@ spec: * InProgress — диск в процессе подключения к ВМ. * Attached — диск подключен к ВМ. * Failed — возникла проблема с подключением диска. - * Terminating - Ресурс находится в процессе удаления. + * Terminating - ресурс находится в процессе удаления. virtualMachineName: description: | Имя виртуальной машины, к которой подключен этот диск. diff --git a/crds/virtualdisk.yaml b/crds/virtualdisk.yaml index 3afde7051..55aa67dd8 100644 --- a/crds/virtualdisk.yaml +++ b/crds/virtualdisk.yaml @@ -167,19 +167,20 @@ spec: objectRef: type: object description: | - Use an existing `VirtualImage` or `ClusterVirtualImage` to create an image. + Use an existing `VirtualImage`, `ClusterVirtualImage` or `VirtualDiskSnapshot` to create a disk. required: ["kind", "name"] properties: kind: type: string - description: A kind of existing `VirtualImage` or `ClusterVirtualImage`. + description: A kind of existing `VirtualImage`, `ClusterVirtualImage` or `VirtualDiskSnapshot`. enum: - - "ClusterVirtualImage" - "VirtualImage" + - "ClusterVirtualImage" + - "VirtualDiskSnapshot" name: type: string description: | - A name of existing `VirtualImage` or `ClusterVirtualImage`. + A name of existing `VirtualImage`, `ClusterVirtualImage` or `VirtualDiskSnapshot`. oneOf: - properties: type: diff --git a/crds/virtualdisksnapshot.yaml b/crds/virtualdisksnapshot.yaml new file mode 100644 index 000000000..b5b4f5d85 --- /dev/null +++ b/crds/virtualdisksnapshot.yaml @@ -0,0 +1,119 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: virtualdisksnapshots.virtualization.deckhouse.io + labels: + heritage: deckhouse + module: virtualization +spec: + group: virtualization.deckhouse.io + scope: Namespaced + names: + categories: + - all + - virtualization + plural: virtualdisksnapshots + singular: virtualdisksnapshot + kind: VirtualDiskSnapshot + shortNames: + - vdsnapshot + - vdsnapshots + preserveUnknownFields: false + versions: + - name: v1alpha2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + description: | + Provides a resource for creating snapshots of existing virtual disks, which can be used as data sources for generating new virtual disks + + A `VolumeSnapshot` is created under the hood of the resource. + required: + - spec + properties: + spec: + type: object + required: + - virtualDiskName + - volumeSnapshotClassName + properties: + virtualDiskName: + type: string + description: | + The name of the virtual disk to take snapshot. + volumeSnapshotClassName: + type: string + description: | + The name of the volume snapshot class to use while snapshotting virtual disk. + allowPotentiallyInconsistent: + type: boolean + default: false + description: | + Specify whether to allow the snapshotting of a virtual machine's disk that is attached to a running virtual machine without an agent, and as a result, cannot be frozen. + + If value is false, the snapshot of the virtual disk will be taken only in the following scenarios: + - the virtual disk is not attached to any virtual machine. + - the virtual disk is attached to a virtual machine that is powered off. + - the virtual disk is attached to a virtual machine with an agent, and the freeze operation was successful. + status: + type: object + properties: + conditions: + description: | + The latest available observations of an object's current state. + type: array + items: + type: object + properties: + lastProbeTime: + description: Last time the condition was checked. + format: date-time + type: string + lastTransitionTime: + description: Last time the condition transit from one status to another. + format: date-time + type: string + message: + description: Human readable message indicating details about last transition. + type: string + reason: + description: (brief) reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + enum: ["True", "False", "Unknown"] + type: + description: Type of condition. + type: string + required: + - status + - type + volumeSnapshotName: + type: string + description: | + The name of the `VolumeSnapshot` created automatically by this resource. + phase: + type: string + description: | + Current status of `VirtualDiskSnapshot` 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 disk 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"] + observedGeneration: + type: integer + description: | + The generation last processed by the controller. + additionalPrinterColumns: + - name: Phase + type: string + jsonPath: .status.phase + subresources: + status: {} diff --git a/crds/virtualmachineblockdeviceattachment.yaml b/crds/virtualmachineblockdeviceattachment.yaml index 88d2593bc..8f066312a 100644 --- a/crds/virtualmachineblockdeviceattachment.yaml +++ b/crds/virtualmachineblockdeviceattachment.yaml @@ -106,7 +106,7 @@ spec: * InProgress - the disk is in the process of being attached. * Attached - the disk is attached to virtual machine. * Failed - there was a problem with attaching the disk. - * Terminating - The process of resource deletion is in progress. + * Terminating - the process of resource deletion is in progress. enum: - "Pending" - "InProgress" diff --git a/images/dvcr-artifact/build/importer.Dockerfile b/images/dvcr-artifact/build/importer.Dockerfile index 437ab0ef7..4c2525883 100644 --- a/images/dvcr-artifact/build/importer.Dockerfile +++ b/images/dvcr-artifact/build/importer.Dockerfile @@ -1,5 +1,5 @@ ARG BUILDER_CACHE_IMAGE=golang:1.21-bookworm -FROM $BUILDER_CACHE_IMAGE as builder +FROM $BUILDER_CACHE_IMAGE AS builder # Cache-friendly download modules. ADD go.mod go.sum /app/ diff --git a/images/dvcr-artifact/build/uploader.Dockerfile b/images/dvcr-artifact/build/uploader.Dockerfile index 862f5ff0f..e7ed190d2 100644 --- a/images/dvcr-artifact/build/uploader.Dockerfile +++ b/images/dvcr-artifact/build/uploader.Dockerfile @@ -1,5 +1,5 @@ ARG BUILDER_CACHE_IMAGE=golang:1.21-bookworm -FROM $BUILDER_CACHE_IMAGE as builder +FROM $BUILDER_CACHE_IMAGE AS builder # Cache-friendly download modules. ADD go.mod go.sum /app/ diff --git a/images/virt-artifact/patches/017-fix-vmi-subresource-url.patch b/images/virt-artifact/patches/017-fix-vmi-subresource-url.patch index 8bed9c233..1981d0f9a 100644 --- a/images/virt-artifact/patches/017-fix-vmi-subresource-url.patch +++ b/images/virt-artifact/patches/017-fix-vmi-subresource-url.patch @@ -1,26 +1,3 @@ -diff --git a/pkg/virt-operator/resource/generate/rbac/controller.go b/pkg/virt-operator/resource/generate/rbac/controller.go -index 8b8313112..2b9061aef 100644 ---- a/pkg/virt-operator/resource/generate/rbac/controller.go -+++ b/pkg/virt-operator/resource/generate/rbac/controller.go -@@ -363,6 +363,18 @@ func newControllerClusterRole() *rbacv1.ClusterRole { - "*", - }, - }, -+ { -+ APIGroups: []string{ -+ "subresources.virtualization.deckhouse.io", -+ }, -+ Resources: []string{ -+ "virtualmachines/addvolume", -+ "virtualmachines/removevolume", -+ }, -+ Verbs: []string{ -+ "update", -+ }, -+ }, - { - APIGroups: []string{ - "subresources.kubevirt.io", diff --git a/staging/src/kubevirt.io/client-go/kubecli/vmi.go b/staging/src/kubevirt.io/client-go/kubecli/vmi.go index a9e071350..59c17b0f8 100644 --- a/staging/src/kubevirt.io/client-go/kubecli/vmi.go diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index 1143151d8..c5a1a33e7 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -25,9 +25,11 @@ import ( "strconv" "strings" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiruntime "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" virtv1 "kubevirt.io/api/core/v1" cdiv1beta1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -39,6 +41,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/cvi" "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" "github.com/deckhouse/virtualization-controller/pkg/controller/vd" + "github.com/deckhouse/virtualization-controller/pkg/controller/vdsnapshot" "github.com/deckhouse/virtualization-controller/pkg/controller/vi" "github.com/deckhouse/virtualization-controller/pkg/controller/vm" "github.com/deckhouse/virtualization-controller/pkg/controller/vmbda" @@ -131,6 +134,7 @@ func main() { // Override content type to JSON so proxy can rewrite payloads. cfg.ContentType = apiruntime.ContentTypeJSON + cfg.NegotiatedSerializer = clientgoscheme.Codecs.WithoutConversion() leaderElectionNS := os.Getenv(common.PodNamespaceVar) if leaderElectionNS == "" { @@ -146,6 +150,7 @@ func main() { virtv2alpha1.AddToScheme, cdiv1beta1.AddToScheme, virtv1.AddToScheme, + vsv1.AddToScheme, } { err = f(scheme) if err != nil { @@ -186,6 +191,12 @@ func main() { os.Exit(1) } + restClient, err := rest.UnversionedRESTClientFor(cfg) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + log.Info("Registering Components.") // Setup context to gracefully handle termination. @@ -240,10 +251,16 @@ func main() { os.Exit(1) } - if err = vmop.SetupController(ctx, mgr, log); err != nil { + if _, err = vmop.NewController(ctx, mgr, log); err != nil { log.Error(err.Error()) os.Exit(1) } + + if _, err = vdsnapshot.NewController(ctx, mgr, log, restClient); err != nil { + log.Error(err.Error()) + os.Exit(1) + } + if err = vmop.SetupGC(mgr, log, gcSettings.VMOP); err != nil { log.Error(err.Error()) os.Exit(1) diff --git a/images/virtualization-artifact/go.mod b/images/virtualization-artifact/go.mod index e6f23dab7..8307b4d9d 100644 --- a/images/virtualization-artifact/go.mod +++ b/images/virtualization-artifact/go.mod @@ -10,6 +10,7 @@ require ( github.com/docker/cli v23.0.5+incompatible github.com/fsnotify/fsnotify v1.7.0 github.com/go-logr/logr v1.4.1 + github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0 github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 github.com/prometheus/client_golang v1.18.0 @@ -23,6 +24,7 @@ require ( k8s.io/client-go v0.29.2 k8s.io/code-generator v0.29.2 k8s.io/component-base v0.29.2 + k8s.io/component-helpers v0.29.2 k8s.io/klog/v2 v2.110.1 k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 k8s.io/utils v0.0.0-20230726121419-3b25d923346b @@ -127,7 +129,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect - k8s.io/component-helpers v0.29.2 // indirect k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect k8s.io/kms v0.29.2 // indirect kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect diff --git a/images/virtualization-artifact/go.sum b/images/virtualization-artifact/go.sum index 5f58e83a8..6b60c73cc 100644 --- a/images/virtualization-artifact/go.sum +++ b/images/virtualization-artifact/go.sum @@ -192,6 +192,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0 h1:qS4r4ljINLWKJ9m9Ge3Q3sGZ/eIoDVDT2RhAdQFHb1k= +github.com/kubernetes-csi/external-snapshotter/client/v6 v6.3.0/go.mod h1:oGXx2XTEzs9ikW2V6IC1dD8trgjRsS/Mvc2JRiC618Y= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= diff --git a/images/virtualization-artifact/local/virtualization-controller/Dockerfile b/images/virtualization-artifact/local/virtualization-controller/Dockerfile index 444285c83..36efa73a4 100644 --- a/images/virtualization-artifact/local/virtualization-controller/Dockerfile +++ b/images/virtualization-artifact/local/virtualization-controller/Dockerfile @@ -1,5 +1,5 @@ ARG BUILDER_CACHE_IMAGE=golang:1.20-alpine3.16 -FROM $BUILDER_CACHE_IMAGE as builder +FROM $BUILDER_CACHE_IMAGE AS builder # Cache-friendly download modules. ADD go.mod go.sum /app/ diff --git a/images/virtualization-artifact/pkg/apiserver/api/install.go b/images/virtualization-artifact/pkg/apiserver/api/install.go index 5cf813bb2..cc3cf0082 100644 --- a/images/virtualization-artifact/pkg/apiserver/api/install.go +++ b/images/virtualization-artifact/pkg/apiserver/api/install.go @@ -63,6 +63,8 @@ func Build(store *storage.VirtualMachineStorage) genericapiserver.APIGroupInfo { "virtualmachines/portforward": store.PortForwardREST(), "virtualmachines/addvolume": store.AddVolumeREST(), "virtualmachines/removevolume": store.RemoveVolumeREST(), + "virtualmachines/freeze": store.FreezeREST(), + "virtualmachines/unfreeze": store.UnfreezeREST(), } apiGroupInfo.VersionedResourcesStorageMap[v1alpha2.SchemeGroupVersion.Version] = resources return apiGroupInfo diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go new file mode 100644 index 000000000..fa0f8991c --- /dev/null +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go @@ -0,0 +1,91 @@ +/* +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 rest + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/tools/cache" + + "github.com/deckhouse/virtualization-controller/pkg/tls/certmanager" + "github.com/deckhouse/virtualization/api/subresources" +) + +type FreezeREST struct { + vmLister cache.GenericLister + proxyCertManager certmanager.CertificateManager + kubevirt KubevirtApiServerConfig +} + +var ( + _ rest.Storage = &FreezeREST{} + _ rest.Connecter = &FreezeREST{} +) + +func NewFreezeREST(vmLister cache.GenericLister, kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager) *FreezeREST { + return &FreezeREST{ + vmLister: vmLister, + kubevirt: kubevirt, + proxyCertManager: proxyCertManager, + } +} + +func (r FreezeREST) New() runtime.Object { + return &subresources.VirtualMachineFreeze{} +} + +func (r FreezeREST) Destroy() { +} + +func (r FreezeREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + freezeOpts, ok := opts.(*subresources.VirtualMachineFreeze) + if !ok { + return nil, fmt.Errorf("invalid options object: %#v", opts) + } + location, transport, err := FreezeLocation(ctx, r.vmLister, name, freezeOpts, r.kubevirt, r.proxyCertManager) + if err != nil { + return nil, err + } + handler := newThrottledUpgradeAwareProxyHandler(location, transport, false, responder, r.kubevirt.ServiceAccount) + return handler, nil +} + +// NewConnectOptions implements rest.Connecter interface +func (r FreezeREST) NewConnectOptions() (runtime.Object, bool, string) { + return &subresources.VirtualMachineFreeze{}, false, "" +} + +// ConnectMethods implements rest.Connecter interface +func (r FreezeREST) ConnectMethods() []string { + return []string{http.MethodPut} +} + +func FreezeLocation( + ctx context.Context, + getter cache.GenericLister, + name string, + opts *subresources.VirtualMachineFreeze, + kubevirt KubevirtApiServerConfig, + proxyCertManager certmanager.CertificateManager, +) (*url.URL, *http.Transport, error) { + return streamLocation(ctx, getter, name, opts, "freeze", kubevirt, proxyCertManager) +} diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go index 6b1308e2a..9e09f6ef5 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go @@ -120,6 +120,10 @@ func streamParams(_ url.Values, opts runtime.Object) error { return nil case *subresources.VirtualMachineRemoveVolume: return nil + case *subresources.VirtualMachineFreeze: + return nil + case *subresources.VirtualMachineUnfreeze: + return nil default: return fmt.Errorf("unknown object for streaming: %v", opts) } diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/unfreeze.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/unfreeze.go new file mode 100644 index 000000000..58ae0da9d --- /dev/null +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/unfreeze.go @@ -0,0 +1,91 @@ +/* +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 rest + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/client-go/tools/cache" + + "github.com/deckhouse/virtualization-controller/pkg/tls/certmanager" + "github.com/deckhouse/virtualization/api/subresources" +) + +type UnfreezeREST struct { + vmLister cache.GenericLister + proxyCertManager certmanager.CertificateManager + kubevirt KubevirtApiServerConfig +} + +var ( + _ rest.Storage = &UnfreezeREST{} + _ rest.Connecter = &UnfreezeREST{} +) + +func NewUnfreezeREST(vmLister cache.GenericLister, kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager) *UnfreezeREST { + return &UnfreezeREST{ + vmLister: vmLister, + kubevirt: kubevirt, + proxyCertManager: proxyCertManager, + } +} + +func (r UnfreezeREST) New() runtime.Object { + return &subresources.VirtualMachineUnfreeze{} +} + +func (r UnfreezeREST) Destroy() { +} + +func (r UnfreezeREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + unfreezeOpts, ok := opts.(*subresources.VirtualMachineUnfreeze) + if !ok { + return nil, fmt.Errorf("invalid options object: %#v", opts) + } + location, transport, err := UnfreezeLocation(ctx, r.vmLister, name, unfreezeOpts, r.kubevirt, r.proxyCertManager) + if err != nil { + return nil, err + } + handler := newThrottledUpgradeAwareProxyHandler(location, transport, false, responder, r.kubevirt.ServiceAccount) + return handler, nil +} + +// NewConnectOptions implements rest.Connecter interface +func (r UnfreezeREST) NewConnectOptions() (runtime.Object, bool, string) { + return &subresources.VirtualMachineUnfreeze{}, false, "" +} + +// ConnectMethods implements rest.Connecter interface +func (r UnfreezeREST) ConnectMethods() []string { + return []string{http.MethodPut} +} + +func UnfreezeLocation( + ctx context.Context, + getter cache.GenericLister, + name string, + opts *subresources.VirtualMachineUnfreeze, + kubevirt KubevirtApiServerConfig, + proxyCertManager certmanager.CertificateManager, +) (*url.URL, *http.Transport, error) { + return streamLocation(ctx, getter, name, opts, "unfreeze", kubevirt, proxyCertManager) +} diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go index 233031d4f..7f4329ab2 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go @@ -46,6 +46,8 @@ type VirtualMachineStorage struct { portforward *vmrest.PortForwardREST addVolume *vmrest.AddVolumeREST removeVolume *vmrest.RemoveVolumeREST + freeze *vmrest.FreezeREST + unfreeze *vmrest.UnfreezeREST convertor rest.TableConvertor } @@ -89,6 +91,8 @@ func NewStorage( portforward: vmrest.NewPortForwardREST(vmLister, kubevirt, proxyCertManager), addVolume: vmrest.NewAddVolumeREST(vmLister, kubevirt, proxyCertManager), removeVolume: vmrest.NewRemoveVolumeREST(vmLister, kubevirt, proxyCertManager), + freeze: vmrest.NewFreezeREST(vmLister, kubevirt, proxyCertManager), + unfreeze: vmrest.NewUnfreezeREST(vmLister, kubevirt, proxyCertManager), convertor: convertor, } } @@ -113,6 +117,14 @@ func (store VirtualMachineStorage) RemoveVolumeREST() *vmrest.RemoveVolumeREST { return store.removeVolume } +func (store VirtualMachineStorage) FreezeREST() *vmrest.FreezeREST { + return store.freeze +} + +func (store VirtualMachineStorage) UnfreezeREST() *vmrest.UnfreezeREST { + return store.unfreeze +} + // New implements rest.Storage interface func (store VirtualMachineStorage) New() runtime.Object { return &virtv2.VirtualMachine{} diff --git a/images/virtualization-artifact/pkg/controller/conditions/manager.go b/images/virtualization-artifact/pkg/controller/conditions/manager.go index 3fa04b85a..3067c8a7d 100644 --- a/images/virtualization-artifact/pkg/controller/conditions/manager.go +++ b/images/virtualization-artifact/pkg/controller/conditions/manager.go @@ -56,6 +56,24 @@ func (m *Manager) Update(c metav1.Condition) { meta.SetStatusCondition(&m.conds, c) } +type Conditioner interface { + Condition() metav1.Condition +} + +// Update2 TODO will be refactored soon. +func (m *Manager) Update2(conditioner Conditioner) { + c := conditioner.Condition() + + if i, found := m.indexConds[c.Type]; found { + if !equalConditions(c, m.conds[i]) { + m.conds[i] = c + } + return + } + m.conds = append(m.conds, c) + m.indexConds[c.Type] = len(m.conds) - 1 +} + func (m *Manager) Generate() []metav1.Condition { return slices.Clone(m.conds) } diff --git a/images/virtualization-artifact/pkg/controller/service/disk_service.go b/images/virtualization-artifact/pkg/controller/service/disk_service.go index 590561dc5..704a8f605 100644 --- a/images/virtualization-artifact/pkg/controller/service/disk_service.go +++ b/images/virtualization-artifact/pkg/controller/service/disk_service.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" storev1 "k8s.io/api/storage/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -40,6 +41,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/dvcr" "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" "github.com/deckhouse/virtualization-controller/pkg/util" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) type DiskService struct { @@ -93,6 +95,15 @@ func (s DiskService) Start( return supplements.EnsureForDataVolume(ctx, s.client, sup, dvBuilder.GetResource(), s.dvcrSettings) } +func (s DiskService) CreatePersistentVolumeClaim(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error { + err := s.client.Create(ctx, pvc) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + func (s DiskService) CleanUp(ctx context.Context, sup *supplements.Generator) (bool, error) { subResourcesHaveDeleted, err := s.CleanUpSupplements(ctx, sup) if err != nil { @@ -244,6 +255,26 @@ func (s DiskService) GetPersistentVolumeClaim(ctx context.Context, sup *suppleme return helper.FetchObject(ctx, sup.PersistentVolumeClaim(), s.client, &corev1.PersistentVolumeClaim{}) } +func (s DiskService) GetVolumeSnapshot(ctx context.Context, name, namespace string) (*vsv1.VolumeSnapshot, error) { + return helper.FetchObject(ctx, types.NamespacedName{Name: name, Namespace: namespace}, s.client, &vsv1.VolumeSnapshot{}) +} + +func (s DiskService) ListVirtualDiskSnapshots(ctx context.Context, namespace string) ([]virtv2.VirtualDiskSnapshot, error) { + var vdSnapshots virtv2.VirtualDiskSnapshotList + err := s.client.List(ctx, &vdSnapshots, &client.ListOptions{ + Namespace: namespace, + }) + if err != nil { + return nil, err + } + + return vdSnapshots.Items, nil +} + +func (s DiskService) GetVirtualDiskSnapshot(ctx context.Context, name, namespace string) (*virtv2.VirtualDiskSnapshot, error) { + return helper.FetchObject(ctx, types.NamespacedName{Name: name, Namespace: namespace}, s.client, &virtv2.VirtualDiskSnapshot{}) +} + func (s DiskService) CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolume, pvc *corev1.PersistentVolumeClaim, storageClassName *string) error { var err error @@ -290,7 +321,7 @@ func (s DiskService) CheckImportProcess(ctx context.Context, dv *cdiv1.DataVolum } podScheduledCond, ok := GetPodCondition(corev1.PodScheduled, cdiImporterPrime.Status.Conditions) - if ok && podScheduledCond.Status == corev1.ConditionFalse && (podScheduledCond.Reason == corev1.PodReasonUnschedulable || strings.Contains(podScheduledCond.Reason, "Error")) { + if ok && podScheduledCond.Status == corev1.ConditionFalse && strings.Contains(podScheduledCond.Reason, "Error") { return fmt.Errorf("%w; %s error %s: %s", ErrDataVolumeNotRunning, key.String(), podScheduledCond.Reason, podScheduledCond.Message) } } diff --git a/images/virtualization-artifact/pkg/controller/service/snapshot_service.go b/images/virtualization-artifact/pkg/controller/service/snapshot_service.go new file mode 100644 index 000000000..f57261de0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/service/snapshot_service.go @@ -0,0 +1,208 @@ +/* +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 service + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +const virtualizationSubresourcePath = "/apis/subresources.virtualization.deckhouse.io/v1alpha2/namespaces/%s/virtualmachines/%s/%s" + +type SnapshotService struct { + restClient *rest.RESTClient + client Client + protection *ProtectionService +} + +func NewSnapshotService(restClient *rest.RESTClient, client Client, protection *ProtectionService) *SnapshotService { + return &SnapshotService{ + restClient: restClient, + client: client, + protection: protection, + } +} + +func (s *SnapshotService) CanFreeze(vm *virtv2.VirtualMachine) bool { + if vm == nil || vm.Status.Phase != virtv2.MachineRunning { + return false + } + + agentReady, _ := GetCondition(vmcondition.TypeAgentReady.String(), vm.Status.Conditions) + if agentReady.Status != metav1.ConditionTrue { + return false + } + + filesystemReady, _ := GetCondition(vmcondition.TypeFilesystemReady.String(), vm.Status.Conditions) + + return filesystemReady.Status == metav1.ConditionTrue +} + +func (s *SnapshotService) Freeze(ctx context.Context, name, namespace string) error { + path := fmt.Sprintf(virtualizationSubresourcePath, namespace, name, "freeze") + + body, err := json.Marshal(&virtv1.FreezeUnfreezeTimeout{ + UnfreezeTimeout: &metav1.Duration{}, + }) + if err != nil { + return err + } + + err = s.restClient.Put().AbsPath(path).Body(body).Do(ctx).Error() + if err != nil { + return fmt.Errorf("failed to freeze internal virtual machine %s/%s: %w", namespace, name, err) + } + + return nil +} + +func (s *SnapshotService) CanUnfreeze(ctx context.Context, vdSnapshotName string, vm *virtv2.VirtualMachine) (bool, error) { + if vm == nil { + return false, nil + } + + filesystemReady, _ := GetCondition(vmcondition.TypeFilesystemReady.String(), vm.Status.Conditions) + if filesystemReady.Reason != vmcondition.ReasonFilesystemFrozen.String() { + return false, nil + } + + vdByName := make(map[string]struct{}) + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice { + vdByName[bdr.Name] = struct{}{} + } + } + + var vdSnapshots virtv2.VirtualDiskSnapshotList + err := s.client.List(ctx, &vdSnapshots, &client.ListOptions{ + Namespace: vm.Namespace, + }) + if err != nil { + return false, err + } + + for _, vdSnapshot := range vdSnapshots.Items { + if vdSnapshot.Name == vdSnapshotName { + continue + } + + _, ok := vdByName[vdSnapshot.Spec.VirtualDiskName] + if ok { + return false, nil + } + } + + return true, nil +} + +func (s *SnapshotService) Unfreeze(ctx context.Context, name, namespace string) error { + path := fmt.Sprintf(virtualizationSubresourcePath, namespace, name, "unfreeze") + + err := s.restClient.Put().AbsPath(path).Do(ctx).Error() + if err != nil { + return fmt.Errorf("failed to unfreeze internal virtual machine %s/%s: %w", namespace, name, err) + } + + 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) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return nil, err + } + + err = s.protection.AddProtection(ctx, &volumeSnapshot) + if err != nil { + return nil, err + } + + return &volumeSnapshot, nil +} + +func (s *SnapshotService) DeleteVolumeSnapshot(ctx context.Context, vs *vsv1.VolumeSnapshot) error { + err := s.protection.RemoveProtection(ctx, vs) + if err != nil { + return err + } + + err = s.client.Delete(ctx, vs) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } + + return nil +} + +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) GetVirtualMachine(ctx context.Context, name, namespace string) (*virtv2.VirtualMachine, error) { + return helper.FetchObject(ctx, types.NamespacedName{Namespace: namespace, Name: name}, s.client, &virtv2.VirtualMachine{}) +} + +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{}) +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/datasource_ready.go b/images/virtualization-artifact/pkg/controller/vd/internal/datasource_ready.go index 7917829e4..795a02d68 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/datasource_ready.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/datasource_ready.go @@ -92,6 +92,11 @@ func (h DatasourceReadyHandler) Handle(ctx context.Context, vd *virtv2.VirtualDi condition.Reason = vdcondition.ClusterImageNotReady condition.Message = service.CapitalizeFirstLetter(err.Error()) + "." return reconcile.Result{}, nil + case errors.As(err, &source.VirtualDiskSnapshotNotReadyError{}): + condition.Status = metav1.ConditionFalse + condition.Reason = vdcondition.VirtualDiskSnapshotNotReady + condition.Message = service.CapitalizeFirstLetter(err.Error()) + "." + return reconcile.Result{}, nil default: return reconcile.Result{}, fmt.Errorf("validation failed for data source %s: %w", ds.Name(), err) } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/resizing.go b/images/virtualization-artifact/pkg/controller/vd/internal/resizing.go index e7b45e112..9a92c7758 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/resizing.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/resizing.go @@ -111,6 +111,14 @@ func (h ResizingHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (re // Expected disk size is GREATER THAN expected pvc size: resize needed, resizing to a larger size. if vdSpecSize != nil && vdSpecSize.Cmp(pvcSpecSize) == 1 { + snapshotting, _ := service.GetCondition(vdcondition.SnapshottingType, vd.Status.Conditions) + if snapshotting.Status == metav1.ConditionTrue { + condition.Status = metav1.ConditionFalse + condition.Reason = vdcondition.ResizingNotAvailable + condition.Message = "The virtual disk cannot be selected for resizing as it is currently snapshotting." + return reconcile.Result{}, nil + } + err = h.diskService.Resize(ctx, pvc, *vdSpecSize) if err != nil { return reconcile.Result{}, err @@ -133,7 +141,7 @@ func (h ResizingHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (re condition.Message = "" default: condition.Status = metav1.ConditionFalse - condition.Reason = vdcondition.NotRequested + condition.Reason = vdcondition.ResizingNotRequested condition.Message = "" } diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/resizing_test.go b/images/virtualization-artifact/pkg/controller/vd/internal/resizing_test.go index 5e0d3f191..2d0e8758f 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/resizing_test.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/resizing_test.go @@ -114,7 +114,7 @@ var _ = Describe("Resizing handler Run", func() { Expect(err).To(BeNil()) resized, _ := service.GetCondition(vdcondition.ResizedType, vd.Status.Conditions) Expect(resized.Status).To(Equal(metav1.ConditionFalse)) - Expect(resized.Reason).To(Equal(vdcondition.NotRequested)) + Expect(resized.Reason).To(Equal(vdcondition.ResizingNotRequested)) }) It("Resize is not requested (vd.spec.size < pvc.spec.size)", func() { @@ -126,7 +126,7 @@ var _ = Describe("Resizing handler Run", func() { Expect(err).To(BeNil()) resized, _ := service.GetCondition(vdcondition.ResizedType, vd.Status.Conditions) Expect(resized.Status).To(Equal(metav1.ConditionFalse)) - Expect(resized.Reason).To(Equal(vdcondition.NotRequested)) + Expect(resized.Reason).To(Equal(vdcondition.ResizingNotRequested)) }) It("Resize is not requested (vd.spec.size == pvc.spec.size)", func() { @@ -136,7 +136,7 @@ var _ = Describe("Resizing handler Run", func() { Expect(err).To(BeNil()) resized, _ := service.GetCondition(vdcondition.ResizedType, vd.Status.Conditions) Expect(resized.Status).To(Equal(metav1.ConditionFalse)) - Expect(resized.Reason).To(Equal(vdcondition.NotRequested)) + Expect(resized.Reason).To(Equal(vdcondition.ResizingNotRequested)) }) It("Resize has started (vd.spec.size > pvc.spec.size)", func() { diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/snapshotting.go b/images/virtualization-artifact/pkg/controller/vd/internal/snapshotting.go new file mode 100644 index 000000000..629342fa8 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/snapshotting.go @@ -0,0 +1,98 @@ +/* +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/reconcile" + + "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 SnapshottingHandler struct { + diskService *service.DiskService +} + +func NewSnapshottingHandler(diskService *service.DiskService) *SnapshottingHandler { + return &SnapshottingHandler{ + diskService: diskService, + } +} + +func (h SnapshottingHandler) Handle(ctx context.Context, vd *virtv2.VirtualDisk) (reconcile.Result, error) { + condition, ok := service.GetCondition(vdcondition.SnapshottingType, vd.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vdcondition.SnapshottingType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vd.Status.Conditions) }() + + if vd.DeletionTimestamp != nil { + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil + } + + readyCondition, ok := service.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) + if !ok || readyCondition.Status != metav1.ConditionTrue { + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil + } + + vdSnapshots, err := h.diskService.ListVirtualDiskSnapshots(ctx, vd.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + for _, vdSnapshot := range vdSnapshots { + if vdSnapshot.Spec.VirtualDiskName != vd.Name { + continue + } + + if vdSnapshot.Status.Phase == virtv2.VirtualDiskSnapshotPhaseReady || vdSnapshot.Status.Phase == virtv2.VirtualDiskSnapshotPhaseTerminating { + continue + } + + resized, _ := service.GetCondition(vdcondition.ResizedType, vd.Status.Conditions) + if resized.Reason == vdcondition.InProgress { + condition.Status = metav1.ConditionFalse + condition.Reason = vdcondition.SnapshottingNotAvailable + condition.Message = "The virtual disk cannot be selected for snapshotting as it is currently resizing." + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionTrue + condition.Reason = vdcondition.Snapshotting + condition.Message = "The virtual disk is selected for taking a snapshot." + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/source/errors.go b/images/virtualization-artifact/pkg/controller/vd/internal/source/errors.go index 90a83a8fb..1851603ee 100644 --- a/images/virtualization-artifact/pkg/controller/vd/internal/source/errors.go +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/errors.go @@ -31,7 +31,7 @@ type ImageNotReadyError struct { } func (e ImageNotReadyError) Error() string { - return fmt.Sprintf("VirtualImage %s not ready", e.name) + return fmt.Sprintf("VirtualImage %q not ready", e.name) } func NewImageNotReadyError(name string) error { @@ -45,7 +45,7 @@ type ClusterImageNotReadyError struct { } func (e ClusterImageNotReadyError) Error() string { - return fmt.Sprintf("ClusterVirtualImage %s not ready", e.name) + return fmt.Sprintf("ClusterVirtualImage %q not ready", e.name) } func NewClusterImageNotReadyError(name string) error { @@ -53,3 +53,17 @@ func NewClusterImageNotReadyError(name string) error { name: name, } } + +type VirtualDiskSnapshotNotReadyError struct { + name string +} + +func (e VirtualDiskSnapshotNotReadyError) Error() string { + return fmt.Sprintf("VirtualDiskSnapshot %q not ready", e.name) +} + +func NewVirtualDiskSnapshotNotReadyError(name string) error { + return VirtualDiskSnapshotNotReadyError{ + name: name, + } +} 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 fc93fb10a..3c31e07d1 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 @@ -45,6 +45,8 @@ type ObjectRefDataSource struct { statService *service.StatService diskService *service.DiskService client client.Client + + vdSnapshotSyncer *ObjectRefVirtualDiskSnapshot } func NewObjectRefDataSource( @@ -53,9 +55,10 @@ func NewObjectRefDataSource( client client.Client, ) *ObjectRefDataSource { return &ObjectRefDataSource{ - statService: statService, - diskService: diskService, - client: client, + statService: statService, + diskService: diskService, + client: client, + vdSnapshotSyncer: NewObjectRefVirtualDiskSnapshot(diskService), } } @@ -65,6 +68,10 @@ func (ds ObjectRefDataSource) Sync(ctx context.Context, vd *virtv2.VirtualDisk) condition, _ := service.GetCondition(vdcondition.ReadyType, vd.Status.Conditions) defer func() { service.SetCondition(condition, &vd.Status.Conditions) }() + if vd.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskObjectRefKindVirtualDiskSnapshot { + return ds.vdSnapshotSyncer.Sync(ctx, vd, &condition) + } + supgen := supplements.NewGenerator(common.VDShortName, vd.Name, vd.Namespace, vd.UID) dv, err := ds.diskService.GetDataVolume(ctx, supgen) if err != nil { @@ -216,6 +223,10 @@ func (ds ObjectRefDataSource) Validate(ctx context.Context, vd *virtv2.VirtualDi return errors.New("object ref missed for data source") } + if vd.Spec.DataSource.ObjectRef.Kind == virtv2.VirtualDiskObjectRefKindVirtualDiskSnapshot { + return ds.vdSnapshotSyncer.Validate(ctx, vd) + } + dvcrDataSource, err := controller.NewDVCRDataSourcesForVMD(ctx, vd.Spec.DataSource, vd, ds.client) if err != nil { return err 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 new file mode 100644 index 000000000..63fbffb7e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/source/object_ref_vdsnapshot.go @@ -0,0 +1,178 @@ +/* +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 source + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/deckhouse/virtualization-controller/pkg/controller/common" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/supplements" + "github.com/deckhouse/virtualization-controller/pkg/logger" + "github.com/deckhouse/virtualization-controller/pkg/util" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdcondition" +) + +type ObjectRefVirtualDiskSnapshot struct { + diskService *service.DiskService +} + +func NewObjectRefVirtualDiskSnapshot(diskService *service.DiskService) *ObjectRefVirtualDiskSnapshot { + return &ObjectRefVirtualDiskSnapshot{ + diskService: diskService, + } +} + +func (ds ObjectRefVirtualDiskSnapshot) Sync(ctx context.Context, vd *virtv2.VirtualDisk, condition *metav1.Condition) (bool, error) { + log, ctx := logger.GetDataSourceContext(ctx, objectRefDataSource) + + supgen := supplements.NewGenerator(common.VDShortName, vd.Name, vd.Namespace, vd.UID) + + vs, err := ds.diskService.GetVolumeSnapshot(ctx, vd.Spec.DataSource.ObjectRef.Name, vd.Namespace) + if err != nil { + return false, err + } + pvc, err := ds.diskService.GetPersistentVolumeClaim(ctx, supgen) + if err != nil { + return false, err + } + pv, err := ds.diskService.GetPersistentVolume(ctx, pvc) + if err != nil { + return false, err + } + + switch { + case isDiskProvisioningFinished(*condition): + log.Info("Disk provisioning finished: clean up") + + setPhaseConditionForFinishedDisk(pv, pvc, condition, &vd.Status.Phase, supgen) + + // Protect Ready Disk and underlying PVC and PV. + err = ds.diskService.Protect(ctx, vd, nil, pvc, pv) + if err != nil { + return false, err + } + + return false, nil + case common.AnyTerminating(pvc, pv): + log.Info("Waiting for supplements to be terminated") + return true, nil + case pvc == nil: + log.Info("Start import to PVC") + + namespacedName := supplements.NewGenerator(common.VDShortName, vd.Name, vd.Namespace, vd.UID).PersistentVolumeClaim() + + storageClassName := vs.Annotations["storageClass"] + accessModesStr := strings.Split(vs.Annotations["accessModes"], ",") + accessModes := make([]corev1.PersistentVolumeAccessMode, 0, len(accessModesStr)) + for _, accessModeStr := range accessModesStr { + accessModes = append(accessModes, corev1.PersistentVolumeAccessMode(accessModeStr)) + } + + spec := corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + DataSource: &corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(vs.GroupVersionKind().GroupVersion().String()), + Kind: vs.Kind, + Name: vd.Spec.DataSource.ObjectRef.Name, + }, + } + + if storageClassName != "" { + spec.StorageClassName = &storageClassName + } + + if vs.Status != nil && vs.Status.RestoreSize != nil { + spec.Resources = corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: *vs.Status.RestoreSize, + }, + } + } + + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespacedName.Name, + Namespace: namespacedName.Namespace, + OwnerReferences: []metav1.OwnerReference{ + service.MakeOwnerReference(vd), + }, + }, + Spec: spec, + } + + err = ds.diskService.CreatePersistentVolumeClaim(ctx, pvc) + if err != nil { + setPhaseConditionToFailed(condition, &vd.Status.Phase, err) + return false, err + } + + vd.Status.Phase = virtv2.DiskProvisioning + condition.Status = metav1.ConditionFalse + condition.Reason = vdcondition.Provisioning + condition.Message = "PVC has created: waiting to be Bound." + + vd.Status.Progress = "0%" + vd.Status.SourceUID = util.GetPointer(vs.UID) + vd.Status.Capacity = ds.diskService.GetCapacity(pvc) + vd.Status.Target.PersistentVolumeClaim = pvc.Name + + return false, nil + case pvc.Status.Phase == corev1.ClaimBound: + vd.Status.Phase = virtv2.DiskReady + condition.Status = metav1.ConditionTrue + condition.Reason = vdcondition.Ready + condition.Message = "" + + vd.Status.Progress = "100%" + vd.Status.Capacity = ds.diskService.GetCapacity(pvc) + vd.Status.Target.PersistentVolumeClaim = pvc.Name + + return true, nil + default: + vd.Status.Phase = virtv2.DiskProvisioning + condition.Status = metav1.ConditionFalse + condition.Reason = vdcondition.Provisioning + condition.Message = fmt.Sprintf("Waiting for the PVC %s to be Bound.", pvc.Name) + + return false, nil + } +} + +func (ds ObjectRefVirtualDiskSnapshot) Validate(ctx context.Context, vd *virtv2.VirtualDisk) error { + if vd.Spec.DataSource == nil || vd.Spec.DataSource.ObjectRef == nil || vd.Spec.DataSource.ObjectRef.Kind != virtv2.VirtualDiskObjectRefKindVirtualDiskSnapshot { + return fmt.Errorf("not a %s data source", virtv2.VirtualDiskObjectRefKindVirtualDiskSnapshot) + } + + vdSnapshot, err := ds.diskService.GetVirtualDiskSnapshot(ctx, vd.Spec.DataSource.ObjectRef.Name, vd.Namespace) + if err != nil { + return err + } + + if vdSnapshot == nil || vdSnapshot.Status.Phase != virtv2.VirtualDiskSnapshotPhaseReady { + return NewVirtualDiskSnapshotNotReadyError(vd.Spec.DataSource.ObjectRef.Name) + } + + return nil +} diff --git a/images/virtualization-artifact/pkg/controller/vd/internal/watcher/vdsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/vdsnapshot_watcher.go new file mode 100644 index 000000000..b47541df6 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vd/internal/watcher/vdsnapshot_watcher.go @@ -0,0 +1,127 @@ +/* +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 true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskSnapshotWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vdSnapshot, ok := obj.(*virtv2.VirtualDiskSnapshot) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a VirtualDiskSnapshot but got a %T", obj)) + return + } + + var vds virtv2.VirtualDiskList + err := w.client.List(ctx, &vds, &client.ListOptions{ + Namespace: vdSnapshot.Namespace, + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual disks: %s", err)) + return + } + + for _, vd := range vds.Items { + // Need to reconcile the virtual disk from which the snapshot was taken. + if vd.Name == vdSnapshot.Spec.VirtualDiskName { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Name, + Namespace: vd.Namespace, + }, + }) + continue + } + + // Need to reconcile the virtual disk with the snapshot data source. + if isSnapshotDataSource(vd.Spec.DataSource, vdSnapshot.Name) { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vd.Name, + Namespace: vd.Namespace, + }, + }) + } + } + + 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 +} + +func isSnapshotDataSource(ds *virtv2.VirtualDiskDataSource, vdSnapshotName string) bool { + if ds == nil || ds.Type != virtv2.DataSourceTypeObjectRef { + return false + } + + if ds.ObjectRef == nil || ds.ObjectRef.Kind != virtv2.VirtualDiskObjectRefKindVirtualDiskSnapshot { + return false + } + + return ds.ObjectRef.Name == vdSnapshotName +} diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go index e403cc52d..a23551631 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_controller.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_controller.go @@ -77,6 +77,7 @@ func NewController( mgr.GetClient(), internal.NewDatasourceReadyHandler(blank, sources), internal.NewLifeCycleHandler(blank, sources, mgr.GetClient()), + internal.NewSnapshottingHandler(disk), internal.NewResizingHandler(disk), internal.NewDeletionHandler(sources), internal.NewAttacheeHandler(mgr.GetClient()), diff --git a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go index b9a579d88..0b0e7cace 100644 --- a/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vd/vd_reconciler.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vd/internal/watcher" "github.com/deckhouse/virtualization-controller/pkg/controller/watchers" "github.com/deckhouse/virtualization-controller/pkg/logger" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" @@ -231,6 +232,11 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr return fmt.Errorf("error setting watch on CVIs: %w", err) } + w := watcher.NewVirtualDiskSnapshotWatcher(mgr.GetClient()) + if err := w.Watch(mgr, ctr); err != nil { + return fmt.Errorf("error setting watch on VDSnapshots: %w", err) + } + return nil } @@ -251,13 +257,13 @@ func (r *Reconciler) enqueueDisksAttachedToVM() handler.MapFunc { var requests []reconcile.Request - for _, bda := range vm.Status.BlockDeviceRefs { - if bda.Kind != virtv2.DiskDevice { + for _, bdr := range vm.Status.BlockDeviceRefs { + if bdr.Kind != virtv2.DiskDevice { continue } requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ - Name: bda.Name, + Name: bdr.Name, Namespace: vm.Namespace, }}) } diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/deletion.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/deletion.go new file mode 100644 index 000000000..6cbcb73bf --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/deletion.go @@ -0,0 +1,91 @@ +/* +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" + + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type DeletionHandler struct { + snapshotter *service.SnapshotService +} + +func NewDeletionHandler(snapshotter *service.SnapshotService) *DeletionHandler { + return &DeletionHandler{ + snapshotter: snapshotter, + } +} + +func (h DeletionHandler) Handle(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (reconcile.Result, error) { + if vdSnapshot.DeletionTimestamp != nil { + vs, err := h.snapshotter.GetVolumeSnapshot(ctx, vdSnapshot.Name, vdSnapshot.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + vd, err := h.snapshotter.GetVirtualDisk(ctx, vdSnapshot.Spec.VirtualDiskName, vdSnapshot.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + vm, err := getVirtualMachine(ctx, vd, h.snapshotter) + if err != nil { + return reconcile.Result{}, err + } + + var requeue bool + + if vs != nil { + err = h.snapshotter.DeleteVolumeSnapshot(ctx, vs) + if err != nil { + return reconcile.Result{}, err + } + requeue = true + } + + if vm != nil { + var canUnfreeze bool + canUnfreeze, err = h.snapshotter.CanUnfreeze(ctx, vdSnapshot.Name, vm) + if err != nil { + return reconcile.Result{}, err + } + + if canUnfreeze { + err = h.snapshotter.Unfreeze(ctx, vm.Name, vm.Namespace) + if err != nil { + return reconcile.Result{}, err + } + } + } + + if requeue { + return reconcile.Result{Requeue: true}, nil + } + + controllerutil.RemoveFinalizer(vdSnapshot, virtv2.FinalizerVDSnapshotCleanup) + return reconcile.Result{}, nil + } + + controllerutil.AddFinalizer(vdSnapshot, virtv2.FinalizerVDSnapshotCleanup) + return reconcile.Result{}, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/handler_test.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/handler_test.go new file mode 100644 index 000000000..f4f370fc1 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/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/vdsnapshot/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go new file mode 100644 index 000000000..b81e887d7 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/interfaces.go @@ -0,0 +1,44 @@ +/* +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" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +//go:generate moq -rm -out mock.go . VirtualDiskReadySnapshotter LifeCycleSnapshotter + +type VirtualDiskReadySnapshotter interface { + GetVirtualDisk(ctx context.Context, name, namespace string) (*virtv2.VirtualDisk, error) +} + +type LifeCycleSnapshotter interface { + Freeze(ctx context.Context, name, namespace string) error + 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) + 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) + GetVolumeSnapshot(ctx context.Context, name, namespace string) (*vsv1.VolumeSnapshot, 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 new file mode 100644 index 000000000..1975a1f58 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle.go @@ -0,0 +1,251 @@ +/* +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" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/vdscondition" +) + +type LifeCycleHandler struct { + snapshotter LifeCycleSnapshotter +} + +func NewLifeCycleHandler(snapshotter LifeCycleSnapshotter) *LifeCycleHandler { + return &LifeCycleHandler{ + snapshotter: snapshotter, + } +} + +func (h LifeCycleHandler) Handle(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (reconcile.Result, error) { + log := logger.FromContext(ctx).With(logger.SlogHandler("lifecycle")) + + condition, ok := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vdscondition.VirtualDiskSnapshotReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vdSnapshot.Status.Conditions) }() + + vs, err := h.snapshotter.GetVolumeSnapshot(ctx, vdSnapshot.Name, vdSnapshot.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + vd, err := h.snapshotter.GetVirtualDisk(ctx, vdSnapshot.Spec.VirtualDiskName, vdSnapshot.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + vm, err := getVirtualMachine(ctx, vd, h.snapshotter) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + if vdSnapshot.DeletionTimestamp != nil { + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseTerminating + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + + return reconcile.Result{}, nil + } + + switch vdSnapshot.Status.Phase { + case "": + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhasePending + case virtv2.VirtualDiskSnapshotPhaseReady: + if vs == nil || vs.Status == nil || vs.Status.ReadyToUse == nil || !*vs.Status.ReadyToUse { + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseFailed + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VolumeSnapshotLost + condition.Message = fmt.Sprintf("The underlieng volume snapshot %q is not ready to use.", vdSnapshot.Status.VolumeSnapshotName) + return reconcile.Result{Requeue: true}, nil + } + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseReady + condition.Status = metav1.ConditionTrue + condition.Reason = vdscondition.VirtualDiskSnapshotReady + condition.Message = "" + return reconcile.Result{}, nil + } + + virtualDiskReadyCondition, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + if vd == nil || virtualDiskReadyCondition.Status != metav1.ConditionTrue { + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.WaitingForTheVirtualDisk + condition.Message = fmt.Sprintf("Waiting for the virtual disk %q to be ready for snapshotting.", vdSnapshot.Spec.VirtualDiskName) + return reconcile.Result{}, nil + } + + var pvc *corev1.PersistentVolumeClaim + if vd.Status.Target.PersistentVolumeClaim != "" { + pvc, err = h.snapshotter.GetPersistentVolumeClaim(ctx, vd.Status.Target.PersistentVolumeClaim, vd.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + } + + if pvc == nil || pvc.Status.Phase != corev1.ClaimBound { + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.WaitingForTheVirtualDisk + condition.Message = "Waiting for the virtual disk's pvc to be in phase Bound." + return reconcile.Result{}, nil + } + + switch { + case vs == nil: + if vm != nil && vm.Status.Phase != virtv2.MachineStopped { + if h.snapshotter.CanFreeze(vm) { + log.Debug("Freeze the virtual machine to take a snapshot") + + err = h.snapshotter.Freeze(ctx, vm.Name, vm.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.FileSystemFreezing + condition.Message = fmt.Sprintf( + "The virtual machine %q with an attached virtual disk %q is in the process of being frozen for taking a snapshot.", + vm.Name, vdSnapshot.Spec.VirtualDiskName, + ) + return reconcile.Result{}, nil + } + + if !vdSnapshot.Spec.AllowPotentiallyInconsistent { + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhasePending + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.PotentiallyInconsistent + condition.Message = fmt.Sprintf( + "The virtual machine %q with an attached virtual disk %q is %s: "+ + "the snapshotting of virtual disk might result in an inconsistent snapshot: "+ + "waiting for the virtual machine to be %s or the disk to be detached", + vm.Status.Phase, vm.Name, vd.Name, virtv2.MachineStopped, + ) + return reconcile.Result{}, nil + } + } + + log.Debug("The corresponding volume snapshot not found: create the new one") + + vs, err = h.snapshotter.CreateVolumeSnapshot(ctx, vdSnapshot, pvc) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseInProgress + vdSnapshot.Status.VolumeSnapshotName = vs.Name + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.Snapshotting + condition.Message = fmt.Sprintf("The snapshotting process for virtual disk %q has started.", vdSnapshot.Spec.VirtualDiskName) + return reconcile.Result{}, nil + case vs.Status != nil && vs.Status.Error != nil && vs.Status.Error.Message != nil: + log.Debug("The volume snapshot has an error") + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseFailed + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VirtualDiskSnapshotFailed + condition.Message = fmt.Sprintf("VolumeSnapshot %q has an error: %s.", vs.Name, *vs.Status.Error.Message) + return reconcile.Result{}, nil + case vs.Status == nil || vs.Status.ReadyToUse == nil || !*vs.Status.ReadyToUse: + log.Debug("Waiting for the volume snapshot to be ready to use") + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseInProgress + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.Snapshotting + condition.Message = fmt.Sprintf("Waiting fot the volume snapshot %q to be ready to use.", vdSnapshot.Name) + return reconcile.Result{}, nil + default: + log.Debug("The volume snapshot is ready to use") + + if vm != nil { + var canUnfreeze bool + canUnfreeze, err = h.snapshotter.CanUnfreeze(ctx, vdSnapshot.Name, vm) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + + if canUnfreeze { + log.Debug("Unfreeze the virtual machine after taking a snapshot") + + err = h.snapshotter.Unfreeze(ctx, vm.Name, vm.Namespace) + if err != nil { + setPhaseConditionToFailed(&condition, &vdSnapshot.Status.Phase, err) + return reconcile.Result{}, err + } + } + } + + vdSnapshot.Status.Phase = virtv2.VirtualDiskSnapshotPhaseReady + condition.Status = metav1.ConditionTrue + condition.Reason = vdscondition.VirtualDiskSnapshotReady + condition.Message = "" + + return reconcile.Result{}, nil + } +} + +func getVirtualMachine(ctx context.Context, vd *virtv2.VirtualDisk, snapshotter LifeCycleSnapshotter) (*virtv2.VirtualMachine, error) { + if vd == nil { + return nil, nil + } + + // TODO: ensure vd.Status.AttachedToVirtualMachines is in the actual state. + switch len(vd.Status.AttachedToVirtualMachines) { + case 0: + return nil, nil + case 1: + vm, err := snapshotter.GetVirtualMachine(ctx, vd.Status.AttachedToVirtualMachines[0].Name, vd.Namespace) + if err != nil { + return nil, err + } + + return vm, nil + default: + return nil, fmt.Errorf("the virtual disk %q is attached to multiple virtual machines", vd.Name) + } +} + +func setPhaseConditionToFailed(cond *metav1.Condition, phase *virtv2.VirtualDiskSnapshotPhase, err error) { + *phase = virtv2.VirtualDiskSnapshotPhaseFailed + cond.Status = metav1.ConditionFalse + cond.Reason = vdscondition.VirtualDiskSnapshotFailed + cond.Message = service.CapitalizeFirstLetter(err.Error()) +} 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 new file mode 100644 index 000000000..bd30162a5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/life_cycle_test.go @@ -0,0 +1,271 @@ +/* +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" + + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/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/vdscondition" +) + +var _ = Describe("LifeCycle handler", func() { + var snapshotter *LifeCycleSnapshotterMock + var pvc *corev1.PersistentVolumeClaim + var vd *virtv2.VirtualDisk + var vs *vsv1.VolumeSnapshot + var vdSnapshot *virtv2.VirtualDiskSnapshot + + BeforeEach(func() { + pvc = &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "pvc-01"}, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } + + vd = &virtv2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{Name: "vd-01"}, + Status: virtv2.VirtualDiskStatus{ + Target: virtv2.DiskTarget{ + PersistentVolumeClaim: pvc.Name, + }, + }, + } + + vs = &vsv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vs-01"}, + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vdsnapshot"}, + Spec: virtv2.VirtualDiskSnapshotSpec{VirtualDiskName: vd.Name}, + Status: virtv2.VirtualDiskSnapshotStatus{ + Conditions: []metav1.Condition{ + { + Type: vdscondition.VirtualDiskReadyType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + snapshotter = &LifeCycleSnapshotterMock{ + CreateVolumeSnapshotFunc: func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { + return vs, nil + }, + GetPersistentVolumeClaimFunc: func(_ context.Context, _, _ string) (*corev1.PersistentVolumeClaim, error) { + return pvc, nil + }, + GetVirtualDiskFunc: func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + return vd, nil + }, + GetVirtualMachineFunc: func(_ context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + return nil, nil + }, + GetVolumeSnapshotFunc: func(_ context.Context, _, _ string) (*vsv1.VolumeSnapshot, error) { + return nil, nil + }, + } + }) + + Context("The virtual disk snapshot without virtual machine", func() { + It("The volume snapshot has created", func() { + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.Snapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The volume snapshot has failed", func() { + snapshotter.GetVolumeSnapshotFunc = func(_ context.Context, _, _ string) (*vsv1.VolumeSnapshot, error) { + vs.Status = &vsv1.VolumeSnapshotStatus{ + Error: &vsv1.VolumeSnapshotError{ + Message: ptr.To("error"), + }, + } + return vs, nil + } + + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseFailed)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskSnapshotFailed)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The volume snapshot is not ready yet", func() { + snapshotter.GetVolumeSnapshotFunc = func(_ context.Context, _, _ string) (*vsv1.VolumeSnapshot, error) { + return vs, nil + } + + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.Snapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The volume snapshot is ready", func() { + snapshotter.GetVolumeSnapshotFunc = func(_ context.Context, _, _ string) (*vsv1.VolumeSnapshot, error) { + vs.Status = &vsv1.VolumeSnapshotStatus{ + ReadyToUse: ptr.To(true), + } + return vs, nil + } + + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseReady)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionTrue)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskSnapshotReady)) + Expect(ready.Message).To(BeEmpty()) + }) + }) + + Context("The virtual disk snapshot with virtual machine", func() { + var vm *virtv2.VirtualMachine + + BeforeEach(func() { + vm = &virtv2.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{Name: "vm"}, + Status: virtv2.VirtualMachineStatus{ + Phase: virtv2.MachineRunning, + }, + } + vd.Status.AttachedToVirtualMachines = []virtv2.AttachedVirtualMachine{{Name: vm.Name}} + + snapshotter.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + return vm, nil + } + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return true + } + snapshotter.FreezeFunc = func(_ context.Context, _, _ string) error { + return nil + } + snapshotter.CanUnfreezeFunc = func(_ context.Context, _ string, _ *virtv2.VirtualMachine) (bool, error) { + return true, nil + } + snapshotter.UnfreezeFunc = func(_ context.Context, _, _ string) error { + return nil + } + }) + + It("Freeze virtual machine", func() { + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.FileSystemFreezing)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("No need to freeze virtual machine", func() { + snapshotter.GetVirtualMachineFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualMachine, error) { + vm.Status.Phase = virtv2.MachineStopped + return vm, nil + } + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.Snapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("Cannot freeze virtual machine: deny potentially inconsistent", func() { + vdSnapshot.Spec.AllowPotentiallyInconsistent = false + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhasePending)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.PotentiallyInconsistent)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("Cannot freeze virtual machine: allow potentially inconsistent", func() { + vdSnapshot.Spec.AllowPotentiallyInconsistent = true + snapshotter.CanFreezeFunc = func(_ *virtv2.VirtualMachine) bool { + return false + } + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseInProgress)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.Snapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("Unfreeze virtual machine", func() { + snapshotter.GetVolumeSnapshotFunc = func(_ context.Context, _, _ string) (*vsv1.VolumeSnapshot, error) { + vs.Status = &vsv1.VolumeSnapshotStatus{ + ReadyToUse: ptr.To(true), + } + return vs, nil + } + h := NewLifeCycleHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + Expect(vdSnapshot.Status.Phase).To(Equal(virtv2.VirtualDiskSnapshotPhaseReady)) + ready, _ := service.GetCondition(vdscondition.VirtualDiskSnapshotReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionTrue)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskSnapshotReady)) + Expect(ready.Message).To(BeEmpty()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go new file mode 100644 index 000000000..2dc111183 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/mock.go @@ -0,0 +1,604 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package internal + +import ( + "context" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + "sync" +) + +// Ensure, that VirtualDiskReadySnapshotterMock does implement VirtualDiskReadySnapshotter. +// If this is not the case, regenerate this file with moq. +var _ VirtualDiskReadySnapshotter = &VirtualDiskReadySnapshotterMock{} + +// VirtualDiskReadySnapshotterMock is a mock implementation of VirtualDiskReadySnapshotter. +// +// func TestSomethingThatUsesVirtualDiskReadySnapshotter(t *testing.T) { +// +// // make and configure a mocked VirtualDiskReadySnapshotter +// mockedVirtualDiskReadySnapshotter := &VirtualDiskReadySnapshotterMock{ +// GetVirtualDiskFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) { +// panic("mock out the GetVirtualDisk method") +// }, +// } +// +// // use mockedVirtualDiskReadySnapshotter in code that requires VirtualDiskReadySnapshotter +// // and then make assertions. +// +// } +type VirtualDiskReadySnapshotterMock struct { + // GetVirtualDiskFunc mocks the GetVirtualDisk method. + GetVirtualDiskFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) + + // calls tracks calls to the methods. + calls struct { + // 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 + } + } + lockGetVirtualDisk sync.RWMutex +} + +// GetVirtualDisk calls GetVirtualDiskFunc. +func (mock *VirtualDiskReadySnapshotterMock) GetVirtualDisk(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) { + if mock.GetVirtualDiskFunc == nil { + panic("VirtualDiskReadySnapshotterMock.GetVirtualDiskFunc: method is nil but VirtualDiskReadySnapshotter.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(mockedVirtualDiskReadySnapshotter.GetVirtualDiskCalls()) +func (mock *VirtualDiskReadySnapshotterMock) 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 +} + +// 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") +// }, +// CreateVolumeSnapshotFunc: func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, error) { +// panic("mock out the CreateVolumeSnapshot 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") +// }, +// GetVirtualDiskFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) { +// panic("mock out the GetVirtualDisk method") +// }, +// GetVirtualMachineFunc: func(ctx context.Context, name string, namespace string) (*virtv2.VirtualMachine, error) { +// panic("mock out the GetVirtualMachine method") +// }, +// GetVolumeSnapshotFunc: func(ctx context.Context, name string, namespace string) (*vsv1.VolumeSnapshot, error) { +// panic("mock out the GetVolumeSnapshot 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) + + // CreateVolumeSnapshotFunc mocks the CreateVolumeSnapshot method. + CreateVolumeSnapshotFunc func(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*vsv1.VolumeSnapshot, 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) + + // GetVirtualDiskFunc mocks the GetVirtualDisk method. + GetVirtualDiskFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualDisk, error) + + // GetVirtualMachineFunc mocks the GetVirtualMachine method. + GetVirtualMachineFunc func(ctx context.Context, name string, namespace string) (*virtv2.VirtualMachine, error) + + // GetVolumeSnapshotFunc mocks the GetVolumeSnapshot method. + GetVolumeSnapshotFunc func(ctx context.Context, name string, namespace string) (*vsv1.VolumeSnapshot, error) + + // 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 + } + // CreateVolumeSnapshot holds details about calls to the CreateVolumeSnapshot method. + 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 + } + // 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 + } + // 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 + } + // 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 + } + // GetVolumeSnapshot holds details about calls to the GetVolumeSnapshot method. + GetVolumeSnapshot []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 + } + // 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 + lockCreateVolumeSnapshot sync.RWMutex + lockFreeze sync.RWMutex + lockGetPersistentVolumeClaim sync.RWMutex + lockGetVirtualDisk sync.RWMutex + lockGetVirtualMachine sync.RWMutex + lockGetVolumeSnapshot 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 +} + +// CreateVolumeSnapshot calls CreateVolumeSnapshotFunc. +func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshot(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot, pvc *corev1.PersistentVolumeClaim) (*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: ctx, + VdSnapshot: vdSnapshot, + Pvc: pvc, + } + mock.lockCreateVolumeSnapshot.Lock() + mock.calls.CreateVolumeSnapshot = append(mock.calls.CreateVolumeSnapshot, callInfo) + mock.lockCreateVolumeSnapshot.Unlock() + return mock.CreateVolumeSnapshotFunc(ctx, vdSnapshot, pvc) +} + +// CreateVolumeSnapshotCalls gets all the calls that were made to CreateVolumeSnapshot. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.CreateVolumeSnapshotCalls()) +func (mock *LifeCycleSnapshotterMock) CreateVolumeSnapshotCalls() []struct { + Ctx context.Context + VdSnapshot *virtv2.VirtualDiskSnapshot + Pvc *corev1.PersistentVolumeClaim +} { + var calls []struct { + Ctx context.Context + VdSnapshot *virtv2.VirtualDiskSnapshot + Pvc *corev1.PersistentVolumeClaim + } + mock.lockCreateVolumeSnapshot.RLock() + calls = mock.calls.CreateVolumeSnapshot + mock.lockCreateVolumeSnapshot.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 +} + +// 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 +} + +// 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 +} + +// GetVolumeSnapshot calls GetVolumeSnapshotFunc. +func (mock *LifeCycleSnapshotterMock) GetVolumeSnapshot(ctx context.Context, name string, namespace string) (*vsv1.VolumeSnapshot, error) { + if mock.GetVolumeSnapshotFunc == nil { + panic("LifeCycleSnapshotterMock.GetVolumeSnapshotFunc: method is nil but LifeCycleSnapshotter.GetVolumeSnapshot was just called") + } + callInfo := struct { + Ctx context.Context + Name string + Namespace string + }{ + Ctx: ctx, + Name: name, + Namespace: namespace, + } + mock.lockGetVolumeSnapshot.Lock() + mock.calls.GetVolumeSnapshot = append(mock.calls.GetVolumeSnapshot, callInfo) + mock.lockGetVolumeSnapshot.Unlock() + return mock.GetVolumeSnapshotFunc(ctx, name, namespace) +} + +// GetVolumeSnapshotCalls gets all the calls that were made to GetVolumeSnapshot. +// Check the length with: +// +// len(mockedLifeCycleSnapshotter.GetVolumeSnapshotCalls()) +func (mock *LifeCycleSnapshotterMock) GetVolumeSnapshotCalls() []struct { + Ctx context.Context + Name string + Namespace string +} { + var calls []struct { + Ctx context.Context + Name string + Namespace string + } + mock.lockGetVolumeSnapshot.RLock() + calls = mock.calls.GetVolumeSnapshot + mock.lockGetVolumeSnapshot.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/vdsnapshot/internal/virtual_disk_ready.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/virtual_disk_ready.go new file mode 100644 index 000000000..b1094e7a0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/virtual_disk_ready.go @@ -0,0 +1,106 @@ +/* +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/vdcondition" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vdscondition" +) + +type VirtualDiskReadyHandler struct { + snapshotter VirtualDiskReadySnapshotter +} + +func NewVirtualDiskReadyHandler(snapshotter VirtualDiskReadySnapshotter) *VirtualDiskReadyHandler { + return &VirtualDiskReadyHandler{ + snapshotter: snapshotter, + } +} + +func (h VirtualDiskReadyHandler) Handle(ctx context.Context, vdSnapshot *virtv2.VirtualDiskSnapshot) (reconcile.Result, error) { + condition, ok := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + if !ok { + condition = metav1.Condition{ + Type: vdscondition.VirtualDiskReadyType, + Status: metav1.ConditionUnknown, + } + } + + defer func() { service.SetCondition(condition, &vdSnapshot.Status.Conditions) }() + + if vdSnapshot.DeletionTimestamp != nil { + condition.Status = metav1.ConditionUnknown + condition.Reason = "" + condition.Message = "" + return reconcile.Result{}, nil + } + + if vdSnapshot.Status.Phase == virtv2.VirtualDiskSnapshotPhaseReady { + condition.Status = metav1.ConditionTrue + condition.Reason = vdscondition.VirtualDiskReady + condition.Message = "" + return reconcile.Result{}, nil + } + + vd, err := h.snapshotter.GetVirtualDisk(ctx, vdSnapshot.Spec.VirtualDiskName, vdSnapshot.Namespace) + if err != nil { + return reconcile.Result{}, err + } + + if vd == nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VirtualDiskNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual disk %q not found.", vdSnapshot.Spec.VirtualDiskName) + return reconcile.Result{}, nil + } + + if vd.GetDeletionTimestamp() != nil { + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VirtualDiskNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual disk %q is in process of deletion.", vd.Name) + return reconcile.Result{}, nil + } + + switch vd.Status.Phase { + case virtv2.DiskReady: + snapshotting, _ := service.GetCondition(vdcondition.SnapshottingType, vd.Status.Conditions) + if snapshotting.Status != metav1.ConditionTrue { + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VirtualDiskNotReadyForSnapshotting + condition.Message = snapshotting.Message + return reconcile.Result{}, nil + } + + condition.Status = metav1.ConditionTrue + condition.Reason = vdscondition.VirtualDiskReady + condition.Message = "" + return reconcile.Result{}, nil + default: + condition.Status = metav1.ConditionFalse + condition.Reason = vdscondition.VirtualDiskNotReadyForSnapshotting + condition.Message = fmt.Sprintf("The virtual disk %q is in the %q phase: waiting for it to reach the Ready phase.", vd.Name, vd.Status.Phase) + return reconcile.Result{}, nil + } +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/virtual_disk_ready_test.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/virtual_disk_ready_test.go new file mode 100644 index 000000000..1cc5b3a34 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/virtual_disk_ready_test.go @@ -0,0 +1,143 @@ +/* +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" + 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/vdscondition" +) + +var _ = Describe("VirtualDiskReady handler", func() { + var snapshotter *VirtualDiskReadySnapshotterMock + var vd *virtv2.VirtualDisk + var vdSnapshot *virtv2.VirtualDiskSnapshot + + BeforeEach(func() { + vd = &virtv2.VirtualDisk{ + ObjectMeta: metav1.ObjectMeta{Name: "vd-01"}, + Status: virtv2.VirtualDiskStatus{ + Phase: virtv2.DiskReady, + Conditions: []metav1.Condition{ + { + Type: vdcondition.SnapshottingType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + vdSnapshot = &virtv2.VirtualDiskSnapshot{ + ObjectMeta: metav1.ObjectMeta{Name: "vdsnapshot"}, + Spec: virtv2.VirtualDiskSnapshotSpec{VirtualDiskName: vd.Name}, + } + + snapshotter = &VirtualDiskReadySnapshotterMock{ + GetVirtualDiskFunc: func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + return vd, nil + }, + } + }) + + Context("condition VirtualDiskReady is True", func() { + It("The virtual disk is ready for snapshotting", func() { + h := NewVirtualDiskReadyHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + ready, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionTrue)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskReady)) + Expect(ready.Message).To(BeEmpty()) + }) + }) + + Context("condition VirtualDiskReady is False", func() { + It("The virtual disk not found", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + return nil, nil + } + h := NewVirtualDiskReadyHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + ready, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskNotReadyForSnapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is in process of deletion", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + vd.DeletionTimestamp = ptr.To(metav1.Now()) + return vd, nil + } + h := NewVirtualDiskReadyHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + ready, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskNotReadyForSnapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is not Ready", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + vd.Status.Phase = virtv2.DiskPending + return vd, nil + } + h := NewVirtualDiskReadyHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + ready, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskNotReadyForSnapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + + It("The virtual disk is not ready for snapshot taking yet", func() { + snapshotter.GetVirtualDiskFunc = func(_ context.Context, _, _ string) (*virtv2.VirtualDisk, error) { + vd.Status.Conditions = nil + vd.Status.Conditions = append(vd.Status.Conditions, metav1.Condition{ + Type: vdcondition.SnapshottingType, + Status: metav1.ConditionFalse, + Reason: vdscondition.VirtualDiskNotReadyForSnapshotting, + Message: "error", + }) + return vd, nil + } + h := NewVirtualDiskReadyHandler(snapshotter) + + _, err := h.Handle(testContext(), vdSnapshot) + Expect(err).To(BeNil()) + ready, _ := service.GetCondition(vdscondition.VirtualDiskReadyType, vdSnapshot.Status.Conditions) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + Expect(ready.Reason).To(Equal(vdscondition.VirtualDiskNotReadyForSnapshotting)) + Expect(ready.Message).ToNot(BeEmpty()) + }) + }) +}) diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/kvvmi_watcher.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/kvvmi_watcher.go new file mode 100644 index 000000000..b98417358 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/kvvmi_watcher.go @@ -0,0 +1,127 @@ +/* +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" + virtv1 "kubevirt.io/api/core/v1" + "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/kvbuilder" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineInstanceWatcher struct { + client client.Client +} + +func NewVirtualMachineInstanceWatcher(client client.Client) *VirtualMachineInstanceWatcher { + return &VirtualMachineInstanceWatcher{ + client: client, + } +} + +func (w VirtualMachineInstanceWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &virtv1.VirtualMachineInstance{}), + 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 VirtualMachineInstanceWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + kvvmi, ok := obj.(*virtv1.VirtualMachineInstance) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an VirtualMachineInstance but got a %T", obj)) + return + } + + vdByName := make(map[string]struct{}) + for _, volume := range kvvmi.Spec.Volumes { + if volume.PersistentVolumeClaim == nil { + continue + } + + var vdName string + vdName, ok = kvbuilder.GetOriginalDiskName(volume.Name) + if !ok { + continue + } + + vdByName[vdName] = struct{}{} + } + + if len(vdByName) == 0 { + return + } + + var vdSnapshots virtv2.VirtualDiskSnapshotList + err := w.client.List(ctx, &vdSnapshots, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list virtual disk snapshots: %s", err)) + return + } + + for _, vdSnapshot := range vdSnapshots.Items { + _, ok = vdByName[vdSnapshot.Spec.VirtualDiskName] + if !ok { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vdSnapshot.Name, + Namespace: vdSnapshot.Namespace, + }, + }) + } + + return +} + +func (w VirtualMachineInstanceWatcher) filterUpdateEvents(e event.UpdateEvent) bool { + oldKVVMI, ok := e.ObjectOld.(*virtv1.VirtualMachineInstance) + if !ok { + slog.Default().Error(fmt.Sprintf("expected an old VirtualMachineInstance but got a %T", e.ObjectOld)) + return false + } + + newKVVMI, ok := e.ObjectNew.(*virtv1.VirtualMachineInstance) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a new VirtualMachineInstance but got a %T", e.ObjectNew)) + return false + } + + return oldKVVMI.Status.FSFreezeStatus != newKVVMI.Status.FSFreezeStatus +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vd_watcher.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vd_watcher.go new file mode 100644 index 000000000..6e390a4c3 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vd_watcher.go @@ -0,0 +1,114 @@ +/* +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/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 true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + UpdateFunc: w.filterUpdateEvents, + }, + ) +} + +func (w VirtualDiskWatcher) enqueueRequests(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + vd, ok := obj.(*virtv2.VirtualDisk) + if !ok { + slog.Default().Error(fmt.Sprintf("expected a VirtualDisk but got a %T", obj)) + return + } + + var vdSnapshots virtv2.VirtualDiskSnapshotList + err := w.client.List(ctx, &vdSnapshots, &client.ListOptions{ + Namespace: obj.GetNamespace(), + }) + if err != nil { + slog.Default().Error(fmt.Sprintf("failed to list vdsnapshots: %s", err)) + return + } + + for _, vdSnapshot := range vdSnapshots.Items { + if vdSnapshot.Spec.VirtualDiskName != vd.Name { + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: vdSnapshot.Name, + Namespace: vdSnapshot.Namespace, + }, + }) + } + + 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 + } + + if oldVD.Status.Phase != newVD.Status.Phase { + return true + } + + oldSnapshotting, _ := service.GetCondition(vdcondition.SnapshottingType, oldVD.Status.Conditions) + newSnapshotting, _ := service.GetCondition(vdcondition.SnapshottingType, newVD.Status.Conditions) + + return oldSnapshotting.Status != newSnapshotting.Status || oldSnapshotting.Reason != newSnapshotting.Reason +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vdsnapshot_watcher.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vdsnapshot_watcher.go new file mode 100644 index 000000000..ef27d3f0c --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vdsnapshot_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 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.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/vdsnapshot/internal/watcher/vs_watcher.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vs_watcher.go new file mode 100644 index 000000000..64b0ffb6e --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/internal/watcher/vs_watcher.go @@ -0,0 +1,51 @@ +/* +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 ( + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + "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 VolumeSnapshotWatcher struct{} + +func NewVolumeSnapshotWatcher() *VolumeSnapshotWatcher { + return &VolumeSnapshotWatcher{} +} + +func (w VolumeSnapshotWatcher) Watch(mgr manager.Manager, ctr controller.Controller) error { + return ctr.Watch( + source.Kind(mgr.GetCache(), &vsv1.VolumeSnapshot{}), + handler.EnqueueRequestForOwner( + mgr.GetScheme(), + mgr.GetRESTMapper(), + &virtv2.VirtualDiskSnapshot{}, + ), + 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 true }, + }, + ) +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_controller.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_controller.go new file mode 100644 index 000000000..4aae683cb --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_controller.go @@ -0,0 +1,79 @@ +/* +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 vdsnapshot + +import ( + "context" + "log/slog" + + "k8s.io/client-go/rest" + "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/vdsnapshot/internal" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +const ControllerName = "vdsnapshot-controller" + +func NewController( + ctx context.Context, + mgr manager.Manager, + log *slog.Logger, + restClient *rest.RESTClient, +) (controller.Controller, error) { + log = log.With(logger.SlogController(ControllerName)) + + protection := service.NewProtectionService(mgr.GetClient(), virtv2.FinalizerVDSnapshotProtection) + freezer := service.NewSnapshotService(restClient, mgr.GetClient(), protection) + + reconciler := NewReconciler( + mgr.GetClient(), + internal.NewVirtualDiskReadyHandler(freezer), + internal.NewLifeCycleHandler(freezer), + internal.NewDeletionHandler(freezer), + ) + + vdSnapshotController, 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, vdSnapshotController) + if err != nil { + return nil, err + } + + if err = builder.WebhookManagedBy(mgr). + For(&virtv2.VirtualDiskSnapshot{}). + WithValidator(NewValidator(log)). + Complete(); err != nil { + return nil, err + } + + log.Info("Initialized VirtualDiskSnapshot controller") + + return vdSnapshotController, nil +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_reconciler.go new file mode 100644 index 000000000..b9894efda --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_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 vdsnapshot + +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/vdsnapshot/internal/watcher" + "github.com/deckhouse/virtualization-controller/pkg/logger" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type Handler interface { + Handle(ctx context.Context, vdsnapshot *virtv2.VirtualDiskSnapshot) (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) + + vdsnapshot := service.NewResource(req.NamespacedName, r.client, r.factory, r.statusGetter) + + err := vdsnapshot.Fetch(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if vdsnapshot.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, vdsnapshot.Changed()) + if err != nil { + 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 + + err = vdsnapshot.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.NewVirtualDiskSnapshotWatcher(mgr.GetClient()), + watcher.NewVirtualDiskWatcher(mgr.GetClient()), + watcher.NewVolumeSnapshotWatcher(), + watcher.NewVirtualMachineInstanceWatcher(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.VirtualDiskSnapshot { + return &virtv2.VirtualDiskSnapshot{} +} + +func (r *Reconciler) statusGetter(obj *virtv2.VirtualDiskSnapshot) virtv2.VirtualDiskSnapshotStatus { + return obj.Status +} diff --git a/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_webhook.go b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_webhook.go new file mode 100644 index 000000000..419275723 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vdsnapshot/vdsnapshot_webhook.go @@ -0,0 +1,81 @@ +/* +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 vdsnapshot + +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) { + vds, ok := obj.(*virtv2.VirtualDiskSnapshot) + if !ok { + return nil, fmt.Errorf("expected a VirtualDiskSnapshot but got a %T", obj) + } + + if vds.Spec.VirtualDiskName == "" { + return nil, fmt.Errorf("virtual disk name cannot be empty") + } + + if vds.Spec.VolumeSnapshotClassName == "" { + return nil, fmt.Errorf("volume snapshot class name cannot be empty") + } + + return nil, nil +} + +func (v *Validator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + oldVDS, ok := oldObj.(*virtv2.VirtualDiskSnapshot) + if !ok { + return nil, fmt.Errorf("expected an old VirtualDiskSnapshot but got a %T", newObj) + } + + newVDS, ok := newObj.(*virtv2.VirtualDiskSnapshot) + if !ok { + return nil, fmt.Errorf("expected a new VirtualDiskSnapshot but got a %T", newObj) + } + + v.logger.Info("Validating VirtualDiskSnapshot") + + if oldVDS.Generation != newVDS.Generation { + return nil, fmt.Errorf("VirtualDiskSnapshot 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/images/virtualization-artifact/pkg/controller/vm/internal/filesystem.go b/images/virtualization-artifact/pkg/controller/vm/internal/filesystem.go new file mode 100644 index 000000000..663e0cb00 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/filesystem.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" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/service" + "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +const nameFilesystemHandler = "FilesystemHandler" + +var filesystemConditions = []string{ + string(vmcondition.TypeFilesystemReady), +} + +func NewFilesystemHandler() *FilesystemHandler { + return &FilesystemHandler{} +} + +type FilesystemHandler struct{} + +func (h *FilesystemHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) { + if s.VirtualMachine().IsEmpty() { + return reconcile.Result{}, nil + } + + changed := s.VirtualMachine().Changed() + + if update := addAllUnknown(changed, filesystemConditions...); update { + return reconcile.Result{Requeue: true}, nil + } + + if isDeletion(changed) { + return reconcile.Result{}, nil + } + + kvvmi, err := s.KVVMI(ctx) + if err != nil { + return reconcile.Result{}, err + } + + mgr := conditions.NewManager(changed.Status.Conditions) + cb := conditions.NewConditionBuilder2(vmcondition.TypeFilesystemReady).Generation(changed.GetGeneration()) + defer func() { mgr.Update2(cb); changed.Status.Conditions = mgr.Generate() }() + + if kvvmi == nil { + cb.Status(metav1.ConditionFalse). + Reason2(vmcondition.ReasonFilesystemNotReady). + Message(fmt.Sprintf("The internal virtual machine %s is not running.", changed.Name)) + return reconcile.Result{}, nil + } + + agentReady, _ := service.GetCondition(vmcondition.TypeAgentReady.String(), changed.Status.Conditions) + if agentReady.Status != metav1.ConditionTrue { + cb.Status(metav1.ConditionUnknown) + return reconcile.Result{}, nil + } + + if kvvmi.Status.FSFreezeStatus == "frozen" { + cb.Status(metav1.ConditionFalse). + Reason2(vmcondition.ReasonFilesystemFrozen). + Message(fmt.Sprintf("The internal virtual machine %s is frozen.", changed.Name)) + return reconcile.Result{}, nil + } + + cb.Status(metav1.ConditionTrue). + Reason2(vmcondition.ReasonFilesystemReady) + return reconcile.Result{}, nil +} + +func (h *FilesystemHandler) Name() string { + return nameFilesystemHandler +} diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go index 138786585..cd0bb5379 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.NewBlockDeviceHandler(client, recorder), internal.NewProvisioningHandler(client), internal.NewAgentHandler(), + internal.NewFilesystemHandler(), internal.NewPodHandler(client), internal.NewSyncKvvmHandler(dvcrSettings, client, recorder), internal.NewSyncMetadataHandler(client), diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index c60f505d1..def0ca00a 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -49,6 +49,7 @@ rules: - get - list - update + - create - delete - watch - patch @@ -123,6 +124,18 @@ rules: - update - list - delete +- apiGroups: + - snapshot.storage.k8s.io + resources: + - volumesnapshots + verbs: + - get + - watch + - create + - patch + - update + - list + - delete - apiGroups: - internal.virtualization.deckhouse.io resources: @@ -138,6 +151,13 @@ rules: verbs: - patch - update +- apiGroups: + - subresources.virtualization.deckhouse.io + resources: + - virtualmachines/freeze + - virtualmachines/unfreeze + verbs: + - update - apiGroups: - subresources.kubevirt.io resources: @@ -160,6 +180,7 @@ rules: - clustervirtualimages - virtualmachineoperations - virtualmachineclasses + - virtualdisksnapshots verbs: - create - delete @@ -181,6 +202,7 @@ rules: - virtualmachineipaddresses/finalizers - virtualmachineoperations/finalizers - virtualmachineclasses/finalizers + - virtualdisksnapshots/finalizers - virtualmachineipaddresses/status - virtualmachineipaddressleases/status - virtualdisks/status @@ -191,6 +213,7 @@ rules: - clustervirtualimages/status - virtualmachineoperations/status - virtualmachineclasses/status + - virtualdisksnapshots/status verbs: - patch - update diff --git a/templates/virtualization-controller/validation-webhook.yaml b/templates/virtualization-controller/validation-webhook.yaml index 22131740b..2efad7d83 100644 --- a/templates/virtualization-controller/validation-webhook.yaml +++ b/templates/virtualization-controller/validation-webhook.yaml @@ -139,4 +139,20 @@ webhooks: {{ .Values.virtualization.internal.controller.cert.ca }} admissionReviewVersions: ["v1"] sideEffects: None - + - name: "vdsnapshot.virtualization-controller.validate.d8-virtualization" + rules: + - apiGroups: ["virtualization.deckhouse.io"] + apiVersions: ["v1alpha2"] + operations: ["CREATE", "UPDATE"] + resources: ["virtualdisksnapshots"] + scope: "Namespaced" + clientConfig: + service: + namespace: d8-{{ .Chart.Name }} + name: virtualization-controller + path: /validate-virtualization-deckhouse-io-v1alpha2-virtualdisksnapshot + port: 443 + caBundle: | + {{ .Values.virtualization.internal.controller.cert.ca }} + admissionReviewVersions: ["v1"] + sideEffects: None