lib/kontena/cli/stacks/common.rb in kontena-cli-1.4.0.pre6 vs lib/kontena/cli/stacks/common.rb in kontena-cli-1.4.0.pre7

- old
+ new

@@ -1,159 +1,174 @@ require_relative 'yaml/reader' require_relative '../services/services_helper' require_relative 'service_generator_v2' require_relative '../../stacks_client' +require_relative 'yaml/stack_file_loader' require 'yaml' module Kontena::Cli::Stacks module Common include Kontena::Cli::Services::ServicesHelper - module StackNameParam - attr_accessor :stack_version + # @return [StackFileLoader] a loader for the stack origin defined through command-line options + def loader + @loader ||= loader_class.for(source) + end + # @return [YAML::Reader] a YAML reader for the target file + def reader + @reader ||= loader.reader + end + + # Stack name read from -n parameter or the stack file + # @return [String] + def stack_name + @stack_name ||= (self.respond_to?(:name) && self.name) ? self.name : loader.stack_name.stack + end + + # An accessor to the YAML Reader outcome. Passes parent name, values from command line and + # the stackname to the reader. + # + # @return [Hash] + def stack + @stack ||= reader.execute( + name: stack_name, + parent_name: self.respond_to?(:parent_name) ? self.parent_name : nil, + values: (self.respond_to?(:values_from_options) ? self.values_from_options : {}) + ) + end + + # @return [Class] an accessor to StackFileLoader constant, for testing purposes + def loader_class + ::Kontena::Cli::Stacks::YAML::StackFileLoader + end + + module RegistryNameParam + def stack_name + @stack_name ||= Kontena::Cli::Stacks::StackName.new(source) + end + def self.included(where) - where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version" do |name| - if name.include?(':') - name, @stack_version = name.split(':',2 ) - end - name - end + where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version", attribute_name: :source end end + module StackNameParam + # Include to add a STACK_NAME parameter + def self.included(where) + where.parameter "STACK_NAME", "Stack name, for example user/stackname or user/stackname:version", attribute_name: :source + end + end + module StackFileOrNameParam + # Include to add a stack file parameter def self.included(where) - where.parameter "[FILE]", "Kontena stack file, registry stack name (user/stack or user/stack:version) or URL", default: "kontena.yml", attribute_name: :filename + where.parameter "[FILE]", "Kontena stack file, registry stack name (user/stack or user/stack:version) or URL", default: "kontena.yml", attribute_name: :source end end module StackNameOption + # Include to add a stack name parameter def self.included(where) where.option ['-n', '--name'], 'NAME', 'Define stack name (by default comes from stack file)' end end module StackValuesToOption attr_accessor :values + # Include to add --values-to variable value dumping feature def self.included(where) where.option '--values-to', '[FILE]', 'Output variable values as YAML to file' end - def dump_variables(reader) - vals = reader.variables.to_h(values_only: true).reject {|k,_| k == 'STACK' || k == 'GRID' } - File.write(values_to, ::YAML.dump(vals)) + # Writes a YAML file from the values received from YAML::Reader to a file defined through + # the --values-to option + def dump_variables + File.write(values_to, ::YAML.dump(reader.variable_values, without_defaults: true, without_vault: true)) end end module StackValuesFromOption - attr_accessor :values + # Include to add --values-from option to read variable values from a YAML file + # and the -v variable=value option that can be used to pass variable values + # directly from command line def self.included(where) + where.prepend InstanceMethods + where.option '--values-from', '[FILE]', 'Read variable values from YAML' do |filename| - if filename - require_config_file(filename) - @values = ::YAML.safe_load(File.read(filename)) - end - filename + values_from_file.merge!(::YAML.safe_load(File.read(filename))) + true end - end - end - def stack_name - @stack_name ||= self.name || stack_name_from_yaml(filename) - end - - def reader_from_yaml(filename, name: nil, values: nil, defaults: nil) - reader = Kontena::Cli::Stacks::YAML::Reader.new(filename, values: values, defaults: defaults) - if reader.stack_name.nil? - exit_with_error "Stack MUST have stack name in YAML top level field 'stack'! Aborting." + where.option '-v', "VARIABLE=VALUE", "Set stack variable values, example: -v domain=example.com. Can be used multiple times.", multivalued: true, attribute_name: :var_option do |var_pair| + var_name, var_value = var_pair.split('=', 2) + values_from_value_options.merge!(::YAML.safe_load(::YAML.dump(var_name => var_value))) + end end - set_env_variables(name || reader.stack_name, current_grid) - reader - end - def stack_from_reader(reader) - outcome = reader.execute + module InstanceMethods + def values_from_file + @values_from_file ||= {} + end - hint_on_validation_notifications(outcome[:notifications]) unless outcome[:notifications].empty? - abort_on_validation_errors(outcome[:errors]) unless outcome[:errors].empty? - kontena_services = generate_services(outcome[:services]) - kontena_volumes = generate_volumes(outcome[:volumes]) - stack = { - 'name' => outcome[:name], - 'stack' => outcome[:stack], - 'expose' => outcome[:expose], - 'version' => outcome[:version], - 'source' => reader.raw_content, - 'registry' => outcome[:registry], - 'services' => kontena_services, - 'volumes' => kontena_volumes, - 'variables' => outcome[:variables] - } - stack - end + def values_from_value_options + @values_from_value_options ||= {} + end - def stack_from_yaml(filename, name: nil, values: nil, defaults: nil) - reader = reader_from_yaml(filename, name: name, values: values, defaults: defaults) - stack_from_reader(reader) - end + def values_from_options + @values_from_options ||= values_from_file.merge(values_from_value_options) + end - def stack_read_and_dump(filename, name: nil, values: nil, defaults: nil) - reader = reader_from_yaml(filename, name: name, values: values, defaults: defaults) - stack = stack_from_reader(reader) - dump_variables(reader) if values_to - stack - end - - def require_config_file(filename) - exit_with_error("File #{filename} does not exist") unless File.exists?(filename) - end - - def generate_volumes(yaml_volumes) - return [] unless yaml_volumes - yaml_volumes.map do |name, config| - if config['external'].is_a?(TrueClass) - config['external'] = name - elsif config['external']['name'] - config['external'] = config['external']['name'] + # Transforms a hash + # dependency_values_from_options('foo.bar' => 1, 'foo') + # => { 'bar' => 1 } + # Used for dependency variable injection + def dependency_values_from_options(name) + name_with_dot = name.to_s + '.' + values_from_options.each_with_object({}) do |kv_pair, obj| + key = kv_pair.first.to_s + value = kv_pair.last + next unless key.start_with?(name_with_dot) + obj[key.sub(name_with_dot, '')] = value + end end - config.merge('name' => name) end end - def generate_services(yaml_services) - return [] unless yaml_services - yaml_services.map do |name, config| - exit_with_error("Image is missing for #{name}. Aborting.") unless config['image'] # why isn't this a validation? - ServiceGeneratorV2.new(config).generate.merge('name' => name) - end - end - - def set_env_variables(stack, grid) + # Sets environment variables from parameters + # @param stack [String] current stack name + # @param grid [String] current grid name + # @param platform [String] current platform name, defaults to param grid value + def set_env_variables(stack, grid, platform = grid) ENV['STACK'] = stack ENV['GRID'] = grid + ENV['PLATFORM'] = platform end # @return [String] def current_dir File.basename(Dir.getwd) end def display_notifications(messages, color = :yellow) - $stderr.puts(Kontena.pastel.send(color, messages.to_yaml.gsub(/^---$/, ''))) + $stderr.puts(pastel.send(color, messages.to_yaml.gsub(/^---$/, ''))) end - def hint_on_validation_notifications(errors) - $stderr.puts "YAML contains the following unsupported options and they were rejected:".colorize(:yellow) - display_notifications(errors) + def hint_on_validation_notifications(notifications, filename = nil) + return if notifications.nil? || notifications.empty? + $stderr.puts pastel.yellow("#{"(#{filename}) " if filename}YAML contains the following unsupported options and they were rejected:") + display_notifications(notifications) end - def abort_on_validation_errors(errors) - $stderr.puts "YAML validation failed! Aborting.".colorize(:red) + def abort_on_validation_errors(errors, filename = nil) + return if errors.nil? || errors.empty? + $stderr.puts pastel.red("#{"(#{filename}) " if filename} YAML validation failed! Aborting.") display_notifications(errors, :red) abort end + # An accessor to stack registry client + # @return [Kontena::StacksClient] def stacks_client @stacks_client ||= Kontena::StacksClient.new(current_account.stacks_url, current_account.token, read_requires_token: current_account.stacks_read_authentication) end end end