From 7824c2dad2ed9fe90b29c75de31794c22b3c641b Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Thu, 20 Jul 2023 15:59:55 -0700 Subject: [PATCH 01/11] Note when ssl helper was not found --- lib/vagrant.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 4fb6c5669e5..ad860e3931c 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -140,6 +140,8 @@ def << msg rescue => err global_logger.warn("unexpected failure loading ssl providers, attempting to continue (#{err})") end +else + global_logger.warn("vagrant ssl helper was not found, continuing...") end # We need these components always so instead of an autoload we From 2170136c37647b29a857e5f4317f0b6f024bfa0a Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 28 Jul 2023 10:24:59 -0700 Subject: [PATCH 02/11] Update test output to only show progress --- tasks/test.rake | 1 - test/unit/base.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks/test.rake b/tasks/test.rake index c47aac691b2..6fae3cd49aa 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -4,6 +4,5 @@ require 'rspec/core/rake_task' namespace :test do RSpec::Core::RakeTask.new(:unit) do |t| t.pattern = "test/unit/**/*_test.rb" - t.rspec_opts = "--color --format documentation" end end diff --git a/test/unit/base.rb b/test/unit/base.rb index 3a9e3984eb3..2f94851820e 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -41,7 +41,7 @@ # Configure RSpec RSpec.configure do |c| - #c.formatter = :progress + c.formatter = :progress c.color_mode = :on if Vagrant::Util::Platform.windows? From 5513173758a53d6810a3d9f99871d6cacf8ffc99 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 28 Jul 2023 10:25:29 -0700 Subject: [PATCH 03/11] Remove duplicate test constant --- .../virtualbox/driver/version_7_0_test.rb | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/unit/plugins/providers/virtualbox/driver/version_7_0_test.rb b/test/unit/plugins/providers/virtualbox/driver/version_7_0_test.rb index ea7d8a1f14c..5ecadbbe48d 100644 --- a/test/unit/plugins/providers/virtualbox/driver/version_7_0_test.rb +++ b/test/unit/plugins/providers/virtualbox/driver/version_7_0_test.rb @@ -810,24 +810,6 @@ UpperIP: 192.168.22.0 VBoxNetworkName: hostonly-vagrantnet-vbox2) -VBOX_HOSTONLYNETS=%(Name: vagrantnet-vbox1 -GUID: 10000000-0000-0000-0000-000000000000 - -State: Enabled -NetworkMask: 255.255.255.0 -LowerIP: 192.168.61.0 -UpperIP: 192.168.61.0 -VBoxNetworkName: hostonly-vagrantnet-vbox1 - -Name: vagrantnet-vbox2 -GUID: 20000000-0000-0000-0000-000000000000 - -State: Enabled -NetworkMask: 255.255.255.0 -LowerIP: 192.168.22.0 -UpperIP: 192.168.22.0 -VBoxNetworkName: hostonly-vagrantnet-vbox2) - VBOX_RANGE_HOSTONLYNETS=%(Name: vagrantnet-vbox1 GUID: 10000000-0000-0000-0000-000000000000 From 51adb125477b50e5acce6e6ad39e38d7b3cd7b3b Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 28 Jul 2023 10:40:02 -0700 Subject: [PATCH 04/11] Add architecture support for boxes Introduce support for handling box architecture. Adds a new `box_architecture` setting that defaults to `:auto` which will perform automatic detection of the host system, but can be overridden with a custom value. Can also be set to `nil` which will result in it fetching the box flagged with the default architecture within the metadata. Box collection has been modified to allow existing boxes already downloaded and unpacked to still function as expected when architecture information is not available. --- lib/vagrant/action/builtin/box_add.rb | 44 +++-- lib/vagrant/action/builtin/box_remove.rb | 226 +++++++++++++--------- lib/vagrant/action/builtin/handle_box.rb | 1 + lib/vagrant/box.rb | 20 +- lib/vagrant/box_collection.rb | 64 ++++-- lib/vagrant/box_metadata.rb | 138 ++++++++++--- lib/vagrant/errors.rb | 16 ++ lib/vagrant/machine.rb | 3 + lib/vagrant/machine_index.rb | 8 + lib/vagrant/util.rb | 2 +- lib/vagrant/util/platform.rb | 23 +++ lib/vagrant/vagrantfile.rb | 2 +- plugins/commands/box/command/add.rb | 9 +- plugins/commands/box/command/list.rb | 45 +++-- plugins/commands/box/command/remove.rb | 14 ++ plugins/commands/box/command/update.rb | 65 +++++-- plugins/commands/cloud/box/show.rb | 72 +++++-- plugins/commands/cloud/client/client.rb | 5 + plugins/commands/cloud/locales/en.yml | 35 +++- plugins/commands/cloud/provider/create.rb | 20 +- plugins/commands/cloud/provider/delete.rb | 54 ++++-- plugins/commands/cloud/provider/update.rb | 26 ++- plugins/commands/cloud/publish.rb | 46 +++-- plugins/commands/cloud/search.rb | 5 +- plugins/commands/cloud/util.rb | 80 ++++++-- plugins/kernel_v2/config/vm.rb | 7 + templates/locales/en.yml | 69 +++++-- vagrant.gemspec | 2 +- 28 files changed, 823 insertions(+), 278 deletions(-) diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index d74e5d29af2..bb73a5345ae 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -165,6 +165,7 @@ def add_direct(urls, env) env, checksum: env[:box_checksum], checksum_type: env[:box_checksum_type], + architecture: env[:architecture] ) end @@ -179,6 +180,9 @@ def add_direct(urls, env) # a Atlas server URL. def add_from_metadata(url, env, expanded) original_url = env[:box_url] + architecture = env[:box_architecture] + display_architecture = architecture == :auto ? + Util::Platform.architecture : architecture provider = env[:box_provider] provider = Array(provider) if provider version = env[:box_version] @@ -228,12 +232,17 @@ def add_from_metadata(url, env, expanded) end metadata_version = metadata.version( - version || ">= 0", provider: provider) + version || ">= 0", + provider: provider, + architecture: architecture, + ) if !metadata_version - if provider && !metadata.version(">= 0", provider: provider) + if provider && !metadata.version(">= 0", provider: provider, architecture: architecture) raise Errors::BoxAddNoMatchingProvider, name: metadata.name, - requested: provider, + requested: [provider, + display_architecture ? "(#{display_architecture})" : nil + ].compact.join(" "), url: display_url else raise Errors::BoxAddNoMatchingVersion, @@ -249,16 +258,16 @@ def add_from_metadata(url, env, expanded) # If a provider was specified, make sure we get that specific # version. provider.each do |p| - metadata_provider = metadata_version.provider(p) + metadata_provider = metadata_version.provider(p, architecture) break if metadata_provider end - elsif metadata_version.providers.length == 1 + elsif metadata_version.providers(architecture).length == 1 # If we have only one provider in the metadata, just use that # provider. metadata_provider = metadata_version.provider( - metadata_version.providers.first) + metadata_version.providers.first, architecture) else - providers = metadata_version.providers.sort + providers = metadata_version.providers(architecture).sort choice = 0 options = providers.map do |p| @@ -279,7 +288,7 @@ def add_from_metadata(url, env, expanded) end metadata_provider = metadata_version.provider( - providers[choice-1]) + providers[choice-1], architecture) end provider_url = metadata_provider.url @@ -302,6 +311,7 @@ def add_from_metadata(url, env, expanded) env, checksum: metadata_provider.checksum, checksum_type: metadata_provider.checksum_type, + architecture: architecture, ) end @@ -317,16 +327,21 @@ def add_from_metadata(url, env, expanded) # @param [Hash] env # @return [Box] def box_add(urls, name, version, provider, md_url, env, **opts) + display_architecture = opts[:architecture] == :auto ? + Util::Platform.architecture : opts[:architecture] env[:ui].output(I18n.t( "vagrant.box_add_with_version", name: name, version: version, - providers: Array(provider).join(", "))) + providers: [ + provider, + display_architecture ? "(#{display_architecture})" : nil + ].compact.join(" "))) # Verify the box we're adding doesn't already exist if provider && !env[:box_force] box = env[:box_collection].find( - name, provider, version) + name, provider, version, opts[:architecture]) if box raise Errors::BoxAlreadyExists, name: name, @@ -377,7 +392,9 @@ def box_add(urls, name, version, provider, md_url, env, **opts) box_url, name, version, force: env[:box_force], metadata_url: md_url, - providers: provider) + providers: provider, + architecture: env[:box_architecture] + ) ensure # Make sure we delete the temporary file after we add it, # unless we were interrupted, in which case we keep it around @@ -396,7 +413,10 @@ def box_add(urls, name, version, provider, md_url, env, **opts) "vagrant.box_added", name: box.name, version: box.version, - provider: box.provider)) + provider: [ + provider, + display_architecture ? "(#{display_architecture})" : nil + ].compact.join(" "))) # Store the added box in the env for future middleware env[:box_added] = box diff --git a/lib/vagrant/action/builtin/box_remove.rb b/lib/vagrant/action/builtin/box_remove.rb index fb834f25a7f..f103284076a 100644 --- a/lib/vagrant/action/builtin/box_remove.rb +++ b/lib/vagrant/action/builtin/box_remove.rb @@ -15,112 +15,162 @@ def initialize(app, env) def call(env) box_name = env[:box_name] - box_provider = env[:box_provider] - box_provider = box_provider.to_sym if box_provider + box_architecture = env[:box_architecture] if env[:box_architecture] + box_provider = env[:box_provider].to_sym if env[:box_provider] box_version = env[:box_version] box_remove_all_versions = env[:box_remove_all_versions] - - boxes = {} - env[:box_collection].all.each do |n, v, p| - boxes[n] ||= {} - boxes[n][p] ||= [] - boxes[n][p] << v + box_remove_all_providers = env[:box_remove_all_providers] + box_remove_all_architectures = env[:box_remove_all_architectures] + + box_info = Util::HashWithIndifferentAccess.new + env[:box_collection].all.each do |name, version, provider, architecture| + next if name != box_name + box_info[version] ||= Util::HashWithIndifferentAccess.new + box_info[version][provider] ||= [] + box_info[version][provider] << architecture end - all_box = boxes[box_name] - if !all_box + # If there's no box info, then the box doesn't exist here + if box_info.empty? raise Errors::BoxRemoveNotFound, name: box_name end - all_versions = nil - if !box_provider - if all_box.length == 1 - # There is only one provider, just use that. - all_versions = all_box.values.first - box_provider = all_box.keys.first - else - raise Errors::BoxRemoveMultiProvider, - name: box_name, - providers: all_box.keys.map(&:to_s).sort.join(", ") - end - else - all_versions = all_box[box_provider] - if !all_versions - raise Errors::BoxRemoveProviderNotFound, + # Filtering only matters if not removing all versions + if !box_remove_all_versions + # If no version was provided, and not removing all versions, + # only allow one version to proceed + if !box_version && box_info.size > 1 + raise Errors::BoxRemoveMultiVersion, name: box_name, - provider: box_provider.to_s, - providers: all_box.keys.map(&:to_s).sort.join(", ") + versions: box_info.keys.sort.map { |k| " * #{k}" }.join("\n") end - end - if !box_version - if all_versions.length == 1 - # There is only one version, just use that. - box_version = all_versions.first - elsif not box_remove_all_versions - # There are multiple versions, we can't choose. - raise Errors::BoxRemoveMultiVersion, - name: box_name, - provider: box_provider.to_s, - versions: all_versions.sort.map { |k| " * #{k}" }.join("\n") + # If a version was provided, make sure it exists + if box_version + if !box_info.keys.include?(box_version) + raise Errors::BoxRemoveVersionNotFound, + name: box_name, + version: box_version, + versions: box_info.keys.sort.map { |k| " * #{k}" }.join("\n") + else + box_info.delete_if { |k, _| k != box_version } + end end - elsif !all_versions.include?(box_version) - raise Errors::BoxRemoveVersionNotFound, - name: box_name, - provider: box_provider.to_s, - version: box_version, - versions: all_versions.sort.map { |k| " * #{k}" }.join("\n") - end - versions_to_remove = [box_version] - versions_to_remove = all_versions if box_remove_all_versions - - versions_to_remove.sort.each do |version_to_remove| - box = env[:box_collection].find( - box_name, box_provider, box_version) - - # Verify that this box is not in use by an active machine, - # otherwise warn the user. - users = box.in_use?(env[:machine_index]) || [] - users = users.find_all { |u| u.valid?(env[:home_path]) } - if !users.empty? - # Build up the output to show the user. - users = users.map do |entry| - "#{entry.name} (ID: #{entry.id})" - end.join("\n") - - force_key = :force_confirm_box_remove - message = I18n.t( - "vagrant.commands.box.remove_in_use_query", - name: box.name, - provider: box.provider, - version: box.version, - users: users) + " " - - # Ask the user if we should do this - stack = Builder.new.tap do |b| - b.use Confirm, message, force_key + # Only a single version remains + box_version = box_info.keys.first + + # Further filtering only matters if not removing all providers + if !box_remove_all_providers + # If no provider was given, check if there are more + # than a single provider for the version + if !box_provider && box_info.values.first.size > 1 + raise Errors::BoxRemoveMultiProvider, + name: box_name, + version: box_version, + providers: box_info.values.first.keys.map(&:to_s).sort.join(", ") end - # Keep used boxes, even if "force" is applied - keep_used_boxes = env[:keep_used_boxes] + # If a provider was given, check the version has it + if box_provider + if !box_info.values.first.key?(box_provider) + raise Errors::BoxRemoveProviderNotFound, + name: box_name, + version: box_version, + provider: box_provider.to_s, + providers: box_info.values.first.keys.map(&:to_s).sort.join(", ") + else + box_info.values.first.delete_if { |k, _| k.to_s != box_provider.to_s } + end + end - result = env[:action_runner].run(stack, env) - if !result[:result] || keep_used_boxes - # They said "no", so continue with the next box - next + # Only a single provider remains + box_provider = box_info.values.first.keys.first + + # Further filtering only matters if not removing all architectures + if !box_remove_all_architectures + # If no architecture was given, check if there are more + # than a single architecture for the provider in version + if !box_architecture && box_info.values.first.values.first.size > 1 + raise Errors::BoxRemoveMultiArchitecture, + name: box_name, + version: box_version, + provider: box_provider.to_s, + architectures: box_info.values.first.values.first.sort.join(", ") + end + + # If architecture was given, check the provider for the version has it + if box_architecture + if !box_info.values.first.values.first.include?(box_architecture) + raise Errors::BoxRemoveArchitectureNotFound, + name: box_name, + version: box_version, + provider: box_provider.to_s, + architecture: box_architecture, + architectures: box_info.values.first.values.first.sort.join(", ") + else + box_info.values.first.values.first.delete_if { |v| v != box_architecture } + end + end end end + end - env[:ui].info(I18n.t("vagrant.commands.box.removing", - name: box.name, - provider: box.provider, - version: box.version)) - box.destroy! - env[:box_collection].clean(box.name) - - # Passes on the removed box to the rest of the middleware chain - env[:box_removed] = box + box_info.each do |version, provider_info| + provider_info.each do |provider, architecture_info| + provider = provider.to_sym + architecture_info.each do |architecture| + box = env[:box_collection].find( + box_name, provider, version, architecture + ) + + # Verify that this box is not in use by an active machine, + # otherwise warn the user. + users = box.in_use?(env[:machine_index]) || [] + users = users.find_all { |u| u.valid?(env[:home_path]) } + if !users.empty? + # Build up the output to show the user. + users = users.map do |entry| + "#{entry.name} (ID: #{entry.id})" + end.join("\n") + + force_key = :force_confirm_box_remove + message = I18n.t( + "vagrant.commands.box.remove_in_use_query", + name: box.name, + architecture: box.architecture, + provider: box.provider, + version: box.version, + users: users) + " " + + # Ask the user if we should do this + stack = Builder.new.tap do |b| + b.use Confirm, message, force_key + end + + # Keep used boxes, even if "force" is applied + keep_used_boxes = env[:keep_used_boxes] + + result = env[:action_runner].run(stack, env) + if !result[:result] || keep_used_boxes + # They said "no", so continue with the next box + next + end + end + + env[:ui].info(I18n.t("vagrant.commands.box.removing", + name: box.name, + architecture: box.architecture, + provider: box.provider, + version: box.version)) + + box.destroy! + env[:box_collection].clean(box.name) + + # Passes on the removed box to the rest of the middleware chain + env[:box_removed] = box + end + end end @app.call(env) diff --git a/lib/vagrant/action/builtin/handle_box.rb b/lib/vagrant/action/builtin/handle_box.rb index d8964f387d8..c2864098186 100644 --- a/lib/vagrant/action/builtin/handle_box.rb +++ b/lib/vagrant/action/builtin/handle_box.rb @@ -87,6 +87,7 @@ def handle_box(env) env[:action_runner].run(Vagrant::Action.action_box_add, env.merge({ box_name: machine.config.vm.box, box_url: machine.config.vm.box_url || machine.config.vm.box, + box_architecture: machine.config.vm.box_architecture, box_server_url: machine.config.vm.box_server_url, box_provider: box_formats, box_version: machine.config.vm.box_version, diff --git a/lib/vagrant/box.rb b/lib/vagrant/box.rb index c5bcc5d25f2..3f636a9e465 100644 --- a/lib/vagrant/box.rb +++ b/lib/vagrant/box.rb @@ -37,6 +37,11 @@ class Box # @return [Symbol] attr_reader :provider + # This is the architecture that this box is build for. + # + # @return [String] + attr_reader :architecture + # The version of this box. # # @return [String] @@ -65,13 +70,15 @@ class Box # @param [Symbol] provider The provider that this box implements. # @param [Pathname] directory The directory where this box exists on # disk. + # @param [String] architecture Architecture the box was built for # @param [String] metadata_url Metadata URL for box # @param [Hook] hook A hook to apply to the box downloader, for example, for authentication - def initialize(name, provider, version, directory, metadata_url: nil, hook: nil) + def initialize(name, provider, version, directory, architecture: nil, metadata_url: nil, hook: nil) @name = name @version = version @provider = provider @directory = directory + @architecture = architecture @metadata_url = metadata_url @hook = hook @@ -130,6 +137,7 @@ def in_use?(index) # If all the data matches, record it if box_data["name"] == self.name && box_data["provider"] == self.provider.to_s && + box_data["architecture"] == self.architecture && box_data["version"] == self.version.to_s results << entry end @@ -194,10 +202,10 @@ def has_update?(version=nil, download_options: {}) version ||= "" version += "> #{@version}" md = self.load_metadata(download_options) - newer = md.version(version, provider: @provider) + newer = md.version(version, provider: @provider, architecture: @architecture) return nil if !newer - [md, newer, newer.provider(@provider)] + [md, newer, newer.provider(@provider, @architecture)] end # Check if a box update check is allowed. Uses a file @@ -241,13 +249,13 @@ def repackage(path) end # Implemented for comparison with other boxes. Comparison is - # implemented by comparing names and providers. + # implemented by comparing names, providers, and architectures. def <=>(other) return super if !other.is_a?(self.class) # Comparison is done by composing the name and provider - "#{@name}-#{@version}-#{@provider}" <=> - "#{other.name}-#{other.version}-#{other.provider}" + "#{@name}-#{@version}-#{@provider}-#{@architecture}" <=> + "#{other.name}-#{other.version}-#{other.provider}-#{other.architecture}" end end end diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index 2e09bc1c12f..56c5de2c0f9 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -28,7 +28,7 @@ class BoxCollection # expects in order to easily manage and modify boxes. The folder structure # is the following: # - # COLLECTION_ROOT/BOX_NAME/PROVIDER/metadata.json + # COLLECTION_ROOT/BOX_NAME/PROVIDER/[ARCHITECTURE]/metadata.json # # Where: # @@ -38,6 +38,8 @@ class BoxCollection # the user of Vagrant. # * PROVIDER - The provider that the box was built for (VirtualBox, # VMware, etc.). + # * ARCHITECTURE - Optional. The architecture that the box was built + # for (amd64, arm64, 386, etc.). # * metadata.json - A simple JSON file that at the bare minimum # contains a "provider" key that matches the provider for the # box. This metadata JSON, however, can contain anything. @@ -80,14 +82,15 @@ def initialize(directory, options=nil) # @param [Boolean] force If true, any existing box with the same name # and provider will be replaced. def add(path, name, version, **opts) + architecture = opts[:architecture] providers = opts[:providers] providers = Array(providers) if providers provider = nil # A helper to check if a box exists. We store this in a variable # since we call it multiple times. - check_box_exists = lambda do |box_formats| - box = find(name, box_formats, version) + check_box_exists = lambda do |box_formats, box_architecture| + box = find(name, box_formats, version, box_architecture) next if !box if !opts[:force] @@ -108,12 +111,12 @@ def add(path, name, version, **opts) with_collection_lock do log_provider = providers ? providers.join(", ") : "any provider" - @logger.debug("Adding box: #{name} (#{log_provider}) from #{path}") + @logger.debug("Adding box: #{name} (#{log_provider} - #{architecture.inspect}) from #{path}") # Verify the box doesn't exist early if we're given a provider. This # can potentially speed things up considerably since we don't need # to unpack any files. - check_box_exists.call(providers) if providers + check_box_exists.call(providers, architecture) if providers # Create a temporary directory since we're not sure at this point if # the box we're unpackaging already exists (if no provider was given) @@ -154,7 +157,7 @@ def add(path, name, version, **opts) end else # Verify the box doesn't already exist - check_box_exists.call([box_provider]) + check_box_exists.call([box_provider], architecture) end # We weren't given a provider, so store this one. @@ -167,7 +170,16 @@ def add(path, name, version, **opts) @logger.debug("Box directory: #{box_dir}") # This is the final directory we'll move it to - final_dir = box_dir.join(provider.to_s) + provider_dir = box_dir.join(provider.to_s) + final_dir = provider_dir + @logger.debug("Provider directory: #{provider_dir}") + # If architecture is set, unpack into architecture specific directory + if architecture + arch = architecture + arch = Util::Platform.architecture if architecture == :auto + final_dir = provider_dir.join(arch) + end + if final_dir.exist? @logger.debug("Removing existing provider directory...") final_dir.rmtree @@ -209,13 +221,13 @@ def add(path, name, version, **opts) end # Return the box - find(name, provider, version) + find(name, provider, version, architecture) end # This returns an array of all the boxes on the system, given by # their name and their provider. # - # @return [Array] Array of `[name, version, provider]` of the boxes + # @return [Array] Array of `[name, version, provider, architecture]` of the boxes # installed on this system. def all results = [] @@ -252,7 +264,16 @@ def all if provider.directory? && provider.join("metadata.json").file? provider_name = provider.basename.to_s.to_sym @logger.debug("Box: #{box_name} (#{provider_name}, #{version})") - results << [box_name, version, provider_name] + results << [box_name, version, provider_name, nil] + elsif provider.directory? + provider.children(true).each do |architecture| + provider_name = provider.basename.to_s.to_sym + if architecture.directory? && architecture.join("metadata.json").file? + architecture_name = architecture.basename.to_s.to_sym + @logger.debug("Box: #{box_name} (#{provider_name} (#{architecture_name}), #{version})") + results << [box_name, version, provider_name, architecture_name] + end + end else @logger.debug("Invalid box #{box_name}, ignoring: #{provider}") end @@ -262,7 +283,7 @@ def all end # Sort the list to group like providers and properly ordered versions results.sort_by! do |box_result| - [box_result[0], box_result[2], Gem::Version.new(box_result[1])] + [box_result[0], box_result[2], Gem::Version.new(box_result[1]), box_result[3]] end results end @@ -274,8 +295,10 @@ def all # @param [String] version Version constraints to adhere to. Example: # "~> 1.0" or "= 1.0, ~> 1.1" # @return [Box] The box found, or `nil` if not found. - def find(name, providers, version) + def find(name, providers, version, box_architecture=:auto) providers = Array(providers) + architecture = box_architecture + architecture = Util::Platform.architecture if architecture == :auto # Build up the requirements we have requirements = version.to_s.split(",").map do |v| @@ -321,6 +344,19 @@ def find(name, providers, version) providers.each do |provider| provider_dir = versiondir.join(provider.to_s) next if !provider_dir.directory? + # If architecture is defined then the box should be within + # a subdirectory. However, if box_architecture value is :auto + # and the box provider directory exists but the architecture + # directory does not, we will use the box provider directory. This + # allows Vagrant to work correctly with boxes which were added + # prior to the addition of architecture support + final_dir = provider_dir + if architecture + arch_dir = provider_dir.join(architecture.to_s) + next if !arch_dir.directory? && box_architecture != :auto + end + final_dir = arch_dir if arch_dir && arch_dir.directory? + @logger.info("Box found: #{name} (#{provider})") metadata_url = nil @@ -334,8 +370,8 @@ def find(name, providers, version) end return Box.new( - name, provider, version_dir_map[v.to_s], provider_dir, - metadata_url: metadata_url, hook: @hook + name, provider, version_dir_map[v.to_s], final_dir, + architecture: architecture, metadata_url: metadata_url, hook: @hook ) end end diff --git a/lib/vagrant/box_metadata.rb b/lib/vagrant/box_metadata.rb index fc4a86a165a..b66580cb7d7 100644 --- a/lib/vagrant/box_metadata.rb +++ b/lib/vagrant/box_metadata.rb @@ -38,7 +38,7 @@ def initialize(io, **_) @description = @raw["description"] @version_map = (@raw["versions"] || []).map do |v| begin - [Gem::Version.new(v["version"]), v] + [Gem::Version.new(v["version"]), Version.new(v)] rescue ArgumentError raise Errors::BoxMetadataMalformedVersion, version: v["version"].to_s @@ -61,11 +61,27 @@ def version(version, **opts) providers = nil providers = Array(opts[:provider]).map(&:to_sym) if opts[:provider] + # NOTE: The :auto value is not expanded here since no architecture + # value comparisons are being done within this method + architecture = opts.fetch(:architecture, :auto) @version_map.keys.sort.reverse.each do |v| next if !requirements.all? { |r| r.satisfied_by?(v) } - version = Version.new(@version_map[v]) - next if (providers & version.providers).empty? if providers + version = @version_map[v] + valid_providers = version.providers + + # If filtering by provider(s), apply filter + valid_providers &= providers if providers + + # Skip if no valid providers are found + next if valid_providers.empty? + + # Skip if no valid provider includes support + # the desired architecture + next if architecture && valid_providers.none? { |p| + version.provider(p, architecture) + } + return version end @@ -79,20 +95,25 @@ def version(version, **opts) # # @return[Array] def versions(**opts) - provider = nil + architecture = opts[:architecture] provider = opts[:provider].to_sym if opts[:provider] - if provider - @version_map.select do |version, raw| - if raw["providers"] - raw["providers"].detect do |p| - p["name"].to_sym == provider - end - end - end.keys.sort.map(&:to_s) - else - @version_map.keys.sort.map(&:to_s) + # Return full version list if no filters provided + if provider.nil? && architecture.nil? + return @version_map.keys.sort.map(&:to_s) + end + + # If a specific provider is not provided, filter + # only on architecture + if provider.nil? + return @version_map.select { |_, version| + !version.providers(architecture).empty? + }.keys.sort.map(&:to_s) end + + @version_map.select { |_, version| + version.provider(provider, architecture) + }.keys.sort.map(&:to_s) end # Represents a single version within the metadata. @@ -106,26 +127,81 @@ def initialize(raw=nil, **_) return if !raw @version = raw["version"] - @provider_map = (raw["providers"] || []).map do |p| - [p["name"].to_sym, p] + @providers = raw.fetch("providers", []).map do |data| + Provider.new(data) end - @provider_map = Hash[@provider_map] + @provider_map = @providers.group_by(&:name) + @provider_map = Util::HashWithIndifferentAccess.new(@provider_map) end # Returns a [Provider] for the given name, or nil if it isn't # supported by this version. - def provider(name) - p = @provider_map[name.to_sym] - return nil if !p - Provider.new(p) + def provider(name, architecture=nil) + name = name.to_sym + arch_name = architecture + arch_name = Util::Platform.architecture if arch_name == :auto + arch_name = arch_name.to_s if arch_name + + # If the provider doesn't exist in the map, return immediately + return if !@provider_map.key?(name) + + # If the arch_name value is set, filter based + # on architecture and return match if found. If + # no match is found and architecture wasn't automatically + # detected, return nil as an explicit match is + # being requested + if arch_name + match = @provider_map[name].detect do |p| + p.architecture == arch_name + end + + return match if match || architecture != :auto + end + + # If the passed architecture value was :auto and no explicit + # match for the architecture was found, check for a provider + # that is flagged as the default architecture, and has an + # architecture value of "unknown" + # + # NOTE: This preserves expected behavior with legacy boxes + if architecture == :auto + match = @provider_map[name].detect do |p| + p.architecture == "unknown" && + p.default_architecture + end + + return match if match + end + + # If the architecture value is set to nil, then just return + # whatever is defined as the default architecture + if architecture.nil? + match = @provider_map[name].detect(&:default_architecture) + + return match if match + end + + # The metadata consumed may not include architecture information, + # in which case the match would just be the single provider + # defined within the provider map for the name + if @provider_map[name].size == 1 && !@provider_map[name].first.architecture_support? + return @provider_map[name].first + end + + # Otherwise, there is no match + nil end # Returns the providers that are available for this version # of the box. # # @return [Array] - def providers - @provider_map.keys.map(&:to_sym) + def providers(architecture=nil) + return @provider_map.keys.map(&:to_sym) if architecture.nil? + + @provider_map.keys.find_all { |k| + provider(k, architecture) + }.map(&:to_sym) end end @@ -152,11 +228,27 @@ class Provider # @return [String] attr_accessor :checksum_type + # The architecture of the box + # + # @return [String] + attr_accessor :architecture + + # Marked as the default architecture + # + # @return [Boolean, NilClass] + attr_accessor :default_architecture + def initialize(raw, **_) @name = raw["name"] @url = raw["url"] @checksum = raw["checksum"] @checksum_type = raw["checksum_type"] + @architecture = raw["architecture"] + @default_architecture = raw["default_architecture"] + end + + def architecture_support? + !@default_architecture.nil? end end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 04354be67ff..d0df2f08f62 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -199,6 +199,10 @@ class BoxNotFoundWithProvider < VagrantError error_key(:box_not_found_with_provider) end + class BoxNotFoundWithProviderArchitecture < VagrantError + error_key(:box_not_found_with_provider_architecture) + end + class BoxNotFoundWithProviderAndVersion < VagrantError error_key(:box_not_found_with_provider_and_version) end @@ -211,6 +215,10 @@ class BoxRemoveNotFound < VagrantError error_key(:box_remove_not_found) end + class BoxRemoveArchitectureNotFound < VagrantError + error_key(:box_remove_architecture_not_found) + end + class BoxRemoveProviderNotFound < VagrantError error_key(:box_remove_provider_not_found) end @@ -219,6 +227,10 @@ class BoxRemoveVersionNotFound < VagrantError error_key(:box_remove_version_not_found) end + class BoxRemoveMultiArchitecture < VagrantError + error_key(:box_remove_multi_architecture) + end + class BoxRemoveMultiProvider < VagrantError error_key(:box_remove_multi_provider) end @@ -239,6 +251,10 @@ class BoxUpdateMultiProvider < VagrantError error_key(:box_update_multi_provider) end + class BoxUpdateMultiArchitecture < VagrantError + error_key(:box_update_multi_architecture) + end + class BoxUpdateNoMetadata < VagrantError error_key(:box_update_no_metadata) end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 163638fe088..7da6bb36593 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -326,6 +326,7 @@ def id=(value) entry.local_data_path = @env.local_data_path entry.name = @name.to_s entry.provider = @provider_name.to_s + entry.architecture = @architecture entry.state = "preparing" entry.vagrantfile_path = @env.root_path entry.vagrantfile_name = @env.vagrantfile_name @@ -334,6 +335,7 @@ def id=(value) entry.extra_data["box"] = { "name" => @box.name, "provider" => @box.provider.to_s, + "architecture" => @box.architecture, "version" => @box.version.to_s, } end @@ -587,6 +589,7 @@ def recover_machine(state) entry.extra_data["box"] = { "name" => @box.name, "provider" => @box.provider.to_s, + "architecture" => @box.architecture, "version" => @box.version.to_s, } end diff --git a/lib/vagrant/machine_index.rb b/lib/vagrant/machine_index.rb index c07919d0885..188667aaa11 100644 --- a/lib/vagrant/machine_index.rb +++ b/lib/vagrant/machine_index.rb @@ -30,6 +30,7 @@ module Vagrant # "uuid": { # "name": "foo", # "provider": "vmware_fusion", + # "architecture": "amd64", # "data_path": "/path/to/data/dir", # "vagrantfile_path": "/path/to/Vagrantfile", # "state": "running", @@ -381,6 +382,11 @@ class Entry # @return [String] attr_accessor :provider + # The name of the architecture. + # + # @return [String] + attr_accessor :architecture + # The last known state of this machine. # # @return [String] @@ -427,6 +433,7 @@ def initialize(id=nil, raw=nil) @local_data_path = raw["local_data_path"] @name = raw["name"] @provider = raw["provider"] + @architecture = raw["architecture"] @state = raw["state"] @full_state = raw["full_state"] @vagrantfile_name = raw["vagrantfile_name"] @@ -510,6 +517,7 @@ def to_json_struct "local_data_path" => @local_data_path.to_s, "name" => @name, "provider" => @provider, + "architecture" => @architecture, "state" => @state, "vagrantfile_name" => @vagrantfile_name, "vagrantfile_path" => @vagrantfile_path.to_s, diff --git a/lib/vagrant/util.rb b/lib/vagrant/util.rb index af978a0988b..1f799af3b69 100644 --- a/lib/vagrant/util.rb +++ b/lib/vagrant/util.rb @@ -46,7 +46,7 @@ module Remote autoload :Numeric, 'vagrant/util/numeric' autoload :Platform, 'vagrant/util/platform' autoload :Powershell, 'vagrant/util/powershell' - autoload :Presence, 'vagrant/util/presence' + autoload :Presence, 'vagrant/util/presence' autoload :Retryable, 'vagrant/util/retryable' autoload :SafeChdir, 'vagrant/util/safe_chdir' autoload :SafeEnv, 'vagrant/util/safe_env' diff --git a/lib/vagrant/util/platform.rb b/lib/vagrant/util/platform.rb index 0adf8a81720..03f25db55d8 100644 --- a/lib/vagrant/util/platform.rb +++ b/lib/vagrant/util/platform.rb @@ -17,6 +17,29 @@ module Util class Platform class << self + # Detect architecture of host system + # + # @return [String] + def architecture + if !defined?(@_host_architecture) + if ENV["VAGRANT_HOST_ARCHITECTURE"].to_s != "" + return @_host_architecture = ENV["VAGRANT_HOST_ARCHITECTURE"] + end + + @_host_architecture = case RbConfig::CONFIG["target_cpu"] + when "x86_64" + "amd64" + when "i386" + "386" + when "arm64", "aarch64" + "arm64" + else + RbConfig::CONFIG["target_cpu"] + end + end + @_host_architecture + end + def logger if !defined?(@_logger) @_logger = Log4r::Logger.new("vagrant::util::platform") diff --git a/lib/vagrant/vagrantfile.rb b/lib/vagrant/vagrantfile.rb index 69d861ab142..d663653c6eb 100644 --- a/lib/vagrant/vagrantfile.rb +++ b/lib/vagrant/vagrantfile.rb @@ -202,7 +202,7 @@ def machine_config(name, provider, boxes, data_path=nil, validate_provider=true) # Load the box Vagrantfile, if there is one if !config.vm.box.to_s.empty? && boxes - box = boxes.find(config.vm.box, box_formats, config.vm.box_version) + box = boxes.find(config.vm.box, box_formats, config.vm.box_version, config.vm.box_architecture) if box box_vagrantfile = find_vagrantfile(box.directory) if box_vagrantfile && !config.vm.ignore_box_vagrantfile diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index d3d2ab8bff4..9d33b27c6a3 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -12,7 +12,9 @@ class Add < Vagrant.plugin("2", :command) include DownloadMixins def execute - options = {} + options = { + architecture: :auto, + } opts = OptionParser.new do |o| o.banner = "Usage: vagrant box add [options] " @@ -34,6 +36,10 @@ def execute options[:location_trusted] = l end + o.on("-a", "--architecture ARCH", String, "Architecture the box should satisfy") do |a| + options[:architecture] = a + end + o.on("--provider PROVIDER", String, "Provider the box should satisfy") do |p| options[:provider] = p end @@ -82,6 +88,7 @@ def execute box_url: url, box_name: options[:name], box_provider: options[:provider], + box_architecture: options[:architecture], box_version: options[:version], box_checksum_type: options[:checksum_type], box_checksum: options[:checksum], diff --git a/plugins/commands/box/command/list.rb b/plugins/commands/box/command/list.rb index d80698579c4..d55c138b643 100644 --- a/plugins/commands/box/command/list.rb +++ b/plugins/commands/box/command/list.rb @@ -44,26 +44,33 @@ def list_boxes(boxes, extra_info) longest_box = boxes.max_by { |x| x[0].length } longest_box_length = longest_box[0].length - # Go through each box and output the information about it. We - # ignore the "v1" param for now since I'm not yet sure if its - # important for the user to know what boxes need to be upgraded - # and which don't, since we plan on doing that transparently. - boxes.each do |name, version, provider| - @env.ui.info("#{name.ljust(longest_box_length)} (#{provider}, #{version})") - - @env.ui.machine("box-name", name) - @env.ui.machine("box-provider", provider) - @env.ui.machine("box-version", version) - - info_file = @env.boxes.find(name, provider, version). - directory.join("info.json") - if info_file.file? - info = JSON.parse(info_file.read) - info.each do |k, v| - @env.ui.machine("box-info", k, v) + # Group boxes by name and version and start iterating + boxes.group_by { |b| [b[0], b[1]] }.each do |box_info, box_data| + name, version = box_info + # Now group by provider so we can collect common architectures + box_data.group_by { |b| b[2] }.each do |provider, data| + architectures = data.map { |d| d.last }.compact.sort.uniq + meta_info = [provider, version] + if !architectures.empty? + meta_info << "(#{architectures.join(", ")})" + end + @env.ui.info("#{name.ljust(longest_box_length)} (#{meta_info.join(", ")})") + data.each do |arch_info| + @env.ui.machine("box-name", name) + @env.ui.machine("box-provider", provider) + @env.ui.machine("box-version", version) + @env.ui.machine("box-architecture", arch_info.last || "n/a") + info_file = @env.boxes.find(name, provider, version, arch_info.last). + directory.join("info.json") + if info_file.file? + info = JSON.parse(info_file.read) + info.each do |k, v| + @env.ui.machine("box-info", k, v) - if extra_info - @env.ui.info(" - #{k}: #{v}", prefix: false) + if extra_info + @env.ui.info(" - #{k}: #{v}", prefix: false) + end + end end end end diff --git a/plugins/commands/box/command/remove.rb b/plugins/commands/box/command/remove.rb index cf320b03b77..9a8b88aed61 100644 --- a/plugins/commands/box/command/remove.rb +++ b/plugins/commands/box/command/remove.rb @@ -21,6 +21,9 @@ def execute options[:force] = f end + o.on("-a", "--architecture ARCH", String, "The specific architecture for the box to remove") do |a| + options[:architecture] = a + end o.on("--provider PROVIDER", String, "The specific provider type for the box to remove") do |p| options[:provider] = p @@ -34,6 +37,14 @@ def execute o.on("--all", "Remove all available versions of the box") do |a| options[:all] = a end + + o.on("--all-providers", "Remove all providers within a version of the box") do |a| + options[:all_providers] = a + end + + o.on("--all-architectures", "Remove all architectures within a provider a version of the box") do |a| + options[:all_architectures] = a + end end # Parse the options @@ -54,10 +65,13 @@ def execute @env.action_runner.run(Vagrant::Action.action_box_remove, { box_name: argv[0], + box_architecture: options[:architecture], box_provider: options[:provider], box_version: options[:version], force_confirm_box_remove: options[:force], box_remove_all_versions: options[:all], + box_remove_all_providers: options[:all_providers], + box_remove_all_architectures: options[:all_architectures] }) # Success, exit status 0 diff --git a/plugins/commands/box/command/update.rb b/plugins/commands/box/command/update.rb index 6a569bde8ca..34312cc5133 100644 --- a/plugins/commands/box/command/update.rb +++ b/plugins/commands/box/command/update.rb @@ -32,6 +32,10 @@ def execute options[:box] = b end + o.on("--architecture ARCHITECTURE", String, "Update box with specific architecture") do |a| + options[:architecture] = a + end + o.on("--provider PROVIDER", String, "Update box with specific provider") do |p| options[:provider] = p.to_sym end @@ -47,7 +51,7 @@ def execute return if !argv if options[:box] - update_specific(options[:box], options[:provider], download_options, options[:force]) + update_specific(options[:box], options[:provider], options[:architecture], download_options, options[:force]) else update_vms(argv, options[:provider], download_options, options[:force]) end @@ -55,41 +59,62 @@ def execute 0 end - def update_specific(name, provider, download_options, force) - boxes = {} - @env.boxes.all.each do |n, v, p| - boxes[n] ||= {} - boxes[n][p] ||= [] - boxes[n][p] << v + def update_specific(name, provider, architecture, download_options, force) + box_info = Vagrant::Util::HashWithIndifferentAccess.new + @env.boxes.all.each do |box_name, box_version, box_provider, box_architecture| + next if name != box_name + box_info[box_provider] ||= Vagrant::Util::HashWithIndifferentAccess.new + box_info[box_provider][box_version] ||= [] + box_info[box_provider][box_version].push(box_architecture.to_s).uniq! end - if !boxes[name] + if box_info.empty? raise Vagrant::Errors::BoxNotFound, name: name.to_s end if !provider - if boxes[name].length > 1 + if box_info.size > 1 raise Vagrant::Errors::BoxUpdateMultiProvider, name: name.to_s, - providers: boxes[name].keys.map(&:to_s).sort.join(", ") + providers: box_info.keys.map(&:to_s).sort.join(", ") end - provider = boxes[name].keys.first - elsif !boxes[name][provider] + provider = box_info.keys.first + elsif !box_info[provider] raise Vagrant::Errors::BoxNotFoundWithProvider, name: name.to_s, provider: provider.to_s, - providers: boxes[name].keys.map(&:to_s).sort.join(", ") + providers: box_info.keys.map(&:to_s).sort.join(", ") end - to_update = [ - [name, provider, boxes[name][provider].sort_by{|n| Gem::Version.new(n)}.last], - ] + version = box_info[provider].keys.sort_by{ |v| Gem::Version.new(v) }.last + architecture_list = box_info[provider][version] + + if !architecture + if architecture_list.size > 1 + raise Vagrant::Errors::BoxUpdateMultiArchitecture, + name: name.to_s, + provider: provider.to_s, + version: version.to_s, + architectures: architecture_list.sort.join(", ") + end - to_update.each do |n, p, v| - box = @env.boxes.find(n, p, v) - box_update(box, "> #{v}", @env.ui, download_options, force) + architecture = architecture_list.first + elsif !architecture_list.include?(architecture) + raise Vagrant::Errors::BoxNotFoundWithProviderArchitecture, + name: name.to_s, + provider: provider.to_s, + version: version.to_s, + architecture: architecture, + architectures: architecture_list.sort.join(", ") end + + # Architecture gets cast to a string when collecting information + # above. Convert it back to a nil if it's empty + architecture = nil if architecture.to_s.empty? + + box = @env.boxes.find(name, provider, version, architecture) + box_update(box, "> #{version}", @env.ui, download_options, force) end def update_vms(argv, provider, download_options, force) @@ -146,6 +171,7 @@ def box_update(box, version, ui, download_options, force) ui.detail("Latest installed version: #{box.version}") ui.detail("Version constraints: #{version}") ui.detail("Provider: #{box.provider}") + ui.detail("Architecture: #{box.architecture.inspect}") if box.architecture update = box.has_update?(version, download_options: download_options) if !update @@ -165,6 +191,7 @@ def box_update(box, version, ui, download_options, force) box_url: box.metadata_url, box_provider: update[2].name, box_version: update[1].version, + box_architecture: update[2].architecture, ui: ui, box_force: force, box_download_client_cert: download_options[:client_cert], diff --git a/plugins/commands/cloud/box/show.rb b/plugins/commands/cloud/box/show.rb index 35fdb8c4821..101387ab6df 100644 --- a/plugins/commands/cloud/box/show.rb +++ b/plugins/commands/cloud/box/show.rb @@ -11,7 +11,12 @@ class Show < Vagrant.plugin("2", :command) include Util def execute - options = {quiet: true, versions: []} + options = { + architectures: [], + providers: [], + quiet: true, + versions: [], + } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud box show [options] organization/box-name" @@ -21,8 +26,14 @@ def execute o.separator "Options:" o.separator "" + o.on("--architectures ARCH", String, "Filter results by architecture support (can be defined multiple times)") do |a| + options[:architectures].push(a).uniq! + end o.on("--versions VERSION", String, "Display box information for a specific version (can be defined multiple times)") do |v| - options[:versions] << v + options[:versions].push(v).uniq! + end + o.on("--providers PROVIDER", String, "Filter results by provider support (can be defined multiple times)") do |pv| + options[:providers].push(pv).uniq! end o.on("--[no-]auth", "Authenticate with Vagrant Cloud if required before searching") do |l| options[:quiet] = !l @@ -40,7 +51,7 @@ def execute @client = client_login(@env, options.slice(:quiet)) org, box_name = argv.first.split('/', 2) - show_box(org, box_name, @client&.token, options.slice(:versions)) + show_box(org, box_name, @client&.token, options.slice(:architectures, :providers, :versions)) end # Display the requested box to the user @@ -57,26 +68,61 @@ def show_box(org, box_name, access_token, options={}) access_token: access_token ) with_box(account: account, org: org, box: box_name) do |box| - if box && !Array(options[:versions]).empty? - box = box.versions.find_all{ |v| options[:versions].include?(v.version) } - else - box = [box] - end + list = [box] + + # If specific version(s) provided, filter out the version + list = list.first.versions.find_all { |v| + options[:versions].include?(v.version) + } if !Array(options[:versions]).empty? + + # If specific provider(s) provided, filter out the provider(s) + list = list.find_all { |item| + if item.is_a?(VagrantCloud::Box) + item.versions.any? { |v| + v.providers.any? { |p| + options[:providers].include?(p.name) + } + } + else + item.providers.any? { |p| + options[:providers].include?(p.name) + } + end + } if !Array(options[:providers]).empty? + + list = list.find_all { |item| + if item.is_a?(VagrantCloud::Box) + item.versions.any? { |v| + v.providers.any? { |p| + options[:architectures].include?(p.architecture) + } + } + else + item.providers.any? { |p| + options[:architectures].include?(p.architecture) + } + end + } if !Array(options[:architectures]).empty? - if !box.empty? - box.each do |b| - format_box_results(b, @env) + if !list.empty? + list.each do |b| + format_box_results(b, @env, options.slice(:providers, :architectures)) @env.ui.output("") end 0 else @env.ui.warn(I18n.t("cloud_command.box.show_filter_empty", - version: options[:version], org: org, box_name: box_name)) + org: org, + box_name: box_name, + architectures: Array(options[:architectures]).empty? ? "N/A" : Array(options[:architectures]).join(", "), + providers: Array(options[:providers]).empty? ? "N/A" : Array(options[:providers]).join(", "), + versions: Array(options[:versions]).empty? ? "N/A" : Array(options[:versions]).join(", ") + )) 1 end end rescue VagrantCloud::Error => e - @env.ui.error(I18n.t("cloud_command.errors.box.show_fail", org: org,box_name:box_name)) + @env.ui.error(I18n.t("cloud_command.errors.box.show_fail", org: org, box_name:box_name)) @env.ui.error(e.message) 1 end diff --git a/plugins/commands/cloud/client/client.rb b/plugins/commands/cloud/client/client.rb index ff44e707c8f..1e7fec351e9 100644 --- a/plugins/commands/cloud/client/client.rb +++ b/plugins/commands/cloud/client/client.rb @@ -168,6 +168,11 @@ def token def with_error_handling(&block) yield + rescue VagrantCloud::Error::ClientError => e + @logger.debug("vagrantcloud request error:") + @logger.debug(e.message) + @logger.debug(e.backtrace.join("\n")) + raise Errors::Unexpected, error: e.message rescue Excon::Error::Unauthorized @logger.debug("Unauthorized!") raise Errors::Unauthorized diff --git a/plugins/commands/cloud/locales/en.yml b/plugins/commands/cloud/locales/en.yml index acdae7a04c9..81c8c2461fb 100644 --- a/plugins/commands/cloud/locales/en.yml +++ b/plugins/commands/cloud/locales/en.yml @@ -39,13 +39,25 @@ en: Box Description: %{description} box_short_desc: |- Box Short Description: %{short_description} + checksum_type: |- + Checksum Type: %{checksum_type} + checksum_value: |- + Checksum Value: %{checksum_value} + architecture: |- + Box Architecture: %{architecture} + default_architecture: |- + Default Architecture: true version_desc: |- Version Description: %{version_description} continue: |- Do you wish to continue? [y/N] box: show_filter_empty: |- - No version matched %{version} for %{org}/%{box_name} + No matches found for %{org}/%{box_name} + Filters applied: + Architectures: %{architectures} + Providers: %{providers} + Versions: %{versions} create_success: |- Created box %{org}/%{box_name} delete_success: |- @@ -68,12 +80,19 @@ en: Uploading box file for '%{org}/%{box_name}' (v%{version}) for provider: '%{provider}' upload_success: |- Uploaded provider %{provider} on %{org}/%{box_name} for version %{version} + delete_multiple_architectures: |- + Multiple architectures detected for %{provider} on %{org}/%{box_name}: + + delete_architectures_prompt: |- + Please enter the architecture name to delete: delete_warn: |- - This will completely remove provider %{provider} on version %{version} from %{box} on Vagrant Cloud. This cannot be undone. + This will completely remove provider %{provider} with architecture %{architecture} + on version %{version} from %{box} on Vagrant Cloud. This cannot be undone. create_success: |- - Created provider %{provider} on %{org}/%{box_name} for version %{version} + Created provider %{provider} with %{architecture} architecture on %{org}/%{box_name} for version %{version} delete_success: |- - Deleted provider %{provider} on %{org}/%{box_name} for version %{version} + Deleted provider %{provider} with %{architecture} architecture on %{org}/%{box_name} + for version %{version} update_success: |- Updated provider %{provider} on %{org}/%{box_name} for version %{version} not_found: |- @@ -122,13 +141,13 @@ en: Failed to locate account information provider: create_fail: |- - Failed to create provider %{provider} on box %{org}/%{box_name} for version %{version} + Failed to create '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box update_fail: |- - Failed to update provider %{provider} on box %{org}/%{box_name} for version %{version} + Failed to update '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box delete_fail: |- - Failed to delete provider %{provider} on box %{org}/%{box_name} for version %{version} + Failed to delete '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box upload_fail: |- - Failed to upload provider %{provider} on box %{org}/%{box_name} for version %{version} + Failed to upload '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box version: create_fail: |- Failed to create version %{version} on box %{org}/%{box_name} diff --git a/plugins/commands/cloud/provider/create.rb b/plugins/commands/cloud/provider/create.rb index e41bc98388d..30c55c7d610 100644 --- a/plugins/commands/cloud/provider/create.rb +++ b/plugins/commands/cloud/provider/create.rb @@ -11,7 +11,9 @@ class Create < Vagrant.plugin("2", :command) include Util def execute - options = {} + options = { + architecture: Vagrant::Util::Platform.architecture, + } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud provider create [options] organization/box-name provider-name version [url]" @@ -21,12 +23,18 @@ def execute o.separator "Options:" o.separator "" + o.on("-a", "--architecture ARCH", String, "Architecture of guest box (defaults to current host architecture)") do |a| + options[:architecture] = a + end o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c| options[:checksum] = c end o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c| options[:checksum_type] = c end + o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| + options[:default_architecture] = d + end end # Parse the options @@ -56,8 +64,10 @@ def execute # @param [String] url Provider asset URL # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options + # @option options [String] :architecture Architecture of guest box # @option options [String] :checksum Checksum of the box asset # @option options [String] :checksum_type Type of the checksum + # @option options [Boolean] :default_architecture Default architecture for named provider # @return [Integer] def create_provider(org, box, version, provider, url, access_token, options={}) if !url @@ -68,21 +78,23 @@ def create_provider(org, box, version, provider, url, access_token, options={}) access_token: access_token ) with_version(account: account, org: org, box: box, version: version) do |version| - provider = version.add_provider(provider) + provider = version.add_provider(provider, options[:architecture]) provider.checksum = options[:checksum] if options.key?(:checksum) provider.checksum_type = options[:checksum_type] if options.key?(:checksum_type) + provider.architecture = options[:architecture] if options.key?(:architecture) + provider.default_architecture = options[:default_architecture] if options.key?(:default_architecture) provider.url = url if url provider.save @env.ui.success(I18n.t("cloud_command.provider.create_success", - provider: provider.name, org: org, box_name: box, version: version.version)) + architecture: options[:architecture], provider: provider.name, org: org, box_name: box, version: version.version)) format_box_results(provider, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.create_fail", - provider: provider, org: org, box_name: box, version: version)) + architecture: options[:architecture], provider: provider.name, org: org, box_name: box, version: version)) @env.ui.error(e.message) 1 end diff --git a/plugins/commands/cloud/provider/delete.rb b/plugins/commands/cloud/provider/delete.rb index e7a430ad3f2..f80ebff32b3 100644 --- a/plugins/commands/cloud/provider/delete.rb +++ b/plugins/commands/cloud/provider/delete.rb @@ -14,7 +14,7 @@ def execute options = {} opts = OptionParser.new do |o| - o.banner = "Usage: vagrant cloud provider delete [options] organization/box-name provider-name version" + o.banner = "Usage: vagrant cloud provider delete [options] organization/box-name provider-name version [architecture]" o.separator "" o.separator "Deletes a provider entry on Vagrant Cloud" o.separator "" @@ -28,7 +28,7 @@ def execute # Parse the options argv = parse_options(opts) return if !argv - if argv.count != 3 + if argv.count < 3 || argv.count > 4 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @@ -36,18 +36,47 @@ def execute org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] + architecture = argv[3] + + @client = client_login(@env) + account = VagrantCloud::Account.new( + custom_server: api_server_url, + access_token: @client.token + ) + + if architecture.nil? + architecture = select_provider_architecture(account, org, box_name, version, provider_name) + end @env.ui.warn(I18n.t("cloud_command.provider.delete_warn", - provider: provider_name, version:version, box: argv.first)) + architecture: architecture, provider: provider_name, version: version, box: argv.first)) if !options[:force] cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end - @client = client_login(@env) + delete_provider(org, box_name, version, provider_name, architecture, account, options) + end - delete_provider(org, box_name, version, provider_name, @client.token, options) + def select_provider_architecture(account, org, box, version, provider) + with_version(account: account, org: org, box: box, version: version) do |box_version| + list = box_version.providers.map(&:architecture) + return list.first if list.size == 1 + + @env.ui.info(I18n.t("cloud_command.provider.delete_multiple_architectures", + org: org, box_name: box, provider: provider)) + list.each do |provider_name| + @env.ui.info(" * #{provider_name}") + end + selected = nil + while selected.nil? + user_input = @env.ui.ask(I18n.t("cloud_command.provider.delete_architectures_prompt") + " ") + selected = user_input if list.include?(user_input) + end + + return selected + end end # Delete a provider for the box version @@ -56,23 +85,20 @@ def execute # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name - # @param [String] access_token User Vagrant Cloud access token + # @param [String] architecture Architecture of guest + # @param [VagrantCloud::Account] account VagrantCloud account # @param [Hash] options Currently unused # @return [Integer] - def delete_provider(org, box, version, provider, access_token, options={}) - account = VagrantCloud::Account.new( - custom_server: api_server_url, - access_token: access_token - ) - with_provider(account: account, org: org, box: box, version: version, provider: provider) do |p| + def delete_provider(org, box, version, provider, architecture, account, options={}) + with_provider(account: account, org: org, box: box, version: version, provider: provider, architecture: architecture) do |p| p.delete @env.ui.error(I18n.t("cloud_command.provider.delete_success", - provider: provider, org: org, box_name: box, version: version)) + architecture: architecture, provider: provider, org: org, box_name: box, version: version)) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.delete_fail", - provider: provider, org: org, box_name: box, version: version)) + architecture: architecture, provider: provider, org: org, box_name: box, version: version)) @env.ui.error(e) 1 end diff --git a/plugins/commands/cloud/provider/update.rb b/plugins/commands/cloud/provider/update.rb index 894089627c3..22bb6b75bd7 100644 --- a/plugins/commands/cloud/provider/update.rb +++ b/plugins/commands/cloud/provider/update.rb @@ -14,25 +14,31 @@ def execute options = {} opts = OptionParser.new do |o| - o.banner = "Usage: vagrant cloud provider update [options] organization/box-name provider-name version [url]" + o.banner = "Usage: vagrant cloud provider update [options] organization/box-name provider-name version architecture [url]" o.separator "" o.separator "Updates a provider entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" + o.on("-a", "--architecture ARCH", String, "Update architecture value of guest box") do |a| + options[:architecture] = a + end o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c| options[:checksum] = c end o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c| options[:checksum_type] = c end + o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| + options[:default_architecture] = d + end end # Parse the options argv = parse_options(opts) return if !argv - if argv.count < 3 || argv.count > 4 + if argv.count < 4 || argv.count > 5 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @@ -42,9 +48,10 @@ def execute org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] - url = argv[3] + architecture = argv[3] + url = argv[4] - update_provider(org, box_name, version, provider_name, url, @client.token, options) + update_provider(org, box_name, version, provider_name, architecture, url, @client.token, options) end # Update a provider for the box version @@ -53,12 +60,13 @@ def execute # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name + # @param [String] architecture Architecture of guest # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @option options [String] :checksum Checksum of the box asset # @option options [String] :checksum_type Type of the checksum # @return [Integer] - def update_provider(org, box, version, provider, url, access_token, options) + def update_provider(org, box, version, provider, architecture, url, access_token, options) if !url @env.ui.warn(I18n.t("cloud_command.upload.no_url")) end @@ -67,21 +75,23 @@ def update_provider(org, box, version, provider, url, access_token, options) access_token: access_token ) - with_provider(account: account, org: org, box: box, version: version, provider: provider) do |p| + with_provider(account: account, org: org, box: box, version: version, provider: provider, architecture: architecture) do |p| p.checksum = options[:checksum] if options.key?(:checksum) p.checksum_type = options[:checksum_type] if options.key?(:checksum_type) + p.architecture = options[:architecture] if options.key?(:architecture) + p.default_architecture = options[:default_architecture] if options.key?(:default_architecture) p.url = url if !url.nil? p.save @env.ui.success(I18n.t("cloud_command.provider.update_success", - provider: provider, org: org, box_name: box, version: version)) + architecture: architecture, provider: provider, org: org, box_name: box, version: version)) format_box_results(p, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.update_fail", - provider: provider, org: org, box_name: box, version: version)) + architecture: architecture, provider: provider, org: org, box_name: box, version: version)) @env.ui.error(e.message) 1 end diff --git a/plugins/commands/cloud/publish.rb b/plugins/commands/cloud/publish.rb index 8409363f200..a8e9c71b651 100644 --- a/plugins/commands/cloud/publish.rb +++ b/plugins/commands/cloud/publish.rb @@ -11,7 +11,10 @@ class Publish < Vagrant.plugin("2", :command) include Util def execute - options = {direct_upload: true} + options = { + architecture: Vagrant::Util::Platform.architecture, + direct_upload: true, + } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud publish [options] organization/box-name version provider-name [provider-file]" @@ -21,8 +24,8 @@ def execute o.separator "Options:" o.separator "" - o.on("--box-version VERSION", String, "Version of box to create") do |v| - options[:box_version] = v + o.on("-a", "--architecture ARCH", String, "Architecture of guest box (defaults to current host architecture)") do |a| + options[:architecture] = a end o.on("--url URL", String, "Remote URL to download this provider (cannot be used with provider-file)") do |u| options[:url] = u @@ -54,6 +57,9 @@ def execute o.on("--[no-]direct-upload", "Upload asset directly to backend storage") do |d| options[:direct_upload] = d end + o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| + options[:default_architecture] = d + end end # Parse the options @@ -63,7 +69,7 @@ def execute if argv.length < 3 || # missing required arguments argv.length > 4 || # too many arguments (argv.length < 4 && !options.key?(:url)) || # file argument required if url is not provided - (argv.length > 3 && options.key?(:url)) # cannot provider url and file argument + (argv.length > 3 && options.key?(:url)) # cannot provide url and file argument raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @@ -78,7 +84,8 @@ def execute @client = client_login(@env) params = options.slice(:private, :release, :url, :short_description, - :description, :version_description, :checksum, :checksum_type) + :description, :version_description, :checksum, :checksum_type, + :architecture, :default_architecture) # Display output to user describing action to be taken display_preamble(org, box_name, version, provider_name, params) @@ -91,12 +98,17 @@ def execute # Load up all the models we'll need to publish the asset box = load_box(org, box_name, @client.token) box_v = load_box_version(box, version) - box_p = load_version_provider(box_v, provider_name) + box_p = load_version_provider(box_v, provider_name, params[:architecture]) # Update all the data set_box_info(box, params.slice(:private, :short_description, :description)) set_version_info(box_v, params.slice(:version_description)) - set_provider_info(box_p, params.slice(:checksum, :checksum_type, :url)) + set_provider_info(box_p, params.slice( + :architecture, + :checksum, + :checksum_type, + :default_architecture, + :url)) # Save any updated state @env.ui.warn(I18n.t("cloud_command.publish.box_save")) @@ -114,7 +126,7 @@ def execute # And we're done! @env.ui.success(I18n.t("cloud_command.publish.complete", org: org, box_name: box_name)) - format_box_results(box, @env) + format_box_results(box_p, @env) 0 rescue VagrantCloud::Error => err @env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name)) @@ -188,14 +200,18 @@ def set_version_info(version, options={}) # # @param [VagrantCloud::Box::Provider] provider Vagrant Cloud box version provider # @param [Hash] options + # @option options [String] architecture Guest architecture of box # @option options [String] :url Remote URL for self hosted # @option options [String] :checksum_type Type of checksum value provided # @option options [String] :checksum Checksum of the box asset + # @option options [Boolean] :default_architecture Default architecture for named provider # @return [VagrantCloud::Box::Provider] def set_provider_info(provider, options={}) provider.url = options[:url] if options.key?(:url) provider.checksum_type = options[:checksum_type] if options.key?(:checksum_type) provider.checksum = options[:checksum] if options.key?(:checksum) + provider.architecture = options[:architecture] if options.key?(:architecture) + provider.default_architecture = options[:default_architecture] if options.key?(:default_architecture) provider end @@ -204,10 +220,13 @@ def set_provider_info(provider, options={}) # @param [VagrantCloud::Box::Version] version The version of the Vagrant Cloud box # @param [String] provider_name Name of the provider # @return [VagrantCloud::Box::Provider] - def load_version_provider(version, provider_name) - provider = version.providers.detect { |pv| pv.name == provider_name } + def load_version_provider(version, provider_name, architecture) + provider = version.providers.detect { |pv| + pv.name == provider_name && + pv.architecture == architecture + } return provider if provider - version.add_provider(provider_name) + version.add_provider(provider_name, architecture) end # Load the requested box version @@ -244,9 +263,11 @@ def load_box(org, box_name, access_token) # @param [String] version Version of the box to publish # @param [String] provider_name Name of the provider being published # @param [Hash] options + # @option options [String] :architecture Name of architecture of provider being published# # @option options [Boolean] :private Box is private # @option options [Boolean] :release Box should be released # @option options [String] :url Remote URL for self-hosted boxes + # @option options [Boolean] :default_architecture Architecture is default for provider name # @option options [String] :description Description of the box # @option options [String] :short_description Short description of the box # @option options [String] :version_description Description of the box version @@ -257,6 +278,9 @@ def display_preamble(org, box_name, version, provider_name, options={}) box_name: box_name, version: version, provider_name: provider_name)) @env.ui.info(I18n.t("cloud_command.publish.confirm.private")) if options[:private] @env.ui.info(I18n.t("cloud_command.publish.confirm.release")) if options[:release] + @env.ui.info(I18n.t("cloud_command.publish.confirm.architecture", + architecture: options[:architecture])) + @env.ui.info(I18n.t("cloud_command.publish.confirm.default_architecture")) if options[:default_architecture] @env.ui.info(I18n.t("cloud_command.publish.confirm.box_url", url: options[:url])) if options[:url] @env.ui.info(I18n.t("cloud_command.publish.confirm.box_description", diff --git a/plugins/commands/cloud/search.rb b/plugins/commands/cloud/search.rb index 7fd7541cf77..a18793b2b12 100644 --- a/plugins/commands/cloud/search.rb +++ b/plugins/commands/cloud/search.rb @@ -21,6 +21,9 @@ def execute o.separator "Options:" o.separator "" + o.on("-a", "--architecture ARCH", "Filter search results to a single architecture. Defaults to all.") do |a| + options[:architecture] = a + end o.on("-j", "--json", "Formats results in JSON") do |j| options[:json] = j end @@ -78,7 +81,7 @@ def search(query, access_token, options={}) custom_server: api_server_url, access_token: access_token ) - params = {query: query}.merge(options.slice(:provider, :sort, :order, :limit, :page)) + params = {query: query}.merge(options.slice(:architecture, :provider, :sort, :order, :limit, :page)) result = account.searcher.search(**params) if result.boxes.empty? diff --git a/plugins/commands/cloud/util.rb b/plugins/commands/cloud/util.rb index 011800a6355..eeaf8338b01 100644 --- a/plugins/commands/cloud/util.rb +++ b/plugins/commands/cloud/util.rb @@ -102,7 +102,8 @@ def format_search_results(search_results, short, json, env) name: b.tag, version: b.current_version.version, downloads: format_downloads(b.downloads.to_s), - providers: b.current_version.providers.map(&:name).join(", ") + providers: b.current_version.providers.map(&:name).uniq.join(", "), + architectures: b.current_version.providers.map(&:architecture).join(", ") } end @@ -126,19 +127,26 @@ def format_search_results(search_results, short, json, env) # @param [VagrantCloud::Box, VagrantCloud::Box::Version] box Box or box version to display # @param [Vagrant::Environment] env Current Vagrant environment # @return [nil] - def format_box_results(box, env) + def format_box_results(box, env, options={}) if box.is_a?(VagrantCloud::Box) - info = box_info(box) - elsif box.is_a?(VagrantCloud::Box::Provider) - info = version_info(box.version) - else + info = box_info(box, options) + elsif box.is_a?(VagrantCloud::Box::Version) info = version_info(box) + else + info = provider_info(box) end width = info.keys.map(&:size).max info.each do |k, v| - whitespace = width - k.size - env.ui.info "#{k}: #{"".ljust(whitespace)} #{v}" + v.to_s.split("\n").each_with_index do |line, idx| + whitespace = width - k.size + line.to_s.size + if idx == 0 + env.ui.info "#{k}: #{line.rjust(whitespace)}" + else + whitespace += k.size + 2 + env.ui.info line.rjust(whitespace) + end + end end nil end @@ -193,9 +201,12 @@ def with_version(account:, org:, box:, version:) # @yieldparam [VagrantCloud::Box::Provider] provider Requested Vagrant Cloud box version provider # @yieldreturn [Integer] # @return [Integer] - def with_provider(account:, org:, box:, version:, provider:) + def with_provider(account:, org:, box:, version:, provider:, architecture:) with_version(account: account, org: org, box: box, version: version) do |v| - p = v.providers.detect { |p| p.name == provider } + p = v.providers.detect { |p| + p.name == provider && + p.architecture == architecture + } if !p @env.ui.error(I18n.t("cloud_command.provider.not_found", org: org, box_name: box, version: version, provider_name: provider)) @@ -211,19 +222,42 @@ def with_provider(account:, org:, box:, version:, provider:) # # @param [VagrantCloud::Box] box Box for extracting information # @return [Hash] - def box_info(box) + def box_info(box, options={}) + current_version = box.current_version + if current_version + current_version = nil if !Array(options[:providers]).empty? && + current_version.providers.none? { |p| options[:providers].include?(p.name) } + current_version = nil if !Array(options[:architectures]).empty? && + current_version.providers.none? { |p| options[:architectures].include?(p.architecture) } + end + versions = box.versions + # Apply provider filter if defined + versions = versions.find_all { |v| + v.providers.any? { |p| + options[:providers].include?(p.name) + } + } if !Array(options[:providers]).empty? + # Apply architecture filter if defined + versions = versions.find_all { |v| + v.providers.any? { |p| + options[:architectures].include?(p.architecture) + } + } if !Array(options[:architectures]).empty? + + raise "no matches" if current_version.nil? && versions.empty? + Hash.new.tap do |i| i["Box"] = box.tag i["Description"] = box.description i["Private"] = box.private ? "yes" : "no" i["Created"] = box.created_at i["Updated"] = box.updated_at - if !box.current_version.nil? + if !current_version.nil? i["Current Version"] = box.current_version.version else i["Current Version"] = "N/A" end - i["Versions"] = box.versions.slice(0, 5).map(&:version).join(", ") + i["Versions"] = versions.slice(0, 5).map(&:version).join(", ") if box.versions.size > 5 i["Versions"] += " ..." end @@ -236,17 +270,35 @@ def box_info(box) # @param [VagrantCloud::Box::Version] version Box version for extracting information # @return [Hash] def version_info(version) + provider_arches = version.providers.group_by(&:name).map { |provider_name, info| + "#{provider_name} (#{info.map(&:architecture).sort.join(", ")})" + }.sort.join("\n") Hash.new.tap do |i| i["Box"] = version.box.tag i["Version"] = version.version i["Description"] = version.description i["Status"] = version.status - i["Providers"] = version.providers.map(&:name).sort.join(", ") + i["Providers"] = provider_arches i["Created"] = version.created_at i["Updated"] = version.updated_at end end + # Extract provider information for display + # + # @param [VagrantCloud::Box::Provider] provider Box provider for extracting information + # @return [Hash] + def provider_info(provider) + { + "Box" => provider.version.box.tag, + "Private" => provider.version.box.private ? "yes" : "no", + "Version" => provider.version.version, + "Provider" => provider.name, + "Architecture" => provider.architecture, + "Default Architecture" => provider.default_architecture ? "yes" : "no", + } + end + # Print table results from search request # # @param [Vagrant::Environment] env Current Vagrant environment diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index e0c4fe740be..3c342a5166f 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -32,6 +32,7 @@ class VMConfig < Vagrant.plugin("2", :config) attr_accessor :base_address attr_accessor :boot_timeout attr_accessor :box + attr_accessor :box_architecture attr_accessor :ignore_box_vagrantfile attr_accessor :box_check_update attr_accessor :box_url @@ -69,6 +70,7 @@ def initialize @base_address = UNSET_VALUE @boot_timeout = UNSET_VALUE @box = UNSET_VALUE + @box_architecture = UNSET_VALUE @ignore_box_vagrantfile = UNSET_VALUE @box_check_update = UNSET_VALUE @box_download_ca_cert = UNSET_VALUE @@ -509,6 +511,11 @@ def finalize! @base_address = nil if @base_address == UNSET_VALUE @boot_timeout = 300 if @boot_timeout == UNSET_VALUE @box = nil if @box == UNSET_VALUE + @box_architecture = :auto if @box_architecture == UNSET_VALUE + # If box architecture value was set, force to string + if @box_architecture && @box_architecture != :auto + @box_architecture = @box_architecture.to_s + end @ignore_box_vagrantfile = false if @ignore_box_vagrantfile == UNSET_VALUE if @box_check_update == UNSET_VALUE diff --git a/templates/locales/en.yml b/templates/locales/en.yml index c6857c8799f..a66eabfaea1 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -659,6 +659,13 @@ en: the box are shown below: %{providers} + box_not_found_with_provider_architecture: |- + The box '%{name}' for the provider '%{provider}' isn't installed + for the architecture '%{architecture}'. Please double-check and + try again. The installed architectures for the box are shown + below: + + %{architectures} box_not_found_with_provider_and_version: |- The box '%{name}' (v%{version}) with provider '%{provider}' could not be found. Please double check and try again. You @@ -669,35 +676,51 @@ en: Provider expected: %{expected} Provider of box: %{actual} box_remove_multi_provider: |- - You requested to remove the box '%{name}'. This box has - multiple providers. You must explicitly select a single - provider to remove with `--provider`. + You requested to remove the box '%{name}' version '%{version}'. This + box has multiple providers. You must explicitly select a single provider to + remove with `--provider` or specify the `--all-providers` flag to remove + all providers. Available providers: %{providers} box_remove_multi_version: |- - You requested to remove the box '%{name}' with provider - '%{provider}'. This box has multiple versions. You must - explicitly specify which version you want to remove with - the `--box-version` flag or specify the `--all` flag to remove all - versions. The available versions for this box are: + You requested to remove the box '%{name}'. This box has multiple + versions. You must explicitly specify which version you want to + remove with the `--box-version` flag or specify the `--all` flag + to remove all versions. The available versions for this box are: %{versions} box_remove_not_found: |- The box you requested to be removed could not be found. No boxes named '%{name}' could be found. box_remove_provider_not_found: |- - You requested to remove the box '%{name}' with provider - '%{provider}'. The box '%{name}' exists but not with - the provider specified. Please double-check and try again. + You requested to remove the box '%{name}' version '%{version}' + with provider '%{provider}'. The box '%{name}' exists but not + with the provider specified. Please double-check and try again. The providers for this are: %{providers} box_remove_version_not_found: |- - You requested to remove the box '%{name}' version '%{version}' with - provider '%{provider}', but that specific version of the box is - not installed. Please double-check and try again. The available versions - for this box are: + You requested to remove the box '%{name}' version '%{version}', + but that specific version of the box is not installed. Please + double-check and try again. The available versions for this box + are: %{versions} + box_remove_multi_architecture: |- + You requested to remove the box '%{name}' version '%{version}' + with provider '%{provider}'. This box has multiple architectures. + You must explicitly select a single architecture to remove with + `--architecture` or specify the `--all-architectures` flag to + remove all architectures. The available architectures for this + box are: + + %{architectures} + box_remove_architecture_not_found: |- + You requested to remove the box '%{name}' version '%{version}' + with provider '%{provider}' and architecture '%{architecture}' but + that specific architecture is not installed. Please double-check + and try again. The available architectures are: + + %{architectures} box_server_not_set: |- A URL to a Vagrant Cloud server is not set, so boxes cannot be added with a shorthand ("mitchellh/precise64") format. You may also be seeing this @@ -710,9 +733,15 @@ en: box_update_multi_provider: |- You requested to update the box '%{name}'. This box has multiple providers. You must explicitly select a single - provider to remove with `--provider`. + provider to update with `--provider`. Available providers: %{providers} + box_update_multi_architecture: |- + You requested to update the box '%{name}' (v%{version}) with provider + '%{provider}'. This box has multiple architectures. You must explicitly + select a single architecture to update with `--architecture`. + + Available architectures: %{architectures} box_update_no_box: |- Box '%{name}' not installed, can't check for updates. box_update_no_metadata: |- @@ -2188,10 +2217,10 @@ en: box: no_installed_boxes: "There are no installed boxes! Use `vagrant box add` to add some." remove_in_use_query: |- - Box '%{name}' (v%{version}) with provider '%{provider}' appears - to still be in use by at least one Vagrant environment. Removing - the box could corrupt the environment. We recommend destroying - these environments first: + Box '%{name}' (v%{version}) with provider '%{provider}' and + architectures '%{architecture}' appears to still be in use by + at least one Vagrant environment. Removing the box could corrupt + the environment. We recommend destroying these environments first: %{users} diff --git a/vagrant.gemspec b/vagrant.gemspec index 88cee701f83..1425a35bb87 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| s.add_dependency "rexml", "~> 3.2" s.add_dependency "rgl", "~> 0.5.10" s.add_dependency "rubyzip", "~> 2.3.2" - s.add_dependency "vagrant_cloud", "~> 3.0.5" + s.add_dependency "vagrant_cloud", "> 3.0.5", "< 3.1" s.add_dependency "wdm", "~> 0.1.1" s.add_dependency "winrm", ">= 2.3.6", "< 3.0" s.add_dependency "winrm-elevated", ">= 1.2.3", "< 2.0" From 6e13612111e026b87f141f91d67508b2c4a77063 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 28 Jul 2023 10:40:32 -0700 Subject: [PATCH 05/11] Add test coverage for architecture support --- .../plugins/commands/box/command/add_test.rb | 12 + .../commands/box/command/remove_test.rb | 36 ++ .../commands/box/command/update_test.rb | 350 +++++++++++++----- .../plugins/commands/cloud/box/show_test.rb | 32 +- .../commands/cloud/provider/create_test.rb | 55 ++- .../commands/cloud/provider/delete_test.rb | 63 +++- .../commands/cloud/provider/update_test.rb | 258 +++++++++++-- .../plugins/commands/cloud/publish_test.rb | 66 +++- .../plugins/commands/cloud/search_test.rb | 2 + test/unit/plugins/kernel_v2/config/vm_test.rb | 28 ++ test/unit/support/isolated_environment.rb | 5 +- .../vagrant/action/builtin/box_add_test.rb | 302 ++++++++++++++- .../vagrant/action/builtin/box_remove_test.rb | 145 +++++++- test/unit/vagrant/box_collection_test.rb | 24 +- test/unit/vagrant/box_metadata_test.rb | 300 +++++++++++++-- test/unit/vagrant/box_test.rb | 173 ++++++++- test/unit/vagrant/machine_test.rb | 3 + test/unit/vagrant/util/platform_test.rb | 64 ++++ test/unit/vagrant/vagrantfile_test.rb | 38 ++ 19 files changed, 1752 insertions(+), 204 deletions(-) diff --git a/test/unit/plugins/commands/box/command/add_test.rb b/test/unit/plugins/commands/box/command/add_test.rb index 6ce467c6f3a..a3d504ec87c 100644 --- a/test/unit/plugins/commands/box/command/add_test.rb +++ b/test/unit/plugins/commands/box/command/add_test.rb @@ -67,4 +67,16 @@ to raise_error(Vagrant::Errors::CLIInvalidUsage) end end + + context "with architecture flag" do + let(:argv) { ["foo", "--architecture", "test-arch"] } + + it "executes the runner with box architecture set" do + expect(action_runner).to receive(:run) do |_, opts| + expect(opts[:box_architecture]).to eq("test-arch") + end + + subject.execute + end + end end diff --git a/test/unit/plugins/commands/box/command/remove_test.rb b/test/unit/plugins/commands/box/command/remove_test.rb index afefe49752b..d1124863162 100644 --- a/test/unit/plugins/commands/box/command/remove_test.rb +++ b/test/unit/plugins/commands/box/command/remove_test.rb @@ -81,4 +81,40 @@ to raise_error(Vagrant::Errors::CLIInvalidUsage) end end + + context "with architecture flag" do + let(:argv) { ["foo", "--architecture", "test-arch"] } + + it "should execute runner with box architecture set" do + expect(action_runner).to receive(:run) do |_, opts| + expect(opts[:box_architecture]).to eq("test-arch") + end + + subject.execute + end + end + + context "with all providers flag" do + let(:argv) { ["foo", "--all-providers"] } + + it "should execute runner with all providers enabled" do + expect(action_runner).to receive(:run) do |_, opts| + expect(opts[:box_remove_all_providers]).to be(true) + end + + subject.execute + end + end + + context "with all architectures flag" do + let(:argv) { ["foo", "--all-architectures"] } + + it "should execute runner with all architectures enabled" do + expect(action_runner).to receive(:run) do |_, opts| + expect(opts[:box_remove_all_architectures]).to be(true) + end + + subject.execute + end + end end diff --git a/test/unit/plugins/commands/box/command/update_test.rb b/test/unit/plugins/commands/box/command/update_test.rb index 2d9007ed7cb..76b4fb3fb51 100644 --- a/test/unit/plugins/commands/box/command/update_test.rb +++ b/test/unit/plugins/commands/box/command/update_test.rb @@ -37,18 +37,16 @@ describe "execute" do context "updating specific box" do let(:argv) { ["--box", "foo"] } - let(:scratch) { Dir.mktmpdir("vagrant-test-command-box-update-execute") } - let(:metadata_url) { Pathname.new(scratch).join("metadata.json") } - + let(:box_args) { ["foo", "1.0", :virtualbox] } + let(:box_opts) { {metadata_url: metadata_url.to_s} } before do metadata_url.open("w") do |f| f.write("") end - test_iso_env.box3( - "foo", "1.0", :virtualbox, metadata_url: metadata_url.to_s) + test_iso_env.box3(*box_args, **box_opts) end after do @@ -72,43 +70,43 @@ it "does the correct update if there is an update" do metadata_url.open("w") do |f| - f.write(<<-RAW) - { - "name": "foo", - "versions": [ - { - "version": "1.0" - }, - { - "version": "1.8", - "providers": [ + f.write( + { + name: "foo", + versions: [ { - "name": "virtualbox", - "url": "bar" - } - ] - }, - { - "version": "1.10", - "providers": [ + version: "1.0" + }, { - "name": "virtualbox", - "url": "bar" - } - ] - }, - { - "version": "1.11", - "providers": [ + version: "1.8", + providers: [ + { + name: "virtualbox", + url: "bar" + } + ] + }, + { + version: "1.10", + providers: [ + { + name: "virtualbox", + url: "bar" + } + ] + }, { - "name": "virtualbox", - "url": "bar" + version: "1.11", + providers: [ + { + name: "virtualbox", + url: "bar" + } + ] } - ] - } - ] - } - RAW + ] + }.to_json + ) end action_called = false @@ -147,25 +145,25 @@ it "updates the proper box" do metadata_url.open("w") do |f| - f.write(<<-RAW) - { - "name": "foo", - "versions": [ - { - "version": "1.0" - }, - { - "version": "1.1", - "providers": [ - { - "name": "vmware", - "url": "bar" - } - ] - } - ] - } - RAW + f.write( + { + name: "foo", + versions: [ + { + version: "1.0" + }, + { + version: "1.1", + providers: [ + { + name: "vmware", + url: "bar" + } + ] + } + ] + }.to_json + ) end test_iso_env.box3("foo", "1.0", :vmware) @@ -200,25 +198,25 @@ it "passes down download options" do metadata_url.open("w") do |f| - f.write(<<-RAW) - { - "name": "foo", - "versions": [ - { - "version": "1.0" - }, - { - "version": "1.1", - "providers": [ + f.write( + { + name: "foo", + versions: [ { - "name": "virtualbox", - "url": "bar" + version: "1.0" + }, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar" + } + ] } - ] - } - ] - } - RAW + ] + }.to_json + ) end action_called = false @@ -249,6 +247,174 @@ to raise_error(Vagrant::Errors::BoxNotFound) end end + + context "with architecture" do + let(:box_opts) { {metadata_url: metadata_url.to_s, architecture: "test-arch"} } + + it "doesn't update if they're up to date" do + called = false + allow(action_runner).to receive(:run) do |callable, opts| + if opts[:box_provider] + called = true + end + + opts + end + + subject.execute + + expect(called).to be(false) + end + + it "does the correct update if there is an update" do + metadata_url.open("w") do |f| + f.write( + { + name: "foo", + versions: [ + { + version: "1.0" + }, + { + version: "1.8", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "test-arch", + default_architecture: true + } + ] + }, + { + version: "1.10", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "test-arch", + default_architecture: true + } + ] + }, + { + version: "1.11", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "test-arch", + default_architecture: true + } + ] + } + ] + }.to_json + ) + end + + action_called = false + allow(action_runner).to receive(:run) do |action, opts| + if opts[:box_provider] + action_called = true + expect(opts[:box_force]).to eq(nil) + expect(opts[:box_url]).to eq(metadata_url.to_s) + expect(opts[:box_provider]).to eq("virtualbox") + expect(opts[:box_version]).to eq("1.11") + expect(opts[:box_architecture]).to eq("test-arch") + expect(opts[:box_download_ca_path]).to be_nil + expect(opts[:box_download_ca_cert]).to be_nil + expect(opts[:box_download_client_cert]).to be_nil + expect(opts[:box_download_insecure]).to be_nil + end + + opts + end + + subject.execute + + expect(action_called).to be(true) + end + + it "raises an error if there are multiple providers" do + test_iso_env.box3("foo", "1.0", :vmware) + + expect(action_runner).to receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxUpdateMultiProvider) + end + + it "raises an error if there are multiple architectures" do + test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch") + + expect(action_runner).to receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxUpdateMultiArchitecture) + end + + context "with multiple architectures and specifying the architecture" do + let(:argv) { ["--box", "foo", "--architecture", "other-arch"] } + + it "updates the proper box" do + metadata_url.open("w") do |f| + f.write( + { + name: "foo", + versions: [ + { + version: "1.0" + }, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "test-arch", + }, + { + name: "virtualbox", + url: "bar", + architecture: "other-arch", + } + ] + } + ] + }.to_json + ) + end + + test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch") + + action_called = false + allow(action_runner).to receive(:run) do |action, opts| + if opts[:box_provider] + action_called = true + expect(opts[:box_url]).to eq(metadata_url.to_s) + expect(opts[:box_provider]).to eq("virtualbox") + expect(opts[:box_version]).to eq("1.1") + expect(opts[:box_architecture]).to eq("other-arch") + end + + opts + end + + subject.execute + + expect(action_called).to be(true) + end + + it "raises an error if that provider doesn't exist" do + expect(action_runner).to receive(:run).never + + expect { subject.execute }. + to raise_error(Vagrant::Errors::BoxNotFoundWithProviderArchitecture) + end + end + + end end context "updating environment machines" do @@ -286,25 +452,27 @@ context "boxes have an update" do let(:md) { - md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) - { - "name": "foo", - "versions": [ + Vagrant::BoxMetadata.new( + StringIO.new( { - "version": "1.0" - }, - { - "version": "1.1", - "providers": [ - { - "name": "virtualbox", - "url": "bar" - } + name: "foo", + versions: [ + { + version: "1.0" + }, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar" + } + ] + } ] - } - ] - } - RAW + }.to_json + ) + ) } before { allow(machine).to receive(:box).and_return(box) } diff --git a/test/unit/plugins/commands/cloud/box/show_test.rb b/test/unit/plugins/commands/cloud/box/show_test.rb index 398f70b990d..460c77fe6fa 100644 --- a/test/unit/plugins/commands/cloud/box/show_test.rb +++ b/test/unit/plugins/commands/cloud/box/show_test.rb @@ -40,7 +40,7 @@ end it "should display the box results" do - expect(subject).to receive(:format_box_results).with(box, env) + expect(subject).to receive(:format_box_results).with(box, env, {}) subject.show_box(org_name, box_name, access_token, options) end @@ -62,7 +62,7 @@ end it "should print the version details" do - expect(subject).to receive(:format_box_results).with(box_version, env) + expect(subject).to receive(:format_box_results).with(box_version, env, {}) subject.show_box(org_name, box_name, access_token, options) end @@ -144,6 +144,34 @@ subject.execute end end + + context "with architectures flag set" do + let(:arch_option) { "test-arch" } + + before { argv.push("--architectures").push(arch_option) } + + it "shouild show box with architectures option set" do + expect(subject).to receive(:show_box) do |*_, opts| + expect(opts[:architectures]).to eq([arch_option]) + end + + subject.execute + end + end + + context "with providers flag set" do + let(:provider_option) { "test-provider" } + + before { argv.push("--providers").push(provider_option) } + + it "shilud show box with providers option set" do + expect(subject).to receive(:show_box) do |*_, opts| + expect(opts[:providers]).to eq([provider_option]) + end + + subject.execute + end + end end end end diff --git a/test/unit/plugins/commands/cloud/provider/create_test.rb b/test/unit/plugins/commands/cloud/provider/create_test.rb index 09ca15699b6..64f2c57c3b0 100644 --- a/test/unit/plugins/commands/cloud/provider/create_test.rb +++ b/test/unit/plugins/commands/cloud/provider/create_test.rb @@ -43,7 +43,7 @@ subject { described_class.new(argv, env) } it "should add a new provider to the box version" do - expect(version).to receive(:add_provider).with(provider_name) + expect(version).to receive(:add_provider).with(provider_name, nil) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end @@ -166,6 +166,59 @@ subject.execute end end + + it "should include detected host architecture by default" do + host_arch = double("host-arch") + expect(Vagrant::Util::Platform).to receive(:architecture).and_return(host_arch) + + expect(subject).to receive(:create_provider) do |*_, opts| + expect(opts[:architecture]).to eq(host_arch) + end + + subject.execute + end + + context "with architecture flag" do + let(:arch_option) { "test-arch" } + + before { argv.push("--architecture").push(arch_option) } + + it "should include the architecture option" do + expect(subject).to receive(:create_provider) do |*args, opts| + expect(opts[:architecture]).to eq(arch_option) + end + + subject.execute + end + end + + context "with default architecture flag" do + before { argv.push(default_arch_flag) } + + context "when flag is enabled" do + let(:default_arch_flag) { "--default-architecture" } + + it "should include default architecture with true value" do + expect(subject).to receive(:create_provider) do |*args, opts| + expect(opts[:default_architecture]).to be(true) + end + + subject.execute + end + end + + context "when flag is disabled" do + let(:default_arch_flag) { "--no-default-architecture" } + + it "should include default architecture with false value" do + expect(subject).to receive(:create_provider) do |*args, opts| + expect(opts[:default_architecture]).to be(false) + end + + subject.execute + end + end + end end end end diff --git a/test/unit/plugins/commands/cloud/provider/delete_test.rb b/test/unit/plugins/commands/cloud/provider/delete_test.rb index a9710e11ba5..2a27495f7ba 100644 --- a/test/unit/plugins/commands/cloud/provider/delete_test.rb +++ b/test/unit/plugins/commands/cloud/provider/delete_test.rb @@ -16,7 +16,8 @@ let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version, providers: [provider]) } - let(:provider) { double("provider", name: box_version_provider) } + let(:provider) { double("provider", name: box_version_provider, architecture: architecture) } + let(:architecture) { double("test-architecture") } describe "#delete_provider" do let(:options) { {} } @@ -30,8 +31,14 @@ with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_provider). - with(account: account, org: org_name, box: box_name, version: box_version, provider: box_version_provider). - and_yield(provider) + with( + account: account, + org: org_name, + box: box_name, + version: box_version, + provider: box_version_provider, + architecture: architecture + ).and_yield(provider) allow(provider).to receive(:delete) end @@ -39,11 +46,11 @@ it "should delete the provider" do expect(provider).to receive(:delete) - subject.delete_provider(org_name, box_name, box_version, box_version_provider, access_token, options) + subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) end it "should return zero on success" do - r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, access_token, options) + r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) expect(r).to eq(0) end @@ -53,7 +60,7 @@ end it "should return non-zero" do - r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, access_token, options) + r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) expect(r).to be_a(Integer) expect(r).not_to eq(0) end @@ -75,11 +82,16 @@ let(:client) { double("client", token: access_token) } before do + allow(VagrantCloud::Account).to receive(:new).and_return(account) + allow(account).to receive(:organization).with(name: org_name). + and_return(organization) allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(iso_env.ui).to receive(:ask). and_return("y") + allow(subject).to receive(:select_provider_architecture). + and_return(architecture) allow(subject).to receive(:delete_provider) end @@ -115,7 +127,7 @@ it "should delete the provider" do expect(subject).to receive(:delete_provider). - with(org_name, box_name, version_arg, provider_arg, access_token, anything) + with(org_name, box_name, version_arg, provider_arg, architecture, account, anything) subject.execute end @@ -124,6 +136,43 @@ subject.execute end + context "with architecture argument" do + let(:architecture_argument) { "test-arch" } + + before { argv << architecture_argument } + + it "should delete the provider" do + expect(subject).to receive(:delete_provider). + with(org_name, box_name, version_arg, provider_arg, architecture_argument, account, anything) + subject.execute + end + + it "should not attempt to select the architecture" do + expect(subject).not_to receive(:select_provider_architecture) + subject.execute + end + end + + context "with multiple provider architectures" do + let(:box_version) { double("box-version", providers: providers) } + let(:providers) { + [ + double("dummy-provider", architecture: "amd64"), + double("dummy-provider", architecture: "arm64") + ] + } + + before do + expect(subject).to receive(:select_provider_architecture).and_call_original + expect(subject).to receive(:with_version).and_yield(box_version) + end + + it "should prompt for architecture selection" do + expect(iso_env.ui).to receive(:ask).and_return("amd64") + subject.execute + end + end + context "with force flag" do before { argv << "--force" } diff --git a/test/unit/plugins/commands/cloud/provider/update_test.rb b/test/unit/plugins/commands/cloud/provider/update_test.rb index 343fc02c438..3e5b2de0a6f 100644 --- a/test/unit/plugins/commands/cloud/provider/update_test.rb +++ b/test/unit/plugins/commands/cloud/provider/update_test.rb @@ -12,6 +12,7 @@ let(:box_name) { "my-box" } let(:box_version) { "1.0.0" } let(:box_version_provider) { "my-provider" } + let(:box_architecture) { "box-architecture" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } @@ -31,7 +32,14 @@ with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_provider). - with(account: account, org: org_name, box: box_name, version: box_version, provider: box_version_provider). + with( + account: account, + org: org_name, + box: box_name, + version: box_version, + provider: box_version_provider, + architecture: box_architecture + ). and_yield(provider) allow(provider).to receive(:save) allow(subject).to receive(:format_box_results) @@ -41,24 +49,60 @@ it "should update the provider" do expect(provider).to receive(:save) - subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) end it "should return 0 on success" do - result = subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + result = subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) expect(result).to eq(0) end it "should return non-zero result on error" do expect(provider).to receive(:save).and_raise(VagrantCloud::Error) - result = subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + result = subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) expect(result).not_to eq(0) expect(result).to be_a(Integer) end it "should not update the URL when unset" do expect(provider).not_to receive(:url=) - subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) end context "when URL is set" do @@ -66,19 +110,78 @@ it "should update the URL" do expect(provider).to receive(:url=).with(provider_url) - subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) end end context "with options set" do let(:checksum) { double("checksum") } let(:checksum_type) { double("checksum_type") } - let(:options) { {checksum: checksum, checksum_type: checksum_type} } + let(:options) { {} } + + after do + subject.update_provider( + org_name, + box_name, + box_version, + box_version_provider, + box_architecture, + provider_url, + access_token, + options + ) + end + + it "should not modify option controlled values when unset" do + expect(provider).not_to receive(:checksum=) + expect(provider).not_to receive(:checksum_type=) + expect(provider).not_to receive(:architecture=) + expect(provider).not_to receive(:default_architecture=) + end + + context "with checksum options set" do + let(:options) { {checksum: checksum, checksum_type: checksum_type} } - it "should set checksum options before saving" do - expect(provider).to receive(:checksum=).with(checksum) - expect(provider).to receive(:checksum_type=).with(checksum_type) - subject.update_provider(org_name, box_name, box_version, box_version_provider, provider_url, access_token, options) + it "should set checksum options before saving" do + expect(provider).to receive(:checksum=).with(checksum) + expect(provider).to receive(:checksum_type=).with(checksum_type) + end + end + + context "with architecture option set" do + let(:architecture) { double("architecture") } + let(:options) { {architecture: architecture} } + + it "should set architecture before saving" do + expect(provider).to receive(:architecture=).with(architecture) + end + end + + context "with default architecture option set" do + context "with true value" do + let(:options) { {default_architecture: true} } + + it "should set default architecture to true" do + expect(provider).to receive(:default_architecture=).with(true) + end + end + + context "with false value" do + let(:options) { {default_architecture: false} } + + it "should set default architecture to false" do + expect(provider).to receive(:default_architecture=).with(false) + end + end end end end @@ -134,37 +237,126 @@ before { argv << version_arg } - it "should create the provider" do - expect(subject).to receive(:update_provider).with(org_name, box_name, version_arg, provider_arg, any_args) - subject.execute + it "shows help" do + expect { subject.execute }. + to raise_error(Vagrant::Errors::CLIInvalidUsage) end - it "should not provide URL value" do - expect(subject).to receive(:update_provider).with(org_name, box_name, version_arg, provider_arg, nil, any_args) - subject.execute - end + context "with architecture argument" do + let(:architecture_arg) { "box-architecture" } + + before { argv << architecture_arg } + + it "should create the provider" do + expect(subject). + to receive(:update_provider). + with( + org_name, + box_name, + version_arg, + provider_arg, + architecture_arg, + any_args + ) + subject.execute + end - context "with URL argument" do - let(:url_arg) { "provider-url" } + it "should not provide URL value" do + expect(subject). + to receive(:update_provider). + with( + org_name, + box_name, + version_arg, + provider_arg, + architecture_arg, + nil, + any_args + ) + subject.execute + end - before { argv << url_arg } + context "with URL argument" do + let(:url_arg) { "provider-url" } + + before { argv << url_arg } + + it "should provide the URL value" do + expect(subject). + to receive(:update_provider). + with( + org_name, + box_name, + version_arg, + provider_arg, + architecture_arg, + url_arg, + any_args + ) + subject.execute + end + end - it "should provide the URL value" do - expect(subject).to receive(:update_provider).with(org_name, box_name, version_arg, provider_arg, url_arg, any_args) - subject.execute + context "with checksum and checksum type flags" do + let(:checksum_arg) { "checksum" } + let(:checksum_type_arg) { "checksum_type" } + + before { argv.push("--checksum").push(checksum_arg).push("--checksum-type").push(checksum_type_arg) } + + it "should include the checksum options" do + expect(subject). + to receive(:update_provider). + with( + org_name, + box_name, + version_arg, + provider_arg, + architecture_arg, + any_args, + hash_including( + checksum: checksum_arg, + checksum_type: checksum_type_arg + ) + ) + subject.execute + end end - end - context "with checksum and checksum type flags" do - let(:checksum_arg) { "checksum" } - let(:checksum_type_arg) { "checksum_type" } + context "with architecture flag" do + let(:architecture_flag) { "test-arch" } - before { argv.push("--checksum").push(checksum_arg).push("--checksum-type").push(checksum_type_arg) } + before { argv.push("--architecture").push(architecture_flag) } - it "should include the checksum options" do - expect(subject).to receive(:update_provider). - with(org_name, box_name, version_arg, provider_arg, any_args, hash_including(checksum: checksum_arg, checksum_type: checksum_type_arg)) - subject.execute + it "should include the architecture flag" do + expect(subject).to receive(:update_provider) do |*_, opts| + expect(opts[:architecture]).to eq(architecture_flag) + end + subject.execute + end + end + + context "with default architecture flag" do + context "enabled" do + before { argv.push("--default-architecture") } + + it "should include default architecture set to true" do + expect(subject).to receive(:update_provider) do |*_, opts| + expect(opts[:default_architecture]).to be(true) + end + subject.execute + end + end + + context "disabled" do + before { argv.push("--no-default-architecture") } + + it "should include default architecture set to false" do + expect(subject).to receive(:update_provider) do |*_, opts| + expect(opts[:default_architecture]).to be(false) + end + subject.execute + end + end end end end diff --git a/test/unit/plugins/commands/cloud/publish_test.rb b/test/unit/plugins/commands/cloud/publish_test.rb index c55ffd8b35f..38938fe0f11 100644 --- a/test/unit/plugins/commands/cloud/publish_test.rb +++ b/test/unit/plugins/commands/cloud/publish_test.rb @@ -20,16 +20,20 @@ let(:ui) { Vagrant::UI::Silent.new } let(:upload_url) { double("upload_url") } let(:access_token) { double("access_token") } + let(:default_architecture) { double("default-architecture") } subject { described_class.new(argv, iso_env) } before do + allow(Vagrant::Util::Platform).to receive(:architecture). + and_return(default_architecture) allow(iso_env).to receive(:ui).and_return(ui) allow(File).to receive(:stat).with(box). and_return(double("box_stat", size: box_size)) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: anything). and_return(account) + allow(provider).to receive(:architecture=).with(default_architecture) end describe "#upload_box_file" do @@ -148,21 +152,69 @@ let(:options) { {} } it "should not modify the provider" do + expect(provider).not_to receive(:url=) + expect(provider).not_to receive(:checksum=) + expect(provider).not_to receive(:checksum_type=) + expect(provider).not_to receive(:architecture=) + expect(provider).not_to receive(:default_architecture=) + subject.set_provider_info(provider, options) end end - context "with options set" do - let(:options) { {url: url, checksum: checksum, checksum_type: checksum_type} } + context "with options" do + let(:options) { {} } let(:url) { double("url") } let(:checksum) { double("checksum") } let(:checksum_type) { double("checksum_type") } + let(:architecture) { double("architecture") } - it "should set info on provider" do - expect(provider).to receive(:url=).with(url) - expect(provider).to receive(:checksum=).with(checksum) - expect(provider).to receive(:checksum_type=).with(checksum_type) - subject.set_provider_info(provider, options) + after { subject.set_provider_info(provider, options) } + + context "with url set" do + before { options[:url] = url } + + it "should set url on provider" do + expect(provider).to receive(:url=).with(url) + end + end + + context "with checksum set" do + before do + options[:checksum] = checksum + options[:checksum_type] = checksum_type + end + + it "should set checksum on provider" do + expect(provider).to receive(:checksum=).with(checksum) + expect(provider).to receive(:checksum_type=).with(checksum_type) + end + end + + context "with architecture set" do + before { options[:architecture] = architecture } + + it "should set architecture on provider" do + expect(provider).to receive(:architecture=).with(architecture) + end + end + + context "with default architecture set" do + context "with true value" do + before { options[:default_architecture] = true } + + it "should set default architecture to true" do + expect(provider).to receive(:default_architecture=).with(true) + end + end + + context "with false value" do + before { options[:default_architecture] = false } + + it "should set default architecture to false" do + expect(provider).to receive(:default_architecture=).with(false) + end + end end end end diff --git a/test/unit/plugins/commands/cloud/search_test.rb b/test/unit/plugins/commands/cloud/search_test.rb index 6fca8f761b8..497b156cfc9 100644 --- a/test/unit/plugins/commands/cloud/search_test.rb +++ b/test/unit/plugins/commands/cloud/search_test.rb @@ -45,6 +45,7 @@ end context "with valid options" do + let(:architecture) { double("architecture") } let(:provider) { double("provider") } let(:sort) { double("sort") } let(:order) { double("order") } @@ -52,6 +53,7 @@ let(:page) { double("page") } let(:options) { { + architecture: architecture, provider: provider, sort: sort, order: order, diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index b23e53f63a9..265d07a2aad 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -116,6 +116,34 @@ def find_network(name) end end + describe "#box_architecture" do + it "is not required" do + subject.box_architecture = nil + subject.finalize! + assert_valid + end + + it "is :auto by default" do + subject.finalize! + assert_valid + expect(subject.box_architecture).to eq(:auto) + end + + it "can be set to custom value" do + subject.box_architecture = "test-arch" + subject.finalize! + assert_valid + expect(subject.box_architecture).to eq("test-arch") + end + + it "is converted to string" do + subject.box_architecture = :test_arch + subject.finalize! + assert_valid + expect(subject.box_architecture).to eq("test_arch") + end + end + context "#box_check_update" do it "defaults to true" do with_temp_env("VAGRANT_BOX_UPDATE_CHECK_DISABLE" => "") do diff --git a/test/unit/support/isolated_environment.rb b/test/unit/support/isolated_environment.rb index da31e553ceb..2550a81240b 100644 --- a/test/unit/support/isolated_environment.rb +++ b/test/unit/support/isolated_environment.rb @@ -100,8 +100,11 @@ def box2(name, provider, options=nil) # @param [String] provider # @return [Pathname] def box3(name, version, provider, **opts) + args = [name, version, provider.to_s] + args << opts[:architecture].to_s if opts[:architecture] + # Create the directory for the box - box_dir = boxes_dir.join(name, version, provider.to_s) + box_dir = boxes_dir.join(*args) box_dir.mkpath # Create the metadata.json for it diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 14e6ed587c2..7e00b3ba8d4 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -210,7 +210,7 @@ def with_web_server(path, **opts) env[:box_provider] = "virtualbox" expect(box_collection).to receive(:find).with( - "foo", ["virtualbox"], "0").and_return(box) + "foo", ["virtualbox"], "0", nil).and_return(box) expect(box_collection).to receive(:add).never expect(app).to receive(:call).never @@ -903,6 +903,304 @@ def with_web_server(path, **opts) subject.call(env) end + context "with box architecture configured" do + before do + allow(Vagrant::Util::Platform).to receive(:architecture).and_return("test-arch") + end + + context "set to :auto" do + before do + env[:box_architecture] = :auto + end + + it "adds the latest version of a box with only one provider and matching architecture" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "amd64", + default_architecture: true, + }, + { + name: "virtualbox", + url: "#{box_path}", + architecture: "test-arch", + default_architecture: false, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + expect(opts[:architecture]).to eq(:auto) + true + }.and_return(box) + + expect(app).to receive(:call).with(env) + + subject.call(env) + end + + it "adds the latest version of a box with only one provider and no unknown architecture set as default" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "#{box_path}", + architecture: "unknown", + default_architecture: true, + }, + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "amd64", + default_architecture: false, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + expect(opts[:architecture]).to eq(:auto) + true + }.and_return(box) + + expect(app).to receive(:call).with(env) + + subject.call(env) + end + + it "errors adding latest version of a box with only one provider and no matching architecture" do + allow(Vagrant::Util::Platform).to receive(:architecture).and_return("test-arch") + + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "#{box_path}", + architecture: "amd64", + default_architecture: true, + }, + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "arm64", + default_architecture: false, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + + expect { + subject.call(env) + }.to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) + end + end + + context "set to nil" do + before do + env[:box_architecture] = nil + end + + it "adds the latest version of a box with only one provider and default architecture" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "#{box_path}", + architecture: "amd64", + default_architecture: true, + }, + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "test-arch", + default_architecture: false, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + expect(opts[:architecture]).to be_nil + true + }.and_return(box) + + expect(app).to receive(:call).with(env) + + subject.call(env) + end + end + + context "set to explicit value" do + before do + env[:box_architecture] = "arm64" + end + + it "adds the latest version of a box with only one provider and matching architecture" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "#{box_path}", + architecture: "arm64", + default_architecture: false, + }, + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "test-arch", + default_architecture: true, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| + expect(checksum(path)).to eq(checksum(box_path)) + expect(name).to eq("foo/bar") + expect(version).to eq("0.7") + expect(opts[:metadata_url]).to eq("file://#{tf.path}") + expect(opts[:architecture]).to eq("arm64") + true + }.and_return(box) + + expect(app).to receive(:call).with(env) + + subject.call(env) + end + + it "errors adding the latest version of a box with only one provider and no matching architecture" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| + f.write( + { + name: "foo/bar", + versions: [ + { + version: "0.5", + }, + { + version: "0.7", + providers: [ + { + name: "virtualbox", + url: "#{box_path}", + architecture: "amd64", + default_architecture: true, + }, + { + name: "virtualbox", + url: "/dev/null/invalid.path", + architecture: "386", + default_architecture: false, + } + ] + } + ] + }.to_json + ) + f.close + end + + env[:box_url] = tf.path + + expect { + subject.call(env) + }.to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) + end + end + end + it "adds the latest version of a box with the specified provider" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-specific-provider", ".json"]).tap do |f| @@ -1304,7 +1602,7 @@ def with_web_server(path, **opts) env[:box_url] = tf.path expect(box_collection).to receive(:find). - with("foo/bar", "virtualbox", "0.7").and_return(box) + with("foo/bar", "virtualbox", "0.7", nil).and_return(box) expect(box_collection).to receive(:add).never expect(app).to receive(:call).never diff --git a/test/unit/vagrant/action/builtin/box_remove_test.rb b/test/unit/vagrant/action/builtin/box_remove_test.rb index 3100ea60733..e8d0b0edc8e 100644 --- a/test/unit/vagrant/action/builtin/box_remove_test.rb +++ b/test/unit/vagrant/action/builtin/box_remove_test.rb @@ -17,7 +17,7 @@ subject { described_class.new(app, env) } let(:box_collection) { double("box_collection") } - let(:home_path) { "foo" } + let(:home_path) { double("home-path") } let(:machine_index) { [] } let(:iso_env) { isolated_environment } @@ -27,12 +27,12 @@ end it "deletes the box if it is the only option" do - allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox]]) + allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox, nil]]) env[:box_name] = "foo" expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!).once @@ -54,7 +54,7 @@ env[:box_provider] = "virtualbox" expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once @@ -76,7 +76,7 @@ env[:box_version] = "1.0" expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once @@ -87,6 +87,131 @@ expect(env[:box_removed]).to equal(box) end + context "with architecture" do + let(:architecture) { "test-arch" } + let(:box) do + box_dir = iso_env.box3("foo", "1.0", :virtualbox, architecture: architecture) + Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir, architecture: architecture) + end + + it "deletes the box if it is the only option" do + allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox, architecture]]) + + env[:box_name] = "foo" + + expect(box_collection).to receive(:find). + with("foo", :virtualbox, "1.0", architecture). + and_return(box) + expect(box_collection).to receive(:clean). + with(box.name). + and_return(true) + expect(box).to receive(:destroy!).once + expect(app).to receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "deletes the box with the specified provider if given" do + allow(box_collection).to receive(:all) + .and_return([ + ["foo", "1.0", :virtualbox, architecture], + ["foo", "1.0", :vmware, architecture], + ]) + + env[:box_name] = "foo" + env[:box_provider] = "virtualbox" + + expect(box_collection).to receive(:find).with( + "foo", :virtualbox, "1.0", architecture).and_return(box) + expect(box_collection).to receive(:clean).with(box.name) + .and_return(false) + expect(box).to receive(:destroy!).once + expect(app).to receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "deletes the box with the specified version if given" do + allow(box_collection).to receive(:all) + .and_return([ + ["foo", "1.0", :virtualbox, architecture], + ["foo", "1.1", :virtualbox, architecture], + ]) + + env[:box_name] = "foo" + env[:box_version] = "1.0" + + expect(box_collection).to receive(:find).with( + "foo", :virtualbox, "1.0", architecture).and_return(box) + expect(box_collection).to receive(:clean).with(box.name) + .and_return(false) + expect(box).to receive(:destroy!).once + expect(app).to receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "deletes the box with the specificed version and architecture" do + allow(box_collection).to receive(:all) + .and_return([ + ["foo", "1.0", :virtualbox, architecture], + ["foo", "1.0", :virtualbox, "other-arch"], + ]) + + env[:box_name] = "foo" + env[:box_version] = "1.0" + env[:box_architecture] = architecture + + expect(box_collection).to receive(:find).with( + "foo", :virtualbox, "1.0", architecture).and_return(box) + expect(box_collection).to receive(:clean).with(box.name) + .and_return(false) + expect(box).to receive(:destroy!).once + expect(app).to receive(:call).with(env).once + + subject.call(env) + + expect(env[:box_removed]).to equal(box) + end + + it "errors when box with specified version does not included specified architecture" do + allow(box_collection).to receive(:all) + .and_return([ + ["foo", "1.0", :virtualbox, architecture], + ["foo", "1.0", :virtualbox, "other-arch"], + ]) + + env[:box_name] = "foo" + env[:box_version] = "1.0" + env[:box_architecture] = "unknown-arch" + + expect { + subject.call(env) + }.to raise_error(Vagrant::Errors::BoxRemoveArchitectureNotFound) + end + + it "errors when box with specified version has multiple architectures" do + allow(box_collection).to receive(:all) + .and_return([ + ["foo", "1.0", :virtualbox, architecture], + ["foo", "1.0", :virtualbox, "other-arch"], + ]) + + env[:box_name] = "foo" + env[:box_version] = "1.0" + + expect { + subject.call(env) + }.to raise_error(Vagrant::Errors::BoxRemoveMultiArchitecture) + end + end + context "checking if a box is in use" do def new_entry(name, provider, version, valid=true) Vagrant::MachineIndex::Entry.new.tap do |entry| @@ -117,7 +242,7 @@ def new_entry(name, provider, version, valid=true) it "does delete if the box is not in use" do expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).once expect(box_collection).to receive(:clean).with(box.name) .and_return(true) @@ -133,7 +258,7 @@ def new_entry(name, provider, version, valid=true) with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!).once @@ -149,7 +274,7 @@ def new_entry(name, provider, version, valid=true) with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).never subject.call(env) @@ -164,7 +289,7 @@ def new_entry(name, provider, version, valid=true) with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).never subject.call(env) @@ -178,7 +303,7 @@ def new_entry(name, provider, version, valid=true) with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( - "foo", :virtualbox, "1.0").and_return(box) + "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!) diff --git a/test/unit/vagrant/box_collection_test.rb b/test/unit/vagrant/box_collection_test.rb index 2d2795f4372..16eb6b2f3c6 100644 --- a/test/unit/vagrant/box_collection_test.rb +++ b/test/unit/vagrant/box_collection_test.rb @@ -36,11 +36,11 @@ # Verify some output results = subject.all expect(results.length).to eq(5) - expect(results.include?(["foo", "1.0", :virtualbox])).to be - expect(results.include?(["foo", "1.0", :vmware])).to be - expect(results.include?(["bar", "0", :ec2])).to be - expect(results.include?(["foo/bar", "1.0", :virtualbox])).to be - expect(results.include?(["foo:colon", "1.0", :virtualbox])).to be + expect(results).to include(["foo", "1.0", :virtualbox, nil]) + expect(results).to include(["foo", "1.0", :vmware, nil]) + expect(results).to include(["bar", "0", :ec2, nil]) + expect(results).to include(["foo/bar", "1.0", :virtualbox, nil]) + expect(results).to include(["foo:colon", "1.0", :virtualbox, nil]) end it "should return the boxes and their providers even if box has wrong version" do @@ -57,11 +57,11 @@ # Verify some output results = subject.all expect(results.length).to eq(4) - expect(results.include?(["foo", "1.0", :virtualbox])).not_to be - expect(results.include?(["foo", "1.0", :vmware])).to be - expect(results.include?(["bar", "0", :ec2])).to be - expect(results.include?(["foo/bar", "1.0", :virtualbox])).to be - expect(results.include?(["foo:colon", "1.0", :virtualbox])).to be + expect(results).not_to include(["foo", "1.0", :virtualbox, nil]) + expect(results).to include(["foo", "1.0", :vmware, nil]) + expect(results).to include(["bar", "0", :ec2, nil]) + expect(results).to include(["foo/bar", "1.0", :virtualbox, nil]) + expect(results).to include(["foo:colon", "1.0", :virtualbox, nil]) end it 'does not raise an exception when a file appears in the boxes dir' do @@ -175,7 +175,7 @@ # Make sure the results still exist results = subject.all expect(results.length).to eq(1) - expect(results.include?(["foo", "1.0", :vmware])).to be + expect(results).to include(["foo", "1.0", :vmware, nil]) end it "doesn't remove the directory if a version exists" do @@ -195,7 +195,7 @@ # Make sure the results still exist results = subject.all expect(results.length).to eq(1) - expect(results.include?(["foo", "1.0", :virtualbox])).to be + expect(results).to include(["foo", "1.0", :virtualbox, nil]) end end diff --git a/test/unit/vagrant/box_metadata_test.rb b/test/unit/vagrant/box_metadata_test.rb index b149d2f3113..ce6c438a414 100644 --- a/test/unit/vagrant/box_metadata_test.rb +++ b/test/unit/vagrant/box_metadata_test.rb @@ -9,34 +9,32 @@ include_context "unit" let(:raw) do - <<-RAW - { - "name": "foo", - "description": "bar", - "versions": [ - { - "version": "1.0.0", - "providers": [ - { "name": "virtualbox" }, - { "name": "vmware" } - ] - }, - { - "version": "1.1.5", - "providers": [ - { "name": "virtualbox" } - ] - }, - { - "version": "1.1.0", - "providers": [ - { "name": "virtualbox" }, - { "name": "vmware" } - ] - } - ] - } - RAW + { + name: "foo", + description: "bar", + versions: [ + { + version: "1.0.0", + providers: [ + { name: "virtualbox" }, + { name: "vmware" } + ], + }, + { + version: "1.1.5", + providers: [ + { name: "virtualbox" } + ] + }, + { + version: "1.1.0", + providers: [ + { name: "virtualbox" }, + { name: "vmware" } + ] + } + ] + }.to_json end subject { described_class.new(raw) } @@ -53,9 +51,7 @@ context "with poorly formatted JSON" do let(:raw) { - <<-RAW - { "name": "foo", } - RAW + {name: "foo"}.to_json + "," } it "raises an exception" do @@ -66,15 +62,14 @@ context "with poorly formatted version" do let(:raw) { - <<-RAW - { "name": "foo", - "versions": [ + { + name: "foo", + versions: [ { - "version": "I AM NOT VALID" + version: "I AM NOT VALID" } ] - } - RAW + }.to_json } it "raises an exception" do @@ -124,6 +119,213 @@ ["1.0.0", "1.1.0"]) end end + + context "with architecture" do + let(:raw) do + { + name: "foo", + description: "bar", + versions: [ + { + version: "1.0.0", + providers: [ + { + name: "virtualbox", + default_architecture: true, + architecture: "amd64" + }, + { + name: "virtualbox", + default_architecture: false, + architecture: "arm64" + }, + { + name: "vmware", + default_architecture: true, + architecture: "arm64" + }, + { + name: "vmware", + default_architecture: false, + architecture: "amd64" + } + ], + }, + { + version: "1.1.5", + providers: [ + { + name: "virtualbox", + architecture: "amd64", + default_architecture: true, + } + ] + }, + { + version: "1.1.6", + providers: [ + { + name: "virtualbox", + architecture: "arm64", + default_architecture: true, + }, + ] + }, + { + version: "1.1.0", + providers: [ + { + name: "virtualbox", + architecture: "amd64", + default_architecture: true, + }, + { + name: "vmware", + architecture: "amd64", + default_architecture: true, + } + ] + }, + { + version: "2.0.0", + providers: [ + { + name: "vmware", + architecture: "arm64", + default_architecture: true, + } + ] + } + ] + }.to_json + end + + subject { described_class.new(raw) } + + before { allow(Vagrant::Util::Platform).to receive(:architecture).and_return("amd64") } + + describe "#version" do + it "matches an exact version" do + result = subject.version("1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "matches a constraint with latest matching version" do + result = subject.version(">= 1.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.5") + end + + it "matches complex constraints" do + result = subject.version(">= 0.9, ~> 1.0.0") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + context "with provider filter" do + it "matches the constraint that has the given provider" do + result = subject.version(">= 0", provider: :vmware) + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.0") + end + + it "matches the exact version that has the given provider" do + result = subject.version("1.0.0", provider: :virtualbox) + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "does not match exact version that has given provider but not host architecture" do + result = subject.version("1.1.6", provider: :virtualbox) + expect(result).to be_nil + end + + context "with architecture filter" do + it "matches the exact version that has provider with host architecture when using :auto" do + result = subject.version("1.0.0", provider: :virtualbox, architecture: :auto) + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "matches the exact version that has provider with defined host architecture" do + result = subject.version("1.0.0", provider: :virtualbox, architecture: "arm64") + expect(result).to_not be_nil + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "does not match the exact version that has provider with defined host architecture" do + result = subject.version("1.0.0", provider: :virtualbox, architecture: "ppc64") + expect(result).to be_nil + end + end + end + + context "with architecture filter" do + it "matches a constraint that has the detected host architecture" do + result = subject.version("> 0", architecture: :auto) + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.1.5") + end + + it "matches a constraint that has the provided architecture" do + result = subject.version("> 0", architecture: "arm64") + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("2.0.0") + end + + it "matches exact version that has the provided architecture" do + result = subject.version("1.0.0", architecture: "arm64") + expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result.version).to eq("1.0.0") + end + + it "does not match exact version that does not have provided architecture" do + result = subject.version("2.0.0", architecture: "amd64") + expect(result).to be_nil + end + end + end + + describe "#versions" do + it "returns the versions it contained" do + expect(subject.versions). + to eq(["1.0.0", "1.1.0", "1.1.5", "1.1.6", "2.0.0"]) + end + + context "with provider filter" do + it "filters versions" do + expect(subject.versions(provider: :vmware)). + to eq(["1.0.0", "1.1.0", "2.0.0"]) + end + end + + context "with architecture filter" do + it "filters versions" do + expect(subject.versions(architecture: "arm64")). + to eq(["1.0.0", "1.1.6", "2.0.0"]) + end + + it "returns none when no matching architecture available" do + expect(subject.versions(architecture: "other")). + to be_empty + end + + it "filters based on host architecture when :auto used" do + expect(subject.versions(architecture: :auto)). + to eq(subject.versions(architecture: "amd64")) + end + end + end + + end end describe Vagrant::BoxMetadata::Version do @@ -203,4 +405,28 @@ expect(subject.checksum_type).to be_nil end end + + describe "architecture" do + it "is set properly" do + raw["architecture"] = "test-arch" + + expect(subject.architecture).to eq("test-arch") + end + + it "is nil if not set" do + expect(subject.architecture).to be_nil + end + end + + describe "#architecture_support?" do + it "is false if architecture is not supported" do + expect(subject.architecture_support?).to be(false) + end + + it "is true if architecture is supported" do + raw["default_architecture"] = false + + expect(subject.architecture_support?).to be(true) + end + end end diff --git a/test/unit/vagrant/box_test.rb b/test/unit/vagrant/box_test.rb index 13be39d1281..941823a2b1a 100644 --- a/test/unit/vagrant/box_test.rb +++ b/test/unit/vagrant/box_test.rb @@ -19,6 +19,7 @@ let(:name) { "foo" } let(:provider) { :virtualbox } let(:version) { "1.0" } + let(:architecture) { "test-architecture" } let(:directory) { environment.box3("foo", "1.0", :virtualbox) } subject { described_class.new(name, provider, version, directory) } @@ -209,6 +210,154 @@ expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end + + context "with architecture" do + subject do + described_class.new( + name, provider, version, directory, + architecture: architecture, + metadata_url: "foo" + ) + end + + it "raises an exception if no metadata_url is set" do + subject = described_class.new( + name, provider, version, directory, + architecture: architecture, + ) + + expect { subject.has_update?("> 0") }. + to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) + end + + it "returns nil if there is no update" do + metadata = Vagrant::BoxMetadata.new( + { + name: "foo", + versions: [ { version: "1.0" } ] + }.to_json + ) + allow(subject).to receive(:load_metadata).and_return(metadata) + + expect(subject.has_update?).to be_nil + end + + it "returns the updated box info if there is an update available" do + metadata = Vagrant::BoxMetadata.new( + { + name: "foo", + versions: [ + {version: "1.0"}, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: architecture, + } + ] + } + ] + }.to_json + ) + + allow(subject).to receive(:load_metadata).and_return(metadata) + + result = subject.has_update? + expect(result).to_not be_nil + + expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) + expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) + + expect(result[0].name).to eq("foo") + expect(result[1].version).to eq("1.1") + expect(result[2].url).to eq("bar") + end + + it "returns nil if update does not support architecture" do + metadata = Vagrant::BoxMetadata.new( + { + name: "foo", + versions: [ + {version: "1.0"}, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "other-architecture", + } + ] + } + ] + }.to_json + ) + + allow(subject).to receive(:load_metadata).and_return(metadata) + + result = subject.has_update? + expect(result).to be_nil + end + + it "returns the updated box info within constraints" do + metadata = Vagrant::BoxMetadata.new( + { + name: "foo", + versions: [ + { + version: "1.0", + }, + { + version: "1.1", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: architecture + }, + ] + }, + { + version: "1.2", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: "other-architecture", + }, + ] + }, + { + version: "1.4", + providers: [ + { + name: "virtualbox", + url: "bar", + architecture: architecture + } + ] + } + ] + }.to_json + ) + + allow(subject).to receive(:load_metadata).and_return(metadata) + + result = subject.has_update?(">= 1.1, < 1.4") + expect(result).to_not be_nil + + expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) + expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) + expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) + + expect(result[0].name).to eq("foo") + expect(result[1].version).to eq("1.1") + expect(result[2].url).to eq("bar") + end + end end context "#automatic_update_check_allowed?" do @@ -236,12 +385,13 @@ context "#in_use?" do let(:index) { [] } - def new_entry(name, provider, version) + def new_entry(name, provider, version, architecture=nil) Vagrant::MachineIndex::Entry.new.tap do |entry| entry.extra_data["box"] = { "name" => name, "provider" => provider, "version" => version, + "architecture" => architecture, } end end @@ -261,6 +411,27 @@ def new_entry(name, provider, version) expect(subject.in_use?(index)).to eq([matching]) end + + context "with architecture information" do + subject { described_class.new(name, provider, version, directory, architecture: architecture) } + + it "returns nil if the index has no matching entries" do + index << new_entry("foo", "bar", "1.0", "amd64") + index << new_entry("foo", "baz", "1.2", "arm64") + index << new_entry(name, provider.to_s, version, "random-arch") + + expect(subject).to_not be_in_use(index) + end + + it "returns matching entries if they exist" do + matching = new_entry(name, provider.to_s, version, architecture) + index << new_entry("foo", "bar", "1.0", "amd64") + index << matching + index << new_entry("foo", "baz", "1.2") + + expect(subject.in_use?(index)).to eq([matching]) + end + end end context "#load_metadata" do diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index 7158b034596..12ba865bad4 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -26,6 +26,7 @@ double("box").tap do |b| allow(b).to receive(:name).and_return("foo") allow(b).to receive(:provider).and_return(:dummy) + allow(b).to receive(:architecture) allow(b).to receive(:version).and_return("1.0") end end @@ -580,6 +581,7 @@ def detect?(machine) box = double("box") allow(box).to receive(:name).and_return("foo") allow(box).to receive(:provider).and_return(:bar) + allow(box).to receive(:architecture) allow(box).to receive(:version).and_return("1.2.3") subject.box = box @@ -599,6 +601,7 @@ def detect?(machine) expect(entry.extra_data["box"]).to eq({ "name" => box.name, "provider" => box.provider.to_s, + "architecture" => nil, "version" => box.version, }) env.machine_index.release(entry) diff --git a/test/unit/vagrant/util/platform_test.rb b/test/unit/vagrant/util/platform_test.rb index d06c38dbec9..acf01f6080c 100644 --- a/test/unit/vagrant/util/platform_test.rb +++ b/test/unit/vagrant/util/platform_test.rb @@ -11,6 +11,70 @@ after { described_class.reset! } subject { described_class } + describe "#architecture" do + let(:cpu_string) { "unknown" } + + before do + allow(RbConfig::CONFIG). + to receive(:[]).with("target_cpu"). + and_return(cpu_string) + end + + context "when cpu is x86_64" do + let(:cpu_string) { "x86_64" } + + it "should be mapped to amd64" do + expect(described_class.architecture).to eq("amd64") + end + end + + context "when cpu is i386" do + let(:cpu_string) { "i386" } + + it "should be mapped to 386" do + expect(described_class.architecture).to eq("386") + end + end + + context "when cpu is arm64" do + let(:cpu_string) { "arm64" } + + it "should be arm64" do + expect(described_class.architecture).to eq("arm64") + end + end + + context "when cpu is aarch64" do + let(:cpu_string) { "aarch64" } + + it "should be mapped to arm64" do + expect(described_class.architecture).to eq("arm64") + end + end + + context "when cpu is unmapped value" do + let(:cpu_string) { "custom-cpu" } + + it "should be returned as-is" do + expect(described_class.architecture).to eq(cpu_string) + end + end + + context "when environment variable override is set" do + let(:host_override) { "custom-host-override" } + + before do + allow(ENV).to receive(:[]). + with("VAGRANT_HOST_ARCHITECTURE"). + and_return(host_override) + end + + it "should return the custom override" do + expect(subject.architecture).to eq(host_override) + end + end + end + describe "#cygwin_path" do let(:path) { "C:\\msys2\\home\\vagrant" } let(:updated_path) { "/home/vagrant" } diff --git a/test/unit/vagrant/vagrantfile_test.rb b/test/unit/vagrant/vagrantfile_test.rb index 4cf75d7055e..f2ec25db7ce 100644 --- a/test/unit/vagrant/vagrantfile_test.rb +++ b/test/unit/vagrant/vagrantfile_test.rb @@ -266,6 +266,44 @@ def self.usable?(raise_error=false) expect(box.version).to eq("1.3") end + it "configures with the custom box architecture" do + register_provider("foo") + configure do |config| + config.vm.box = "base" + config.vm.box_architecture = "custom-arch" + end + + results = subject.machine_config(:default, :foo, boxes) + config = results[:config] + expect(config.vm.box).to eq("base") + expect(config.vm.box_architecture).to eq("custom-arch") + end + + it "configures with the default box architecture" do + register_provider("foo") + configure do |config| + config.vm.box = "base" + end + + results = subject.machine_config(:default, :foo, boxes) + config = results[:config] + expect(config.vm.box).to eq("base") + expect(config.vm.box_architecture).to eq(:auto) + end + + it "configures box architecture to nil" do + register_provider("foo") + configure do |config| + config.vm.box = "base" + config.vm.box_architecture = nil + end + + results = subject.machine_config(:default, :foo, boxes) + config = results[:config] + expect(config.vm.box).to eq("base") + expect(config.vm.box_architecture).to be_nil + end + it "configures with box config of other supported formats" do register_provider("foo", nil, box_format: "bar") From 66435a8a98b1c449f82d2c0b418b191e8025fddc Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 28 Jul 2023 14:12:03 -0700 Subject: [PATCH 06/11] Add architecture to docs --- website/content/docs/boxes/box_repository.mdx | 7 +++++-- website/content/docs/vagrantfile/machine_settings.mdx | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/website/content/docs/boxes/box_repository.mdx b/website/content/docs/boxes/box_repository.mdx index bdd57662db9..3c3c2668216 100644 --- a/website/content/docs/boxes/box_repository.mdx +++ b/website/content/docs/boxes/box_repository.mdx @@ -50,7 +50,9 @@ It is a JSON document, structured in the following way: "name": "virtualbox", "url": "http://example.com/bionic64_010_virtualbox.box", "checksum_type": "sha1", - "checksum": "foo" + "checksum": "foo", + "architecture": "amd64", + "default_architecture": true } ] } @@ -59,7 +61,8 @@ It is a JSON document, structured in the following way: ``` As you can see, the JSON document can describe multiple versions of a box, -multiple providers, and can add/remove providers in different versions. +multiple providers, and can add/remove providers/architectures in different +versions. This JSON file can be passed directly to `vagrant box add` from the local filesystem using a file path or via a URL, and Vagrant will diff --git a/website/content/docs/vagrantfile/machine_settings.mdx b/website/content/docs/vagrantfile/machine_settings.mdx index e0bed5580fe..62e312b1f61 100644 --- a/website/content/docs/vagrantfile/machine_settings.mdx +++ b/website/content/docs/vagrantfile/machine_settings.mdx @@ -42,6 +42,13 @@ the name of the synced folder plugin. of an installed box or a shorthand name of a box in [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud). +- `config.vm.box_architecture` (string) - The architecture of the box to be used. + Supported architecture values: "386", "amd64", "arm", "arm64", "ppc64le", "ppc64", + "mips64le", "mips64", "mipsle", "mips", and "s390x". The special value `:auto` will + detect the host architecture and fetch the appropriate box, if available. When the + value is set to `nil`, it will fetch the box flagged as the default architecture. + Defaults to `:auto`. + - `config.vm.box_check_update` (boolean) - If true, Vagrant will check for updates to the configured box on every `vagrant up`. If an update is found, Vagrant will tell the user. By default this is true. Updates will only be checked From 9ef5c495987aa24fe981f9a6f77dc9b764a0d177 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Fri, 15 Sep 2023 17:30:32 -0700 Subject: [PATCH 07/11] Use api endpoint for expanded urls When expanding the box url, prefer the API endpoint which is updated to include provider architecture information. Test the API endpoint and the legacy endpoint and use which ever is valid, with the API taking precedence. This allows Vagrant to continue with non Vagrant Cloud servers that do not implement the API endpoint. --- lib/vagrant/action/builtin/box_add.rb | 37 ++++++++-- .../vagrant/action/builtin/box_add_test.rb | 72 +++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index bb73a5345ae..0cb5ab4dcad 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -68,17 +68,29 @@ def call(env) # If we received a shorthand URL ("mitchellh/precise64"), # then expand it properly. expanded = false - url.each_index do |i| - next if url[i] !~ /^[^\/]+\/[^\/]+$/ + # Mark if only a single url entry was provided + single_entry = url.size == 1 - if !File.file?(url[i]) + url = url.map do |url_entry| + if url_entry =~ /^[^\/]+\/[^\/]+$/ && !File.file?(url_entry) server = Vagrant.server_url env[:box_server_url] raise Errors::BoxServerNotSet if !server expanded = true - url[i] = "#{server}/#{url[i]}" + # If only a single entry, expand to both the API endpoint and + # the direct shorthand endpoint. + if single_entry + url_entry = [ + "#{server}/api/v2/vagrant/#{url_entry}", + "#{server}/#{url_entry}" + ] + else + url_entry = "#{server}/#{url_entry}" + end end - end + + url_entry + end.flatten # Call the hook to transform URLs into authenticated URLs. # In the case we don't have a plugin that does this, then it @@ -99,6 +111,21 @@ def call(env) end end + # If only a single entry was provided, and it was expanded, + # inspect the metadata check results and extract the one that + # was successful, with preference to the API endpoint + if single_entry && expanded + idx = is_metadata_results.index { |v| v === true } + # If none of the urls were successful, set the index + # as the last entry + idx = is_metadata_results.size - 1 if idx.nil? + + # Now reset collections with single value + is_metadata_results = [is_metadata_results[idx]] + authed_urls = [authed_urls[idx]] + url = [url[idx]] + end + if expanded && url.length == 1 is_error = is_metadata_results.find do |b| b.is_a?(Errors::DownloaderError) diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 7e00b3ba8d4..642e053f1ad 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -557,6 +557,78 @@ def with_web_server(path, **opts) FileUtils.rm_rf(td) end + it "adds from API endpoint when available" do + box_path = iso_env.box2_file(:virtualbox) + td = Pathname.new(Dir.mktmpdir("vagrant-test-box-api-endpoint")) + tf = td.join("mitchellh", "precise64.json") + tf.dirname.mkpath + tf.open("w") do |f| + f.write(<<-RAW) + { + "name": "mitchellh/precise64", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.7", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + end + apif = td.join("api", "v2", "vagrant", "mitchellh", "precise64.json") + apif.dirname.mkpath + apif.open("w") do |f| + f.write(<<-RAW) + { + "name": "mitchellh/precise64", + "versions": [ + { + "version": "0.5" + }, + { + "version": "0.8", + "providers": [ + { + "name": "virtualbox", + "url": "#{box_path}" + } + ] + } + ] + } + RAW + end + + with_web_server(tf.dirname) do |port| + url = "http://127.0.0.1:#{port}" + env[:box_url] = "mitchellh/precise64.json" + env[:box_server_url] = url + + expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| + expect(name).to eq("mitchellh/precise64") + expect(version).to eq("0.8") + expect(checksum(path)).to eq(checksum(box_path)) + expect(opts[:metadata_url]).to eq( + "#{url}/api/v2/vagrant/#{env[:box_url]}") + true + }.and_return(box) + + expect(app).to receive(:call).with(env) + + subject.call(env) + end + + FileUtils.rm_rf(td) + end + it "add from shorthand path with configured server url" do box_path = iso_env.box2_file(:virtualbox) td = Pathname.new(Dir.mktmpdir("vagrant-test-box-add-server-url")) From 3ea1beca9e2e3524f00b80f8999d4c21eed4c9ec Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Mon, 18 Sep 2023 15:41:28 -0700 Subject: [PATCH 08/11] Skip box directories without metadata file --- lib/vagrant/box_collection.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index 56c5de2c0f9..a4ee6d91834 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -357,6 +357,10 @@ def find(name, providers, version, box_architecture=:auto) end final_dir = arch_dir if arch_dir && arch_dir.directory? + # If the final directory does not contain a `metadata.json` file, then + # skip this directory + next if !final_dir.join("metadata.json").exist? + @logger.info("Box found: #{name} (#{provider})") metadata_url = nil From c8a7989b887090a79b4216d921222baf2230c46c Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Mon, 25 Sep 2023 10:51:36 -0700 Subject: [PATCH 09/11] Adjust internal layout to allow downgrading With the initial layout of `provider/architecture`, after installing a box with architecture support downgrading Vagrant would result in it being unable to process the box collection. Swapping the layout to be `architecture/provider` allows downgrades to still properly process the box collection. --- lib/vagrant/action/builtin/box_add.rb | 4 +- lib/vagrant/box_collection.rb | 128 +++++++++++------- test/unit/support/isolated_environment.rb | 3 +- .../vagrant/action/builtin/box_add_test.rb | 9 +- 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index 0cb5ab4dcad..9e83749208f 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -338,7 +338,7 @@ def add_from_metadata(url, env, expanded) env, checksum: metadata_provider.checksum, checksum_type: metadata_provider.checksum_type, - architecture: architecture, + architecture: metadata_provider.architecture, ) end @@ -420,7 +420,7 @@ def box_add(urls, name, version, provider, md_url, env, **opts) force: env[:box_force], metadata_url: md_url, providers: provider, - architecture: env[:box_architecture] + architecture: opts[:architecture] ) ensure # Make sure we delete the temporary file after we add it, diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index a4ee6d91834..117d31f3abd 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -170,28 +170,26 @@ def add(path, name, version, **opts) @logger.debug("Box directory: #{box_dir}") # This is the final directory we'll move it to - provider_dir = box_dir.join(provider.to_s) - final_dir = provider_dir - @logger.debug("Provider directory: #{provider_dir}") - # If architecture is set, unpack into architecture specific directory if architecture arch = architecture arch = Util::Platform.architecture if architecture == :auto - final_dir = provider_dir.join(arch) + box_dir = box_dir.join(arch) end + provider_dir = box_dir.join(provider.to_s) + @logger.debug("Provider directory: #{provider_dir}") - if final_dir.exist? + if provider_dir.exist? @logger.debug("Removing existing provider directory...") - final_dir.rmtree + provider_dir.rmtree end # Move to final destination - final_dir.mkpath + provider_dir.mkpath # Recursively move individual files from the temporary directory # to the final location. We do this instead of moving the entire # directory to avoid issues on Windows. [GH-1424] - copy_pairs = [[final_temp_dir, final_dir]] + copy_pairs = [[final_temp_dir, provider_dir]] while !copy_pairs.empty? from, to = copy_pairs.shift from.children(true).each do |f| @@ -248,34 +246,52 @@ def all next if versiondir.basename.to_s.start_with?(".") version = versiondir.basename.to_s + # Ensure version of box is correct before continuing + if !Gem::Version.correct?(version) + ui = Vagrant::UI::Prefixed.new(Vagrant::UI::Colored.new, "vagrant") + ui.warn(I18n.t("vagrant.box_version_malformed", + version: version, box_name: box_name)) + @logger.debug("Invalid version #{version} for box #{box_name}") + next + end - versiondir.children(true).each do |provider| - # Ensure version of box is correct before continuing - if !Gem::Version.correct?(version) - ui = Vagrant::UI::Prefixed.new(Vagrant::UI::Colored.new, "vagrant") - ui.warn(I18n.t("vagrant.box_version_malformed", - version: version, box_name: box_name)) - @logger.debug("Invalid version #{version} for box #{box_name}") + versiondir.children(true).each do |architecture_or_provider| + # If the entry is not a directory, it is invalid and should be ignored + if !architecture_or_provider.directory? + @logger.debug("Invalid box #{box_name} (v#{version}) - invalid item: #{architecture_or_provider}") next end - # Verify this is a potentially valid box. If it looks - # correct enough then include it. - if provider.directory? && provider.join("metadata.json").file? - provider_name = provider.basename.to_s.to_sym - @logger.debug("Box: #{box_name} (#{provider_name}, #{version})") - results << [box_name, version, provider_name, nil] - elsif provider.directory? - provider.children(true).each do |architecture| + # Now the directory can be assumed to be the architecture + architecture_name = architecture_or_provider.basename.to_s.to_sym + + # Cycle through directories to find providers + architecture_or_provider.children(true).each do |provider| + if !provider.directory? + @logger.debug("Invalid box #{box_name} (v#{version}, #{architecture_name}) - invalid item: #{provider}") + next + end + + # If the entry contains a metadata file, add it + if provider.join("metadata.json").file? provider_name = provider.basename.to_s.to_sym - if architecture.directory? && architecture.join("metadata.json").file? - architecture_name = architecture.basename.to_s.to_sym - @logger.debug("Box: #{box_name} (#{provider_name} (#{architecture_name}), #{version})") - results << [box_name, version, provider_name, architecture_name] - end + @logger.debug("Box: #{box_name} (#{provider_name} (#{architecture_name}), #{version})") + results << [box_name, version, provider_name, architecture_name] + end + end + + # If the base entry contains a metadata file, then it was + # added prior to architecture support and is a provider directory. + # If it contains a metadata file, include it with the results only + # if an entry hasn't already been included for the local system's + # architecture + if architecture_or_provider.join("metadata.json").file? + provider_name = architecture_or_provider.basename.to_s.to_sym + if results.include?([box_name, version, provider_name, Util::Platform.architecture.to_sym]) + next end - else - @logger.debug("Invalid box #{box_name}, ignoring: #{provider}") + @logger.debug("Box: #{box_name} (#{provider_name}, #{version})") + results << [box_name, version, provider_name, nil] end end end @@ -341,25 +357,41 @@ def find(name, providers, version, box_architecture=:auto) end versiondir = box_directory.join(version_dir_map[v.to_s]) + providers.each do |provider| - provider_dir = versiondir.join(provider.to_s) - next if !provider_dir.directory? - # If architecture is defined then the box should be within - # a subdirectory. However, if box_architecture value is :auto - # and the box provider directory exists but the architecture - # directory does not, we will use the box provider directory. This - # allows Vagrant to work correctly with boxes which were added - # prior to the addition of architecture support - final_dir = provider_dir - if architecture - arch_dir = provider_dir.join(architecture.to_s) - next if !arch_dir.directory? && box_architecture != :auto + providerdir = versiondir.join(architecture.to_s).join(provider.to_s) + + # If the architecture was automatically set to the host + # architecture, then a match on the architecture subdirectory + # or the provider directory (which is a box install prior to + # architecture support) is valid + if box_architecture == :auto + if !providerdir.directory? + providerdir = versiondir.join(provider.to_s) + end + + if providerdir.join("metadata.json").file? + @logger.info("Box found: #{name} (#{provider})") + + metadata_url = nil + metadata_url_file = box_directory.join("metadata_url") + metadata_url = metadata_url_file.read if metadata_url_file.file? + + if metadata_url && @hook + hook_env = @hook.call( + :authenticate_box_url, box_urls: [metadata_url]) + metadata_url = hook_env[:box_urls].first + end + + return Box.new( + name, provider, version_dir_map[v.to_s], providerdir, + architecture: architecture, metadata_url: metadata_url, hook: @hook + ) + end end - final_dir = arch_dir if arch_dir && arch_dir.directory? - # If the final directory does not contain a `metadata.json` file, then - # skip this directory - next if !final_dir.join("metadata.json").exist? + # If there is no metadata file found, skip + next if !providerdir.join("metadata.json").file? @logger.info("Box found: #{name} (#{provider})") @@ -374,7 +406,7 @@ def find(name, providers, version, box_architecture=:auto) end return Box.new( - name, provider, version_dir_map[v.to_s], final_dir, + name, provider, version_dir_map[v.to_s], providerdir, architecture: architecture, metadata_url: metadata_url, hook: @hook ) end diff --git a/test/unit/support/isolated_environment.rb b/test/unit/support/isolated_environment.rb index 2550a81240b..b1ecd47145a 100644 --- a/test/unit/support/isolated_environment.rb +++ b/test/unit/support/isolated_environment.rb @@ -100,8 +100,9 @@ def box2(name, provider, options=nil) # @param [String] provider # @return [Pathname] def box3(name, version, provider, **opts) - args = [name, version, provider.to_s] + args = [name, version] args << opts[:architecture].to_s if opts[:architecture] + args << provider.to_s # Create the directory for the box box_dir = boxes_dir.join(*args) diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 642e053f1ad..c3366d504de 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -1024,7 +1024,7 @@ def with_web_server(path, **opts) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") - expect(opts[:architecture]).to eq(:auto) + expect(opts[:architecture]).to eq("test-arch") true }.and_return(box) @@ -1072,7 +1072,7 @@ def with_web_server(path, **opts) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") - expect(opts[:architecture]).to eq(:auto) + expect(opts[:architecture]).to eq("unknown") true }.and_return(box) @@ -1130,6 +1130,7 @@ def with_web_server(path, **opts) end it "adds the latest version of a box with only one provider and default architecture" do + allow(Vagrant::Util::Platform).to receive(:architecture).and_return("default-arch") box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( @@ -1145,7 +1146,7 @@ def with_web_server(path, **opts) { name: "virtualbox", url: "#{box_path}", - architecture: "amd64", + architecture: "default-arch", default_architecture: true, }, { @@ -1168,7 +1169,7 @@ def with_web_server(path, **opts) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") - expect(opts[:architecture]).to be_nil + expect(opts[:architecture]).to eq("default-arch") true }.and_return(box) From e7191131659b9857f46ed2414144a2f32b3de5ba Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Mon, 25 Sep 2023 10:54:49 -0700 Subject: [PATCH 10/11] Update constraint on vagrant_cloud library --- vagrant.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vagrant.gemspec b/vagrant.gemspec index 1425a35bb87..80b54baccbc 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| s.add_dependency "rexml", "~> 3.2" s.add_dependency "rgl", "~> 0.5.10" s.add_dependency "rubyzip", "~> 2.3.2" - s.add_dependency "vagrant_cloud", "> 3.0.5", "< 3.1" + s.add_dependency "vagrant_cloud", "~> 3.1.0" s.add_dependency "wdm", "~> 0.1.1" s.add_dependency "winrm", ">= 2.3.6", "< 3.0" s.add_dependency "winrm-elevated", ">= 1.2.3", "< 2.0" From 74b4a2b1f58177d79f608c34a3decb3388d045b6 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 26 Sep 2023 16:20:37 -0700 Subject: [PATCH 11/11] Adjust installation for unknown default architecture When the reported architecture is unknown and the provider is listed as the default architecture, add the box without architecture information so it is installed without architecture information on the path within the collection. --- lib/vagrant/action/builtin/box_add.rb | 13 ++++++++++++- test/unit/vagrant/action/builtin/box_add_test.rb | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index 9e83749208f..f04b8b86127 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -329,6 +329,17 @@ def add_from_metadata(url, env, expanded) provider_url = authed_urls[0] end + # The architecture name used when adding the box should be + # the value extracted from the metadata provider + arch_name = metadata_provider.architecture + + # In the special case where the architecture name is "unknown" and + # it is listed as the default architecture, unset the architecture + # name so it is installed without architecture information + if arch_name == "unknown" && metadata_provider.default_architecture + arch_name = nil + end + box_add( [[provider_url, metadata_provider.url]], metadata.name, @@ -338,7 +349,7 @@ def add_from_metadata(url, env, expanded) env, checksum: metadata_provider.checksum, checksum_type: metadata_provider.checksum_type, - architecture: metadata_provider.architecture, + architecture: arch_name, ) end diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index c3366d504de..704e7626ea4 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -1033,7 +1033,7 @@ def with_web_server(path, **opts) subject.call(env) end - it "adds the latest version of a box with only one provider and no unknown architecture set as default" do + it "adds the latest version of a box with only one provider and unknown architecture set as default" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( @@ -1072,7 +1072,7 @@ def with_web_server(path, **opts) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") - expect(opts[:architecture]).to eq("unknown") + expect(opts[:architecture]).to be_nil true }.and_return(box)