Chris Roberts 51adb12547 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.
2023-09-14 16:15:03 -07:00

182 lines
7.1 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require "log4r"
module Vagrant
module Action
module Builtin
# This middleware will remove a box for a given provider.
class BoxRemove
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::action::builtin::box_remove")
end
def call(env)
box_name = env[:box_name]
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]
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
# If there's no box info, then the box doesn't exist here
if box_info.empty?
raise Errors::BoxRemoveNotFound, name: box_name
end
# 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,
versions: box_info.keys.sort.map { |k| " * #{k}" }.join("\n")
end
# 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
# 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
# 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
# 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
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)
end
end
end
end
end