diff --git a/api/client/kubeclient/client.go b/api/client/kubeclient/client.go index b8e4cec35..98c0bdb5d 100644 --- a/api/client/kubeclient/client.go +++ b/api/client/kubeclient/client.go @@ -82,6 +82,7 @@ type VirtualMachineInterface interface { PortForward(name string, opts v1alpha2.VirtualMachinePortForward) (StreamInterface, error) Freeze(ctx context.Context, name string, opts v1alpha2.VirtualMachineFreeze) error Unfreeze(ctx context.Context, name string) error + Migrate(ctx context.Context, name string, opts v1alpha2.VirtualMachineMigrate) error } type client struct { diff --git a/api/client/kubeclient/request.go b/api/client/kubeclient/request.go index 00689fead..859e82c23 100644 --- a/api/client/kubeclient/request.go +++ b/api/client/kubeclient/request.go @@ -28,7 +28,7 @@ import ( "k8s.io/client-go/rest" ) -const operationURLTpl = "/apis/subresources.virtualization.deckhouse.io/v1alpha2/namespaces/%s/%s/%s/%s" +const subresourceURLTpl = "/apis/subresources.virtualization.deckhouse.io/v1alpha2/namespaces/%s/%s/%s/%s" func RequestFromConfig(config *rest.Config, resource, name, namespace, subresource string, queryParams url.Values) (*http.Request, error) { @@ -48,7 +48,7 @@ func RequestFromConfig(config *rest.Config, resource, name, namespace, subresour u.Path = path.Join( u.Path, - fmt.Sprintf(operationURLTpl, namespace, resource, name, subresource), + fmt.Sprintf(subresourceURLTpl, namespace, resource, name, subresource), ) if len(queryParams) > 0 { u.RawQuery = queryParams.Encode() diff --git a/api/client/kubeclient/vm.go b/api/client/kubeclient/vm.go index 14c2363bb..3d2f41eb5 100644 --- a/api/client/kubeclient/vm.go +++ b/api/client/kubeclient/vm.go @@ -112,7 +112,7 @@ func (v vm) PortForward(name string, opts v1alpha2.VirtualMachinePortForward) (S } func (v vm) Freeze(ctx context.Context, name string, opts v1alpha2.VirtualMachineFreeze) error { - path := fmt.Sprintf(operationURLTpl, v.namespace, v.resource, name, "freeze") + path := fmt.Sprintf(subresourceURLTpl, v.namespace, v.resource, name, "freeze") unfreezeTimeout := virtv1.FreezeUnfreezeTimeout{ UnfreezeTimeout: &metav1.Duration{}, @@ -127,21 +127,25 @@ func (v vm) Freeze(ctx context.Context, name string, opts v1alpha2.VirtualMachin return err } - err = v.restClient.Put().AbsPath(path).Body(body).Do(ctx).Error() - if err != nil { - return err - } - - return nil + return v.restClient.Put().AbsPath(path).Body(body).Do(ctx).Error() } func (v vm) Unfreeze(ctx context.Context, name string) error { - path := fmt.Sprintf(operationURLTpl, v.namespace, v.resource, name, "unfreeze") + path := fmt.Sprintf(subresourceURLTpl, v.namespace, v.resource, name, "unfreeze") + + return v.restClient.Put().AbsPath(path).Do(ctx).Error() +} + +func (v vm) Migrate(ctx context.Context, name string, opts v1alpha2.VirtualMachineMigrate) error { + path := fmt.Sprintf(subresourceURLTpl, v.namespace, v.resource, name, "migrate") - err := v.restClient.Put().AbsPath(path).Do(ctx).Error() + migrateOpts := virtv1.MigrateOptions{ + DryRun: opts.DryRun, + } + + body, err := json.Marshal(&migrateOpts) if err != nil { return err } - - return nil + return v.restClient.Put().AbsPath(path).Body(body).Do(ctx).Error() } diff --git a/api/core/v1alpha2/virtual_machine_operation.go b/api/core/v1alpha2/virtual_machine_operation.go index c813a0184..aef8ba41d 100644 --- a/api/core/v1alpha2/virtual_machine_operation.go +++ b/api/core/v1alpha2/virtual_machine_operation.go @@ -23,7 +23,15 @@ const ( VMOPResource = "virtualmachineoperations" ) -// VirtualMachineOperation is operation performed on the VirtualMachine. +// VirtualMachineOperation resource provides the ability to declaratively manage state changes of virtual machines. +// +kubebuilder:object:root=true +// +kubebuilder:metadata:labels={heritage=deckhouse,module=virtualization} +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={virtualization,all},scope=Namespaced,shortName={vmop,vmops},singular=virtualmachineoperation +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="VirtualMachineOperation phase." +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="VirtualMachineOperation type." +// +kubebuilder:printcolumn:name="VirtualMachine",type="string",JSONPath=".spec.virtualMachineName",description="VirtualMachine name." +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time of creation resource." // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type VirtualMachineOperation struct { @@ -34,16 +42,22 @@ type VirtualMachineOperation struct { Status VirtualMachineOperationStatus `json:"status,omitempty"` } +// +kubebuilder:validation:XValidation:rule="self.type == 'Start' ? !has(self.force) || !self.force : true",message="The `Start` operation cannot be performed forcibly." +// +kubebuilder:validation:XValidation:rule="self.type == 'Migrate' ? !has(self.force) || !self.force : true",message="The `Migrate` operation cannot be performed forcibly." type VirtualMachineOperationSpec struct { - Type VMOPType `json:"type"` - VirtualMachine string `json:"virtualMachineName"` - Force bool `json:"force,omitempty"` + Type VMOPType `json:"type"` + // The name of the virtual machine for which the operation is performed. + VirtualMachine string `json:"virtualMachineName"` + // Force the execution of the operation. Applies only for Restart and Stop. In this case, the action on the virtual machine is performed immediately. + Force bool `json:"force,omitempty"` } type VirtualMachineOperationStatus struct { - Phase VMOPPhase `json:"phase"` - Conditions []metav1.Condition `json:"conditions,omitempty"` - ObservedGeneration int64 `json:"observedGeneration,omitempty"` + Phase VMOPPhase `json:"phase"` + // The latest detailed observations of the VirtualMachineOperation resource. + Conditions []metav1.Condition `json:"conditions,omitempty"` + // The generation last processed by the controller. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // VirtualMachineOperationList contains a list of VirtualMachineOperation @@ -54,6 +68,13 @@ type VirtualMachineOperationList struct { Items []VirtualMachineOperation `json:"items"` } +// Represents the current phase of resource: +// * Pending - the operation is queued for execution. +// * InProgress - operation in progress. +// * Completed - the operation was successful. +// * Failed - the operation failed. Check conditions and events for more information. +// * Terminating - the operation is deleted. +// +kubebuilder:validation:Enum={Pending,InProgress,Completed,Failed,Terminating} type VMOPPhase string const ( @@ -64,10 +85,17 @@ const ( VMOPPhaseTerminating VMOPPhase = "Terminating" ) +// Type is operation over the virtualmachine: +// * Start - start the virtualmachine. +// * Stop - stop the virtualmachine. +// * Restart - restart the virtualmachine. +// * Migrate - migrate the virtualmachine. +// +kubebuilder:validation:Enum={Restart,Start,Stop,Migrate} type VMOPType string const ( VMOPTypeRestart VMOPType = "Restart" VMOPTypeStart VMOPType = "Start" VMOPTypeStop VMOPType = "Stop" + VMOPTypeMigrate VMOPType = "Migrate" ) diff --git a/api/core/v1alpha2/vmopcondition/condition.go b/api/core/v1alpha2/vmopcondition/condition.go index a2a285c26..5177094a4 100644 --- a/api/core/v1alpha2/vmopcondition/condition.go +++ b/api/core/v1alpha2/vmopcondition/condition.go @@ -61,6 +61,9 @@ const ( // ReasonStopInProgress is a ReasonCompleted indicating that the stop signal has been sent and stop is in progress. ReasonStopInProgress ReasonCompleted = "StopInProgress" + // ReasonMigrationInProgress is a ReasonCompleted indicating that the migrate signal has been sent and stop is in progress. + ReasonMigrationInProgress ReasonCompleted = "MigrationInProgress" + // ReasonOperationFailed is a ReasonCompleted indicating that operation has failed. ReasonOperationFailed ReasonCompleted = "OperationFailed" diff --git a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 3658802ca..991ad0d55 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -129,6 +129,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "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.VirtualMachineMigrate": schema_virtualization_api_subresources_v1alpha2_VirtualMachineMigrate(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), @@ -3940,7 +3941,7 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineOperation(ref common. return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "VirtualMachineOperation is operation performed on the VirtualMachine.", + Description: "VirtualMachineOperation resource provides the ability to declaratively manage state changes of virtual machines.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -4048,15 +4049,17 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineOperationSpec(ref com }, "virtualMachineName": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "The name of the virtual machine for which the operation is performed.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "force": { SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", + Description: "Force the execution of the operation. Applies only for Restart and Stop. In this case, the action on the virtual machine is performed immediately.", + Type: []string{"boolean"}, + Format: "", }, }, }, @@ -4081,7 +4084,8 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineOperationStatus(ref c }, "conditions": { SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, + Description: "The latest detailed observations of the VirtualMachineOperation resource.", + Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -4094,8 +4098,9 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineOperationStatus(ref c }, "observedGeneration": { SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", + Description: "The generation last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", }, }, }, @@ -4599,6 +4604,46 @@ func schema_virtualization_api_subresources_v1alpha2_VirtualMachineFreeze(ref co } } +func schema_virtualization_api_subresources_v1alpha2_VirtualMachineMigrate(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: "", + }, + }, + "dryRun": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema_virtualization_api_subresources_v1alpha2_VirtualMachinePortForward(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/api/scripts/update-codegen.sh b/api/scripts/update-codegen.sh index a2b67a2dd..70d36f8f5 100755 --- a/api/scripts/update-codegen.sh +++ b/api/scripts/update-codegen.sh @@ -32,7 +32,9 @@ function source::settings { MODULE="github.com/deckhouse/virtualization/api" PREFIX_GROUP="virtualization.deckhouse.io_" # TODO: Temporary filter until all CRDs become auto-generated. - ALLOWED_RESOURCE_GEN_CRD=("VirtualMachineClass" "VirtualMachineBlockDeviceAttachment" "ExampleKind1" "ExampleKind2") + ALLOWED_RESOURCE_GEN_CRD=("VirtualMachineClass" + "VirtualMachineBlockDeviceAttachment" + "VirtualMachineOperation") source "${CODEGEN_PKG}/kube_codegen.sh" } diff --git a/api/subresources/register.go b/api/subresources/register.go index 1f714aff0..0c036f7b9 100644 --- a/api/subresources/register.go +++ b/api/subresources/register.go @@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineRemoveVolume{}, &VirtualMachineFreeze{}, &VirtualMachineUnfreeze{}, + &VirtualMachineMigrate{}, &virtv2.VirtualMachine{}, &virtv2.VirtualMachineList{}, ) diff --git a/api/subresources/types.go b/api/subresources/types.go index 321c2e3d6..264029e84 100644 --- a/api/subresources/types.go +++ b/api/subresources/types.go @@ -78,3 +78,12 @@ type VirtualMachineFreeze struct { type VirtualMachineUnfreeze struct { metav1.TypeMeta } + +// +genclient +// +genclient:readonly +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type VirtualMachineMigrate struct { + metav1.TypeMeta + DryRun []string +} diff --git a/api/subresources/v1alpha2/register.go b/api/subresources/v1alpha2/register.go index 87d8d39c6..5b7e58f59 100644 --- a/api/subresources/v1alpha2/register.go +++ b/api/subresources/v1alpha2/register.go @@ -58,6 +58,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineRemoveVolume{}, &VirtualMachineFreeze{}, &VirtualMachineUnfreeze{}, + &VirtualMachineMigrate{}, &virtv2.VirtualMachine{}, &virtv2.VirtualMachineList{}, ) diff --git a/api/subresources/v1alpha2/types.go b/api/subresources/v1alpha2/types.go index 9e1437bc0..916d7cbad 100644 --- a/api/subresources/v1alpha2/types.go +++ b/api/subresources/v1alpha2/types.go @@ -85,3 +85,13 @@ type VirtualMachineFreeze struct { type VirtualMachineUnfreeze 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 VirtualMachineMigrate struct { + metav1.TypeMeta `json:",inline"` + DryRun []string `json:"dryRun,omitempty"` +} diff --git a/api/subresources/v1alpha2/zz_generated.conversion.go b/api/subresources/v1alpha2/zz_generated.conversion.go index 7c708d573..330a5be60 100644 --- a/api/subresources/v1alpha2/zz_generated.conversion.go +++ b/api/subresources/v1alpha2/zz_generated.conversion.go @@ -68,6 +68,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*VirtualMachineMigrate)(nil), (*subresources.VirtualMachineMigrate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_VirtualMachineMigrate_To_subresources_VirtualMachineMigrate(a.(*VirtualMachineMigrate), b.(*subresources.VirtualMachineMigrate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*subresources.VirtualMachineMigrate)(nil), (*VirtualMachineMigrate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_subresources_VirtualMachineMigrate_To_v1alpha2_VirtualMachineMigrate(a.(*subresources.VirtualMachineMigrate), b.(*VirtualMachineMigrate), 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 { @@ -123,6 +133,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineMigrate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v1alpha2_VirtualMachineMigrate(a.(*url.Values), b.(*VirtualMachineMigrate), 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 { @@ -240,6 +255,42 @@ func Convert_url_Values_To_v1alpha2_VirtualMachineFreeze(in *url.Values, out *Vi return autoConvert_url_Values_To_v1alpha2_VirtualMachineFreeze(in, out, s) } +func autoConvert_v1alpha2_VirtualMachineMigrate_To_subresources_VirtualMachineMigrate(in *VirtualMachineMigrate, out *subresources.VirtualMachineMigrate, s conversion.Scope) error { + out.DryRun = *(*[]string)(unsafe.Pointer(&in.DryRun)) + return nil +} + +// Convert_v1alpha2_VirtualMachineMigrate_To_subresources_VirtualMachineMigrate is an autogenerated conversion function. +func Convert_v1alpha2_VirtualMachineMigrate_To_subresources_VirtualMachineMigrate(in *VirtualMachineMigrate, out *subresources.VirtualMachineMigrate, s conversion.Scope) error { + return autoConvert_v1alpha2_VirtualMachineMigrate_To_subresources_VirtualMachineMigrate(in, out, s) +} + +func autoConvert_subresources_VirtualMachineMigrate_To_v1alpha2_VirtualMachineMigrate(in *subresources.VirtualMachineMigrate, out *VirtualMachineMigrate, s conversion.Scope) error { + out.DryRun = *(*[]string)(unsafe.Pointer(&in.DryRun)) + return nil +} + +// Convert_subresources_VirtualMachineMigrate_To_v1alpha2_VirtualMachineMigrate is an autogenerated conversion function. +func Convert_subresources_VirtualMachineMigrate_To_v1alpha2_VirtualMachineMigrate(in *subresources.VirtualMachineMigrate, out *VirtualMachineMigrate, s conversion.Scope) error { + return autoConvert_subresources_VirtualMachineMigrate_To_v1alpha2_VirtualMachineMigrate(in, out, s) +} + +func autoConvert_url_Values_To_v1alpha2_VirtualMachineMigrate(in *url.Values, out *VirtualMachineMigrate, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + if values, ok := map[string][]string(*in)["dryRun"]; ok && len(values) > 0 { + out.DryRun = *(*[]string)(unsafe.Pointer(&values)) + } else { + out.DryRun = nil + } + return nil +} + +// Convert_url_Values_To_v1alpha2_VirtualMachineMigrate is an autogenerated conversion function. +func Convert_url_Values_To_v1alpha2_VirtualMachineMigrate(in *url.Values, out *VirtualMachineMigrate, s conversion.Scope) error { + return autoConvert_url_Values_To_v1alpha2_VirtualMachineMigrate(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 diff --git a/api/subresources/v1alpha2/zz_generated.deepcopy.go b/api/subresources/v1alpha2/zz_generated.deepcopy.go index 4ea3bebee..708abe50c 100644 --- a/api/subresources/v1alpha2/zz_generated.deepcopy.go +++ b/api/subresources/v1alpha2/zz_generated.deepcopy.go @@ -106,6 +106,36 @@ func (in *VirtualMachineFreeze) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineMigrate) DeepCopyInto(out *VirtualMachineMigrate) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineMigrate. +func (in *VirtualMachineMigrate) DeepCopy() *VirtualMachineMigrate { + if in == nil { + return nil + } + out := new(VirtualMachineMigrate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineMigrate) 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 diff --git a/api/subresources/zz_generated.deepcopy.go b/api/subresources/zz_generated.deepcopy.go index 3e68e8a95..4b48199f6 100644 --- a/api/subresources/zz_generated.deepcopy.go +++ b/api/subresources/zz_generated.deepcopy.go @@ -106,6 +106,36 @@ func (in *VirtualMachineFreeze) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineMigrate) DeepCopyInto(out *VirtualMachineMigrate) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineMigrate. +func (in *VirtualMachineMigrate) DeepCopy() *VirtualMachineMigrate { + if in == nil { + return nil + } + out := new(VirtualMachineMigrate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineMigrate) 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 diff --git a/crds/doc-ru-virtualmachineoperations.yaml b/crds/doc-ru-virtualmachineoperations.yaml index ba53283b9..d3fbc9386 100644 --- a/crds/doc-ru-virtualmachineoperations.yaml +++ b/crds/doc-ru-virtualmachineoperations.yaml @@ -15,6 +15,7 @@ spec: * Start - запустить виртуальную машину. * Stop - остановить виртуальную машину. * Restart - перезапустить виртуальную машину. + * Migrate - мигрировать виртуальную машину на другой узел, доступный для запуска данной ВМ. virtualMachineName: description: | Имя виртуальной машины, для которой выполняется операция. diff --git a/crds/virtualmachineoperations.yaml b/crds/virtualmachineoperations.yaml index 98751ace9..8623e4294 100644 --- a/crds/virtualmachineoperations.yaml +++ b/crds/virtualmachineoperations.yaml @@ -1,100 +1,172 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: virtualmachineoperations.virtualization.deckhouse.io + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 labels: heritage: deckhouse module: virtualization + name: virtualmachineoperations.virtualization.deckhouse.io spec: group: virtualization.deckhouse.io - scope: Namespaced names: - plural: virtualmachineoperations - singular: virtualmachineoperation - listKind: VirtualMachineOperationList + categories: + - virtualization + - all kind: VirtualMachineOperation + listKind: VirtualMachineOperationList + plural: virtualmachineoperations shortNames: - vmop - vmops - preserveUnknownFields: false + singular: virtualmachineoperation + scope: Namespaced versions: - - name: v1alpha2 - served: true - storage: true + - additionalPrinterColumns: + - description: VirtualMachineOperation phase. + jsonPath: .status.phase + name: Phase + type: string + - description: VirtualMachineOperation type. + jsonPath: .spec.type + name: Type + type: string + - description: VirtualMachine name. + jsonPath: .spec.virtualMachineName + name: VirtualMachine + type: string + - description: Time of creation resource. + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: - type: object - description: | - This resource provides the ability to declaratively manage state changes of virtual machines. - required: - - spec + description: + VirtualMachineOperation resource provides the ability to declaratively + manage state changes of virtual machines. properties: - spec: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: type: object - required: - - virtualMachineName + spec: properties: + force: + description: + Force the execution of the operation. Applies only for + Restart and Stop. In this case, the action on the virtual machine + is performed immediately. + type: boolean type: - type: string - enum: ["Start", "Stop", "Restart"] - description: | - Operation over the virtualmachine: + description: |- + Type is operation over the virtualmachine: * Start - start the virtualmachine. * Stop - stop the virtualmachine. * Restart - restart the virtualmachine. + * Migrate - migrate the virtualmachine. + enum: + - Restart + - Start + - Stop + - Migrate + type: string virtualMachineName: + description: + The name of the virtual machine for which the operation + is performed. type: string - description: | - The name of the virtual machine for which the operation is performed. - force: - type: boolean - description: | - Force the execution of the operation. Applies only for Restart and Stop. In this case, the action on the virtual machine is performed immediately. - oneOf: - - properties: - type: - enum: ["Start"] - required: ["virtualMachineName"] - not: - anyOf: - - required: - - force - - properties: - type: - enum: ["Restart", "Stop"] - required: ["virtualMachineName"] - status: - description: | - The latest observed state of the VirtualMachineOperation resource. + required: + - type + - virtualMachineName type: object + x-kubernetes-validations: + - message: The `Start` operation cannot be performed forcibly. + rule: "self.type == 'Start' ? !has(self.force) || !self.force : true" + - message: The `Migrate` operation cannot be performed forcibly. + rule: + "self.type == 'Migrate' ? !has(self.force) || !self.force : + true" + status: properties: conditions: - description: | - The latest detailed observations of the VirtualMachineOperation resource. - type: array + description: + The latest detailed observations of the VirtualMachineOperation + resource. items: - type: object + description: + "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" properties: - 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. + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: - description: Human readable message indicating details about last transition. + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer reason: - description: (brief) reason for the condition's last transition. + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: - description: Status of the condition, one of True, False, Unknown. + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string - enum: ["True", "False", "Unknown"] type: - description: Type of condition. + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime @@ -102,38 +174,34 @@ spec: - reason - status - type + type: object + type: array + observedGeneration: + description: " The generation last processed by the controller." + format: int64 + type: integer phase: - type: string - description: | + description: |- Represents the current phase of resource: - * Pending - the operation is queued for execution. * InProgress - operation in progress. * Completed - the operation was successful. * Failed - the operation failed. Check conditions and events for more information. * Terminating - the operation is deleted. enum: - - "Pending" - - "InProgress" - - "Completed" - - "Failed" - - "Terminating" - observedGeneration: - type: integer - description: | - The generation last processed by the controller. - additionalPrinterColumns: - - name: Phase - type: string - jsonPath: .status.phase - - name: Type - jsonPath: .spec.type - type: string - - name: VM - jsonPath: .spec.virtualMachineName - type: string - - name: Age - type: date - jsonPath: .metadata.creationTimestamp + - Pending + - InProgress + - Completed + - Failed + - Terminating + type: string + required: + - phase + type: object + required: + - spec + type: object + served: true + storage: true subresources: status: {} diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index 088c9fb7a..e1405db52 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -261,7 +261,7 @@ func main() { os.Exit(1) } - if err = vmop.SetupController(ctx, mgr, log); err != nil { + if err = vmop.SetupController(ctx, mgr, virtClient, log); err != nil { log.Error(err.Error()) os.Exit(1) } diff --git a/images/virtualization-artifact/pkg/apiserver/api/install.go b/images/virtualization-artifact/pkg/apiserver/api/install.go index bf5dc794e..6bd7b683c 100644 --- a/images/virtualization-artifact/pkg/apiserver/api/install.go +++ b/images/virtualization-artifact/pkg/apiserver/api/install.go @@ -66,6 +66,7 @@ func Build(store *storage.VirtualMachineStorage) genericapiserver.APIGroupInfo { "virtualmachines/removevolume": store.RemoveVolumeREST(), "virtualmachines/freeze": store.FreezeREST(), "virtualmachines/unfreeze": store.UnfreezeREST(), + "virtualmachines/migrate": store.Migrate(), } apiGroupInfo.VersionedResourcesStorageMap[v1alpha2.SchemeGroupVersion.Version] = resources return apiGroupInfo diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/add_volume.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/add_volume.go index 6d6c8150f..2688425c9 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/add_volume.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/add_volume.go @@ -87,5 +87,5 @@ func AddVolumeLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "addvolume", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("addvolume"), kubevirt, proxyCertManager) } diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/console.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/console.go index 552bd770c..1bdd8d83e 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/console.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/console.go @@ -96,5 +96,5 @@ func ConsoleLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "console", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("console"), kubevirt, proxyCertManager) } diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go index 8dccdd3ce..327e5807b 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/freeze.go @@ -87,5 +87,5 @@ func FreezeLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "freeze", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("freeze"), kubevirt, proxyCertManager) } diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/migrate.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/migrate.go new file mode 100644 index 000000000..65e37aa4b --- /dev/null +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/migrate.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" + + "github.com/deckhouse/virtualization-controller/pkg/tls/certmanager" + virtlisters "github.com/deckhouse/virtualization/api/client/generated/listers/core/v1alpha2" + "github.com/deckhouse/virtualization/api/subresources" +) + +type MigrateREST struct { + vmLister virtlisters.VirtualMachineLister + proxyCertManager certmanager.CertificateManager + kubevirt KubevirtApiServerConfig +} + +var ( + _ rest.Storage = &MigrateREST{} + _ rest.Connecter = &MigrateREST{} +) + +func NewMigrateREST(vmLister virtlisters.VirtualMachineLister, kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager) *MigrateREST { + return &MigrateREST{ + vmLister: vmLister, + kubevirt: kubevirt, + proxyCertManager: proxyCertManager, + } +} + +func (r MigrateREST) New() runtime.Object { + return &subresources.VirtualMachineMigrate{} +} + +func (r MigrateREST) Destroy() { +} + +func (r MigrateREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + migrateOpts, ok := opts.(*subresources.VirtualMachineMigrate) + if !ok { + return nil, fmt.Errorf("invalid options object: %#v", opts) + } + location, transport, err := MigrateLocation(ctx, r.vmLister, name, migrateOpts, 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 MigrateREST) NewConnectOptions() (runtime.Object, bool, string) { + return &subresources.VirtualMachineMigrate{}, false, "" +} + +// ConnectMethods implements rest.Connecter interface +func (r MigrateREST) ConnectMethods() []string { + return []string{http.MethodPut} +} + +func MigrateLocation( + ctx context.Context, + getter virtlisters.VirtualMachineLister, + name string, + opts *subresources.VirtualMachineMigrate, + kubevirt KubevirtApiServerConfig, + proxyCertManager certmanager.CertificateManager, +) (*url.URL, *http.Transport, error) { + return streamLocation(ctx, getter, name, opts, newKVVMPather("migrate"), kubevirt, proxyCertManager) +} diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/portforward.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/portforward.go index e251cfd13..ad4e45154 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/portforward.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/portforward.go @@ -92,7 +92,7 @@ func PortForwardLocation( proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { streamPath := buildPortForwardResourcePath(opts) - return streamLocation(ctx, getter, name, opts, streamPath, kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather(streamPath), kubevirt, proxyCertManager) } func buildPortForwardResourcePath(opts *subresources.VirtualMachinePortForward) string { diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/remove_volume.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/remove_volume.go index 1a53e8a32..d3c665113 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/remove_volume.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/remove_volume.go @@ -87,5 +87,5 @@ func RemoveVolumeRESTLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "removevolume", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("removevolume"), 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 3165b0b96..fe239be6f 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/stream.go @@ -38,11 +38,35 @@ import ( ) const ( - userHeader = "X-Remote-User" - groupHeader = "X-Remote-Group" - kubevirtPathTmpl = "/apis/subresources.kubevirt.io/v1/namespaces/%s/virtualmachineinstances/%s/%s" + userHeader = "X-Remote-User" + groupHeader = "X-Remote-Group" + kvvmiPathTmpl = "/apis/subresources.kubevirt.io/v1/namespaces/%s/virtualmachineinstances/%s/%s" + kvvmPathTmpl = "/apis/subresources.kubevirt.io/v1/namespaces/%s/virtualmachines/%s/%s" ) +func newKVVMIPather(subresource string) pather { + return pather{ + template: kvvmiPathTmpl, + subresource: subresource, + } +} + +func newKVVMPather(subresource string) pather { + return pather{ + template: kvvmPathTmpl, + subresource: subresource, + } +} + +type pather struct { + template string + subresource string +} + +func (p pather) Path(namespace, name string) string { + return fmt.Sprintf(p.template, namespace, name, p.subresource) +} + var upgradeableMethods = []string{http.MethodGet, http.MethodPost} func streamLocation( @@ -50,7 +74,7 @@ func streamLocation( getter virtlisters.VirtualMachineLister, name string, opts runtime.Object, - streamPath string, + pather pather, kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { @@ -72,7 +96,7 @@ func streamLocation( location := &url.URL{ Scheme: "https", Host: kubevirt.Endpoint, - Path: fmt.Sprintf(kubevirtPathTmpl, vm.Namespace, name, streamPath), + Path: pather.Path(vm.Namespace, name), RawQuery: params.Encode(), } ca, err := os.ReadFile(kubevirt.CaBundlePath) @@ -112,6 +136,8 @@ func streamParams(_ url.Values, opts runtime.Object) error { return nil case *subresources.VirtualMachineUnfreeze: return nil + case *subresources.VirtualMachineMigrate: + 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 index 0e45131c1..80ff7c930 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/unfreeze.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/unfreeze.go @@ -87,5 +87,5 @@ func UnfreezeLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "unfreeze", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("unfreeze"), kubevirt, proxyCertManager) } diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/vnc.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/vnc.go index 6e147c346..793f478a5 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/vnc.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/vnc.go @@ -89,5 +89,5 @@ func VNCLocation( kubevirt KubevirtApiServerConfig, proxyCertManager certmanager.CertificateManager, ) (*url.URL, *http.Transport, error) { - return streamLocation(ctx, getter, name, opts, "vnc", kubevirt, proxyCertManager) + return streamLocation(ctx, getter, name, opts, newKVVMIPather("vnc"), 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 c35deadf8..f6d721a2f 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go @@ -49,6 +49,7 @@ type VirtualMachineStorage struct { removeVolume *vmrest.RemoveVolumeREST freeze *vmrest.FreezeREST unfreeze *vmrest.UnfreezeREST + migrate *vmrest.MigrateREST convertor rest.TableConvertor vmClient versionedv1alpha2.VirtualMachinesGetter } @@ -97,6 +98,7 @@ func NewStorage( removeVolume: vmrest.NewRemoveVolumeREST(vmLister, kubevirt, proxyCertManager), freeze: vmrest.NewFreezeREST(vmLister, kubevirt, proxyCertManager), unfreeze: vmrest.NewUnfreezeREST(vmLister, kubevirt, proxyCertManager), + migrate: vmrest.NewMigrateREST(vmLister, kubevirt, proxyCertManager), convertor: convertor, vmClient: vmClient, } @@ -130,6 +132,8 @@ func (store VirtualMachineStorage) UnfreezeREST() *vmrest.UnfreezeREST { return store.unfreeze } +func (store VirtualMachineStorage) Migrate() *vmrest.MigrateREST { return store.migrate } + // New implements rest.Storage interface func (store VirtualMachineStorage) New() runtime.Object { return &virtv2.VirtualMachine{} diff --git a/images/virtualization-artifact/pkg/controller/service/vm_operation.go b/images/virtualization-artifact/pkg/controller/service/vm_operation.go index 8bea59108..189442210 100644 --- a/images/virtualization-artifact/pkg/controller/service/vm_operation.go +++ b/images/virtualization-artifact/pkg/controller/service/vm_operation.go @@ -19,7 +19,9 @@ package service import ( "context" "fmt" + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,17 +29,21 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/common" "github.com/deckhouse/virtualization-controller/pkg/controller/powerstate" "github.com/deckhouse/virtualization-controller/pkg/sdk/framework/helper" + "github.com/deckhouse/virtualization/api/client/kubeclient" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" "github.com/deckhouse/virtualization/api/core/v1alpha2/vmopcondition" + "github.com/deckhouse/virtualization/api/subresources/v1alpha2" ) type VMOperationService struct { - client client.Client + client client.Client + virtClient kubeclient.Client } -func NewVMOperationService(client client.Client) VMOperationService { +func NewVMOperationService(client client.Client, virtClient kubeclient.Client) VMOperationService { return VMOperationService{ - client: client, + client: client, + virtClient: virtClient, } } @@ -57,6 +63,8 @@ func (s VMOperationService) Do(ctx context.Context, vmop *virtv2.VirtualMachineO return s.DoStop(ctx, vmop.GetNamespace(), vmop.Spec.VirtualMachine, vmop.Spec.Force) case virtv2.VMOPTypeRestart: return s.DoRestart(ctx, vmop.GetNamespace(), vmop.Spec.VirtualMachine, vmop.Spec.Force) + case virtv2.VMOPTypeMigrate: + return s.DoMigrate(ctx, vmop.GetNamespace(), vmop.Spec.VirtualMachine) default: return fmt.Errorf("unexpected operation type %q: %w", vmop.Spec.Type, common.ErrUnknownValue) } @@ -90,6 +98,14 @@ func (s VMOperationService) DoRestart(ctx context.Context, vmNamespace, vmName s return powerstate.RestartVM(ctx, s.client, kvvm, kvvmi, force) } +func (s VMOperationService) DoMigrate(ctx context.Context, vmNamespace, vmName string) error { + err := s.virtClient.VirtualMachines(vmNamespace).Migrate(ctx, vmName, v1alpha2.VirtualMachineMigrate{}) + if err != nil { + return fmt.Errorf(`failed to migrate virtual machine "%s/%s": %w`, vmNamespace, vmName, err) + } + return nil +} + func (s VMOperationService) IsAllowedForVM(vmop *virtv2.VirtualMachineOperation, vm *virtv2.VirtualMachine) bool { if vm == nil { return false @@ -100,7 +116,7 @@ func (s VMOperationService) IsAllowedForVM(vmop *virtv2.VirtualMachineOperation, func (s VMOperationService) IsApplicableForRunPolicy(vmop *virtv2.VirtualMachineOperation, runPolicy virtv2.RunPolicy) bool { switch runPolicy { case virtv2.AlwaysOnPolicy: - return vmop.Spec.Type == virtv2.VMOPTypeRestart + return vmop.Spec.Type == virtv2.VMOPTypeRestart || vmop.Spec.Type == virtv2.VMOPTypeMigrate case virtv2.AlwaysOffPolicy: return false case virtv2.ManualPolicy, virtv2.AlwaysOnUnlessStoppedManually: @@ -124,6 +140,8 @@ func (s VMOperationService) IsApplicableForVMPhase(vmop *virtv2.VirtualMachineOp phase == virtv2.MachineDegraded || phase == virtv2.MachineStarting || phase == virtv2.MachinePause + case virtv2.VMOPTypeMigrate: + return phase == virtv2.MachineRunning default: return false } @@ -165,6 +183,8 @@ func (s VMOperationService) InProgressReasonForType(vmop *virtv2.VirtualMachineO return vmopcondition.ReasonStopInProgress case virtv2.VMOPTypeRestart: return vmopcondition.ReasonRestartInProgress + case virtv2.VMOPTypeMigrate: + return vmopcondition.ReasonMigrationInProgress } return vmopcondition.ReasonCompletedUnknown } @@ -188,21 +208,39 @@ func (s VMOperationService) IsComplete(ctx context.Context, vmop *virtv2.Virtual return false, err } - // Use vmop creation time or time from SignalSent condition. - signalSentTime := vmop.GetCreationTimestamp().Time - signalSendCond, found := GetCondition(vmopcondition.SignalSentType.String(), vmop.Status.Conditions) - if found && signalSendCond.LastTransitionTime.After(signalSentTime) { - signalSentTime = signalSendCond.LastTransitionTime.Time + return kvvmi != nil && vmPhase == virtv2.MachineRunning && + s.isAfterSignalSentOrCreation(kvvmi.GetCreationTimestamp().Time, vmop), nil + case virtv2.VMOPTypeMigrate: + kvvmi, err := s.getKVVMI(ctx, vmop.GetNamespace(), vmop.Spec.VirtualMachine) + if err != nil { + return false, err } - - return vmPhase == virtv2.MachineRunning && - kvvmi.GetCreationTimestamp().After(signalSentTime), nil + if kvvmi == nil { + return false, nil + } + if s.isAfterSignalSentOrCreation(kvvmi.GetCreationTimestamp().Time, vmop) { + return true, nil + } + migrationState := kvvmi.Status.MigrationState + return migrationState != nil && + migrationState.EndTimestamp != nil && + s.isAfterSignalSentOrCreation(migrationState.EndTimestamp.Time, vmop), nil default: return false, nil } } +func (s VMOperationService) isAfterSignalSentOrCreation(timestamp time.Time, vmop *virtv2.VirtualMachineOperation) bool { + // Use vmop creation time or time from SignalSent condition. + signalSentTime := vmop.GetCreationTimestamp().Time + signalSendCond, found := GetCondition(vmopcondition.SignalSentType.String(), vmop.Status.Conditions) + if found && signalSendCond.Status == metav1.ConditionTrue && signalSendCond.LastTransitionTime.After(signalSentTime) { + signalSentTime = signalSendCond.LastTransitionTime.Time + } + return timestamp.After(signalSentTime) +} + func (s VMOperationService) IsFinalState(vmop *virtv2.VirtualMachineOperation) bool { if vmop == nil { return false diff --git a/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go b/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go index a36fe75c3..5e7186580 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go +++ b/images/virtualization-artifact/pkg/controller/vmop/internal/operation.go @@ -90,7 +90,7 @@ func (h OperationHandler) Handle(ctx context.Context, s state.VMOperationState) // Send signal to perform operation, set phase to InProgress on success and to Fail on error. err := h.vmopSrv.Do(ctx, changed) if err != nil { - failMsg := fmt.Sprintf("Sending powerstate signal %q to VM", changed.Spec.Type) + failMsg := fmt.Sprintf("Sending signal %q to VM", changed.Spec.Type) log.Debug(failMsg, logger.SlogErr(err)) failMsg = fmt.Sprintf("%s: %v", failMsg, err) h.recorder.Event(changed, corev1.EventTypeWarning, virtv2.ReasonErrVMOPFailed, failMsg) @@ -111,7 +111,7 @@ func (h OperationHandler) Handle(ctx context.Context, s state.VMOperationState) return reconcile.Result{}, nil } - msg := fmt.Sprintf("Sent powerstate signal %q to VM without errors.", changed.Spec.Type) + msg := fmt.Sprintf("Sent signal %q to VM without errors.", changed.Spec.Type) log.Debug(msg) h.recorder.Event(changed, corev1.EventTypeNormal, virtv2.ReasonVMOPSucceeded, msg) diff --git a/images/virtualization-artifact/pkg/controller/vmop/vmop_controller.go b/images/virtualization-artifact/pkg/controller/vmop/vmop_controller.go index 5da7956a2..40b1a69ae 100644 --- a/images/virtualization-artifact/pkg/controller/vmop/vmop_controller.go +++ b/images/virtualization-artifact/pkg/controller/vmop/vmop_controller.go @@ -32,6 +32,7 @@ import ( "github.com/deckhouse/virtualization-controller/pkg/controller/vmop/internal" "github.com/deckhouse/virtualization-controller/pkg/logger" vmopcolelctor "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/vmop" + "github.com/deckhouse/virtualization/api/client/kubeclient" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -42,13 +43,14 @@ const ( func SetupController( ctx context.Context, mgr manager.Manager, + virtClient kubeclient.Client, lg *slog.Logger, ) error { log := lg.With(logger.SlogController(controllerName)) recorder := mgr.GetEventRecorderFor(controllerName) client := mgr.GetClient() - vmopSrv := service.NewVMOperationService(mgr.GetClient()) + vmopSrv := service.NewVMOperationService(mgr.GetClient(), virtClient) handlers := []Handler{ internal.NewLifecycleHandler(vmopSrv), diff --git a/templates/virtualization-controller/rbac-for-us.yaml b/templates/virtualization-controller/rbac-for-us.yaml index 987765767..3a1d679a8 100644 --- a/templates/virtualization-controller/rbac-for-us.yaml +++ b/templates/virtualization-controller/rbac-for-us.yaml @@ -157,6 +157,7 @@ rules: resources: - virtualmachines/freeze - virtualmachines/unfreeze + - virtualmachines/migrate verbs: - update - apiGroups: