Repository: ManageIQ/manageiq-providers-kubevirt
Pull request #284: VM Clone Cleanups
Author: login: agrare
Labels: name: refactoring
Created: 2025-03-06T17:24:30Z | Merged: 2025-03-07T17:41:10Z

Follow-up to https://github.com/ManageIQ/manageiq-providers-kubevirt/pull/276
<!--
1. Describe what this Pull Request does and why you think it is needed.
   If this PR includes UI or CLI changes, please include Before/After screenshots
   If this PR includes performance changes, please include Before/After metrics showing improvement.
-->

<!--
2. If this fixes an existing issue, please specify in `Fixes #<id>` format
   (As described in https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue)
-->

<!--
3. Ask @miq-bot to apply a scope label (bug, enhancement, etc) and any additional reviewers or assignees.
   (As described in https://github.com/ManageIQ/miq_bot#requested-tasks)
   e.g. `@miq-bot add-label label_name`
        `@miq-bot add-reviewer @name`
-->



Diff:
diff --git a/app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb b/app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
index df996dc..afc1106 100644
--- a/app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
+++ b/app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
@@ -80,58 +80,45 @@ def user_options(options)
   end
 
   def values(template, options)
-    default_params = {}
-    template.parameters.each do |param|
-      name = param[:name].downcase
+    template.parameters.each_with_object({}) do |param, default_params|
+      name  = param[:name].downcase
       value = options[name.to_sym]
-      if value && name == "memory"
-        value = "#{value}Mi"
-      end
+      value = "#{value}Mi" if value && name == "memory"
 
       default_params[name] = value || param[:value]
     end
-    default_params
   end
 
   def param_substitution!(object, params)
-    result = object
-    case result
+    case object
     when Hash
-      result.each do |k, v|
-        result[k] = param_substitution!(v, params)
-      end
+      object.transform_values! { |v| param_substitution!(v, params) }
     when Array
-      result.map { |v| param_substitution!(v, params) }
+      object.map! { |v| param_substitution!(v, params) }
     when String
-      result = sub_specific_object(params, object)
+      substitute_object_value!(object, params)
     end
-    result
   end
 
   #
-  # Performs substitution on specific object.
+  # Performs substitution on specific value.
   #
-  # @params params [Hash] Containing parameter names and values used for substitution.
   # @params object [String] Object on which substitution takes place.
+  # @params params [Hash] Containing parameter names and values used for substitution.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
-    result = object
+  def substitute_object_value!(object, params)
     params.each_key do |name|
-      token = "${#{name.upcase}}"
+      token    = "${#{name.upcase}}"
       os_token = "${{#{name.upcase}}}"
       next unless object.include?(token) || object.include?(os_token)
 
-      result = if params[name].kind_of?(String)
-                 if object.include?(os_token)
-                   object.sub!(os_token, params[name])
-                 else
-                   object.sub!(token, params[name])
-                 end
-               else
-                 params[name]
-               end
+      if params[name].kind_of?(String)
+        object.include?(os_token) ? object.sub!(os_token, params[name]) : object.sub!(token, params[name])
+      else
+        object = params[name]
+      end
     end
-    result
+    object
   end
 end
diff --git a/spec/models/manageiq/providers/kubevirt/infra_manager/provision/cloning_spec.rb b/spec/models/manageiq/providers/kubevirt/infra_manager/provision/cloning_spec.rb
index b8a04d4..eb23c15 100644
--- a/spec/models/manageiq/providers/kubevirt/infra_manager/provision/cloning_spec.rb
+++ b/spec/models/manageiq/providers/kubevirt/infra_manager/provision/cloning_spec.rb
@@ -22,6 +22,13 @@
         :parameters => parameters
       )
     end
