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.
256 lines
7.5 KiB
Ruby
256 lines
7.5 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
require "json"
|
|
|
|
module Vagrant
|
|
# BoxMetadata represents metadata about a box, including the name
|
|
# it should have, a description of it, the versions it has, and
|
|
# more.
|
|
class BoxMetadata
|
|
|
|
autoload :Remote, "vagrant/box_metadata/remote"
|
|
|
|
# The name that the box should be if it is added.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :name
|
|
|
|
# The long-form human-readable description of a box.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :description
|
|
|
|
# Loads the metadata associated with the box from the given
|
|
# IO.
|
|
#
|
|
# @param [IO] io An IO object to read the metadata from.
|
|
def initialize(io, **_)
|
|
begin
|
|
@raw = JSON.load(io)
|
|
rescue JSON::ParserError => e
|
|
raise Errors::BoxMetadataMalformed,
|
|
error: e.to_s
|
|
end
|
|
|
|
@raw ||= {}
|
|
@name = @raw["name"]
|
|
@description = @raw["description"]
|
|
@version_map = (@raw["versions"] || []).map do |v|
|
|
begin
|
|
[Gem::Version.new(v["version"]), Version.new(v)]
|
|
rescue ArgumentError
|
|
raise Errors::BoxMetadataMalformedVersion,
|
|
version: v["version"].to_s
|
|
end
|
|
end
|
|
@version_map = Hash[@version_map]
|
|
end
|
|
|
|
# Returns data about a single version that is included in this
|
|
# metadata.
|
|
#
|
|
# @param [String] version The version to return, this can also
|
|
# be a constraint.
|
|
# @return [Version] The matching version or nil if a matching
|
|
# version was not found.
|
|
def version(version, **opts)
|
|
requirements = version.split(",").map do |v|
|
|
Gem::Requirement.new(v.strip)
|
|
end
|
|
|
|
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_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
|
|
|
|
nil
|
|
end
|
|
|
|
# Returns all the versions supported by this metadata. These
|
|
# versions are sorted so the last element of the list is the
|
|
# latest version. Optionally filter versions by a matching
|
|
# provider.
|
|
#
|
|
# @return[Array<String>]
|
|
def versions(**opts)
|
|
architecture = opts[:architecture]
|
|
provider = opts[:provider].to_sym if opts[:provider]
|
|
|
|
# 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.
|
|
class Version
|
|
# The version that this Version object represents.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :version
|
|
|
|
def initialize(raw=nil, **_)
|
|
return if !raw
|
|
|
|
@version = raw["version"]
|
|
@providers = raw.fetch("providers", []).map do |data|
|
|
Provider.new(data)
|
|
end
|
|
@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, 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<Symbol>]
|
|
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
|
|
|
|
# Provider represents a single provider-specific box available
|
|
# for a version for a box.
|
|
class Provider
|
|
# The name of the provider.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :name
|
|
|
|
# The URL of the box.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :url
|
|
|
|
# The checksum value for this box, if any.
|
|
#
|
|
# @return [String]
|
|
attr_accessor :checksum
|
|
|
|
# The type of checksum (if any) associated with this 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
|
|
end
|