vagrant/plugins/provisioners/container/client.rb
Chris Roberts ea25996b21
Update Vagrant behavior outside of installers
Remove customized require behaviors and modify the bin executable
to check for missing tools that Vagrant expects to exist when
running outside of an installer.
2025-04-02 11:40:17 -07:00

206 lines
6.4 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require 'digest/sha1'
module VagrantPlugins
module ContainerProvisioner
class Client
def initialize(machine, container_command)
@machine = machine
@container_command = container_command
end
# Build an image given a path to a Dockerfile
#
# @param [Array<Array<String, Hash>>] - Path and options to the Dockerfile to pass to
# container build command
def build_images(images)
@machine.communicate.tap do |comm|
images.each do |path, opts|
@machine.ui.info(I18n.t("vagrant.container_building_single", path: path))
comm.sudo("#{@container_command} build #{opts[:args]} #{path}") do |type, data|
handle_comm(type, data)
end
end
end
end
# Pull image given a list of images
#
# @param [String] - Image name
def pull_images(*images)
@machine.communicate.tap do |comm|
images.each do |image|
@machine.ui.info(I18n.t("vagrant.container_pulling_single", name: image))
comm.sudo("#{@container_command} pull #{image}") do |type, data|
handle_comm(type, data)
end
end
end
end
def run(containers)
containers.each do |name, config|
cids_dir = "/var/lib/vagrant/cids"
config[:cidfile] ||= "#{cids_dir}/#{Digest::SHA1.hexdigest name}"
@machine.ui.info(I18n.t("vagrant.container_running", name: name))
@machine.communicate.sudo("mkdir -p #{cids_dir}")
run_container({
name: name,
original_name: name,
}.merge(config))
end
end
# Run a OCI container. If the container does not exist it will be
# created. If the image is stale it will be recreated and restarted
def run_container(config)
raise "Container's cidfile was not provided!" if !config[:cidfile]
id = "$(cat #{config[:cidfile]})"
if container_exists?(id)
if container_args_changed?(config)
@machine.ui.info(I18n.t("vagrant.container_restarting_container_args",
name: config[:name],
))
stop_container(id)
create_container(config)
elsif container_image_changed?(config)
@machine.ui.info(I18n.t("vagrant.container_restarting_container_image",
name: config[:name],
))
stop_container(id)
create_container(config)
else
start_container(id)
end
else
create_container(config)
end
end
def container_exists?(id)
lookup_container(id, true)
end
# Start container
#
# @param String - Image id
def start_container(id)
if !container_running?(id)
@machine.communicate.sudo("#{@container_command} start #{id}")
end
end
# Stop and remove container
#
# @param String - Image id
def stop_container(id)
@machine.communicate.sudo %[
#{@container_command} stop #{id}
#{@container_command} rm #{id}
]
end
def container_running?(id)
lookup_container(id)
end
def container_image_changed?(config)
# Returns true if there is a container running with the given :name,
# and the container is not using the latest :image.
# Here, "<cmd> inspect <container>" returns the id of the image
# that the container is using. We check that the latest image that
# has been built with that name (:image) matches the one that the
# container is running.
cmd = ("#{@container_command} inspect --format='{{.Image}}' #{config[:name]} |" +
" grep $(#{@container_command} images -q #{config[:image]})")
return !@machine.communicate.test(cmd)
end
def container_args_changed?(config)
path = container_data_path(config)
return true if !path.exist?
args = container_run_args(config)
sha = Digest::SHA1.hexdigest(args)
return true if path.read.chomp != sha
return false
end
def create_container(config)
args = container_run_args(config)
@machine.communicate.sudo %[rm -f "#{config[:cidfile]}"]
@machine.communicate.sudo %[#{@container_command} run #{args}]
sha = Digest::SHA1.hexdigest(args)
container_data_path(config).open("w+") do |f|
f.write(sha)
end
end
# Looks up if a container with a given id exists using the
# `ps` command. Returns Boolean
#
# @param String - Image id
def lookup_container(id, list_all = false)
container_ps = "sudo #{@container_command} ps -q"
container_ps << " -a" if list_all
@machine.communicate.tap do |comm|
return comm.test("#{container_ps} --no-trunc | grep -wFq #{id}")
end
end
def container_name(config)
name = config[:name]
# If the name is the automatically assigned name, then
# replace the "/" with "-" because "/" is not a valid
# character for a container name.
name = name.gsub("/", "-").gsub(":", "-") if name == config[:original_name]
name
end
# Compiles run arguments to be appended to command string.
# Returns String
def container_run_args(config)
name = container_name(config)
args = "--cidfile=#{config[:cidfile]} "
args << "-d " if config[:daemonize]
args << "--name #{name} " if name && config[:auto_assign_name]
args << "--restart=#{config[:restart]}" if config[:restart]
args << " #{config[:args]}" if config[:args]
"#{args} #{config[:image]} #{config[:cmd]}".strip
end
def container_data_path(config)
name = container_name(config)
@machine.data_dir.join("#{@container_command}-#{name}")
end
protected
# This handles outputting the communication data back to the UI
def handle_comm(type, data)
if [:stderr, :stdout].include?(type)
# Clear out the newline since we add one
data = data.chomp
return if data.empty?
options = {}
#options[:color] = color if !config.keep_color
@machine.ui.info(data.chomp, **options)
end
end
end
end
end