+    let(:new_vm) do
+      require 'kubevirt'
+      Kubevirt::V1VirtualMachine.new(
+        :metadata => Kubevirt::K8sIoApimachineryPkgApisMetaV1ObjectMeta.new(:uid => SecureRandom.uuid),
+        :spec     => Kubevirt::V1VirtualMachineSpec.new(:template => Kubevirt::V1VirtualMachineInstanceTemplateSpec.new)
+      )
+    end
 
     before do
       allow(source).to receive(:provider_object).and_return(template)
@@ -32,8 +39,6 @@
     it "calls clone on template" do
       connection = double("Kubevirt::DefaultApi")
 
-      require 'kubevirt'
-      new_vm = Kubevirt::V1VirtualMachine.new(:metadata => Kubevirt::K8sIoApimachineryPkgApisMetaV1ObjectMeta.new(:uid => SecureRandom.uuid))
       allow(connection).to receive(:create_namespaced_virtual_machine).with(namespace, hash_including(:metadata => hash_including(:name => "test"))).and_return(new_vm, 200, {})
       allow(source).to     receive(:with_provider_connection).and_yield(connection)
 
@@ -42,6 +47,22 @@
       expect(subject.phase_context[:new_vm_ems_ref]).to eq(new_vm.metadata.uid)
     end
 
+    context "with hash parameters" do
+      let(:options)    { {:cores_per_socket => {:cores => 2, :sockets => 2, :threads => 4}, :vm_memory => ["1024", "1024"], :root_password => "1234"} }
+      let(:parameters) { [Kubeclient::Resource.new(:name => "NAME"), Kubeclient::Resource.new(:name => "CLOUD_USER_PASSWORD"), Kubeclient::Resource.new(:name => "CPU_CORES")] }
+      let(:vm_object)  { Kubeclient::Resource.new(:apiVersion => "kubevirt.io/v1", :kind => "VirtualMachine", :metadata => {:name => "${NAME}"}, :spec => {:domain => "${CPU_CORES}"}) }
+
+      it "replaces spec.domain.cpu with the hash" do
+        allow(connection)
+          .to receive(:create_namespaced_virtual_machine)
+          .with(namespace, hash_including(:spec => hash_including(:domain => {:cores => 2, :sockets => 2, :threads => 4})))
+          .and_return(new_vm, 200, {})
+        allow(source).to receive(:with_provider_connection).and_yield(connection)
+
+        subject.start_clone(:name => "test")
+      end
+    end
+
     context "with persistent volume claims" do
       let(:pvc_object) { Kubeclient::Resource.new(:kind => "PersistentVolumeClaim") }
       let(:objects)    { [vm_object, pvc_object] }
@@ -50,8 +71,6 @@
         connection = double("Kubevirt::DefaultApi")
         kubeclient = double("Kubeclient")
 
-        require 'kubevirt'
-        new_vm = Kubevirt::V1VirtualMachine.new(:metadata => Kubevirt::K8sIoApimachineryPkgApisMetaV1ObjectMeta.new(:uid => SecureRandom.uuid))
         allow(connection).to receive(:create_namespaced_virtual_machine).with(namespace, hash_including(:metadata => hash_including(:name => "test"))).and_return(new_vm, 200, {})
         allow(source).to     receive(:with_provider_connection).and_yield(connection)
         allow(subject).to    receive(:kubeclient).and_return(kubeclient)


Reviews:
user: login: Fryguy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T15:51:39Z
user: login: agrare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T15:53:12Z
user: login: agrare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T16:49:50Z
user: login: agrare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T16:52:17Z
user: login: agrare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T16:52:52Z
user: login: Fryguy
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T16:53:30Z
user: login: Fryguy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T16:54:29Z
user: login: agrare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T17:21:34Z
user: login: Fryguy
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T17:40:59Z

Review comments:
diff_hunk: @@ -115,7 +107,7 @@ def param_substitution!(object, params)
   # @params object [String] Object on which substitution takes place.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
+  def substitute_string_param(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: Fryguy
body: Can we name this with a `!` since it modifies the parameter?  Also, interestingly, it says `result = object`, then later does object.sub!, but takes the return value and puts it back in result.
created_at: 2025-03-07T15:51:39Z
author_association: MEMBER
diff_hunk: @@ -115,7 +107,7 @@ def param_substitution!(object, params)
   # @params object [String] Object on which substitution takes place.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
+  def substitute_string_param(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: agrare
body: Yeah I'm not sure this was ever working in the case where the param is not a string...working on a spec test for that case
created_at: 2025-03-07T15:53:12Z
author_association: MEMBER
diff_hunk: @@ -115,7 +107,7 @@ def param_substitution!(object, params)
   # @params object [String] Object on which substitution takes place.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
+  def substitute_string_param(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: agrare
body: Okay done, I feel better changing this method now that it has better spec coverage :laughing: 
created_at: 2025-03-07T16:49:50Z
author_association: MEMBER
diff_hunk: @@ -42,6 +47,22 @@
       expect(subject.phase_context[:new_vm_ems_ref]).to eq(new_vm.metadata.uid)
     end
 
+    context "with hash parameters" do
+      let(:options)    { {:cores_per_socket => {:cores => 2, :sockets => 2, :threads => 4}, :vm_memory => ["1024", "1024"], :root_password => "1234"} }
path: spec/models/manageiq/providers/kubevirt/infra_manager/provision/cloning_spec.rb
user: login: agrare
body: NOTE this `:cores_per_socket => {:cores => 2, :sockets => 2, :threads => 4}` and `:spec => hash_including(:domain => {:cores => 2, :sockets => 2, :threads => 4}` tests the non-string case of the `substitute_string_param!` method https://github.com/ManageIQ/manageiq-providers-kubevirt/pull/284/files#diff-4da493788b2d0b8f8e9969e0b7dd53c65c1c0dbe421837be10467e81a5849c13R119
created_at: 2025-03-07T16:52:17Z
author_association: MEMBER
diff_hunk: @@ -80,58 +80,45 @@ def user_options(options)
   end
 
   def values(template, options)
-    default_params = {}
-    template.parameters.each do |param|
-      name = param[:name].downcase
+    template.parameters.each_with_object({}) do |param, default_params|
+      name  = param[:name].downcase
       value = options[name.to_sym]
-      if value && name == "memory"
-        value = "#{value}Mi"
-      end
+      value = "#{value}Mi" if value && name == "memory"
 
       default_params[name] = value || param[:value]
     end
-    default_params
   end
 
   def param_substitution!(object, params)
-    result = object
-    case result
+    case object
     when Hash
-      result.each do |k, v|
-        result[k] = param_substitution!(v, params)
-      end
+      object.transform_values! { |v| param_substitution!(v, params) }
     when Array
-      result.map { |v| param_substitution!(v, params) }
+      object.map! { |v| param_substitution!(v, params) }
     when String
-      result = sub_specific_object(params, object)
+      substitute_string_param!(object, params)
     end
-    result
   end
 
   #
   # Performs substitution on specific object.
   #
-  # @params params [Hash] Containing parameter names and values used for substitution.
   # @params object [String] Object on which substitution takes place.
+  # @params params [Hash] Containing parameter names and values used for substitution.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
-    result = object
+  def substitute_string_param!(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: agrare
body: I don't think `substitute _string_ param!` is a good name though since this does more than just strings
And `substitute_param!` isn't good since the calling method is `param_substitution`
created_at: 2025-03-07T16:52:51Z
author_association: MEMBER
diff_hunk: @@ -80,58 +80,45 @@ def user_options(options)
   end
 
   def values(template, options)
-    default_params = {}
-    template.parameters.each do |param|
-      name = param[:name].downcase
+    template.parameters.each_with_object({}) do |param, default_params|
+      name  = param[:name].downcase
       value = options[name.to_sym]
-      if value && name == "memory"
-        value = "#{value}Mi"
-      end
+      value = "#{value}Mi" if value && name == "memory"
 
       default_params[name] = value || param[:value]
     end
-    default_params
   end
 
   def param_substitution!(object, params)
-    result = object
-    case result
+    case object
     when Hash
-      result.each do |k, v|
-        result[k] = param_substitution!(v, params)
-      end
+      object.transform_values! { |v| param_substitution!(v, params) }
     when Array
-      result.map { |v| param_substitution!(v, params) }
+      object.map! { |v| param_substitution!(v, params) }
     when String
-      result = sub_specific_object(params, object)
+      substitute_string_param!(object, params)
     end
-    result
   end
 
   #
   # Performs substitution on specific object.
   #
-  # @params params [Hash] Containing parameter names and values used for substitution.
   # @params object [String] Object on which substitution takes place.
+  # @params params [Hash] Containing parameter names and values used for substitution.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
-    result = object
+  def substitute_string_param!(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: Fryguy
body: ah ok - glad you dug in - from the original code it felt like it was only called in the string case in the case statement, so that's why I suggested string in the name.  Up to you what to call it.
created_at: 2025-03-07T16:54:29Z
author_association: MEMBER
diff_hunk: @@ -80,58 +80,45 @@ def user_options(options)
   end
 
   def values(template, options)
-    default_params = {}
-    template.parameters.each do |param|
-      name = param[:name].downcase
+    template.parameters.each_with_object({}) do |param, default_params|
+      name  = param[:name].downcase
       value = options[name.to_sym]
-      if value && name == "memory"
-        value = "#{value}Mi"
-      end
+      value = "#{value}Mi" if value && name == "memory"
 
       default_params[name] = value || param[:value]
     end
-    default_params
   end
 
   def param_substitution!(object, params)
-    result = object
-    case result
+    case object
     when Hash
-      result.each do |k, v|
-        result[k] = param_substitution!(v, params)
-      end
+      object.transform_values! { |v| param_substitution!(v, params) }
     when Array
-      result.map { |v| param_substitution!(v, params) }
+      object.map! { |v| param_substitution!(v, params) }
     when String
-      result = sub_specific_object(params, object)
+      substitute_string_param!(object, params)
     end
-    result
   end
 
   #
   # Performs substitution on specific object.
   #
-  # @params params [Hash] Containing parameter names and values used for substitution.
   # @params object [String] Object on which substitution takes place.
+  # @params params [Hash] Containing parameter names and values used for substitution.
   # @returns [String] The outcome of substitution.
   #
-  def sub_specific_object(params, object)
-    result = object
+  def substitute_string_param!(object, params)
path: app/models/manageiq/providers/kubevirt/infra_manager/provision/cloning.rb
user: login: agrare
body: Yeah the String case is when the value from the object that we're substituting is a string like: `:spec => {:domain => "${CPU}"}` it is the "${CPU}" case.  Then in substitute_string_param it is looking at what is being substituted.
created_at: 2025-03-07T17:21:34Z
author_association: MEMBER

PR comments:
user: login: Fryguy
created_at: 2025-03-07T16:54:56Z
body: I'm good to merge, unless you want to change that method name
author_association: MEMBER
user: login: agrare
created_at: 2025-03-07T17:26:29Z
body: Okay how about `substitute_object_value!` since it is replacing the value in the object :) naming is hard
author_association: MEMBER
user: login: miq-bot
created_at: 2025-03-07T17:30:45Z
body: <rubocop />Checked commits https://github.com/agrare/manageiq-providers-kubevirt/compare/ac5182f409431a63b66852f32939a263891529c8~...82cdfd0737a0e1576d0c4aaad9559d3a422a9856 with ruby 3.1.5, rubocop 1.56.3, haml-lint 0.59.0, and yamllint 
2 files checked, 0 offenses detected
Everything looks fine. :+1:

author_association: MEMBER