Class: Rzo::App::Subcommand

Inherits:
Object
  • Object
show all
Extended by:
Logging
Includes:
ConfigValidation, Logging
Defined in:
lib/rzo/app/subcommand.rb

Overview

The base class for subcommands rubocop:disable Metrics/ClassLength

Direct Known Subclasses

Config, Generate, Roles

Constant Summary

Constants included from ConfigValidation

ConfigValidation::CHECKS_PERSONAL_CONFIG, ConfigValidation::CHECKS_REPO_CONFIG, ConfigValidation::RZO_PERSONAL_CONFIG_SCHEMA, ConfigValidation::RZO_REPO_CONFIG_SCHEMA

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

debug, error, fatal, info, input_stream, log, log, map_file_option, map_file_option, reset_logging!, reset_logging!, say, stream_logger, syslog_logger, warn, write_output

Methods included from ConfigValidation

#compute_issues, #validate_complete_config!, #validate_control_repos, #validate_defaults_key, #validate_existence, #validate_inform!, #validate_personal_config!, #validate_personal_schema, #validate_schema

Constructor Details

#initialize(opts = {}, stdout = $stdout, stderr = $stderr) ⇒ Subcommand

Initialize a subcommand with options injected by the application controller.

controller.



41
42
43
44
45
46
47
# File 'lib/rzo/app/subcommand.rb', line 41

def initialize(opts = {}, stdout = $stdout, stderr = $stderr)
  @opts = opts
  @stdout = stdout
  @stderr = stderr
  reset_logging!(opts)
  @pwd = Dir.pwd
end

Instance Attribute Details

#configObject (readonly)

The Rizzo configuration after loading ~/.rizzo.yaml (--config). See #load_config!



19
20
21
# File 'lib/rzo/app/subcommand.rb', line 19

def config
  @config
end

#optsObject (readonly)

The options hash injected from the application controller via the initialize method.



16
17
18
# File 'lib/rzo/app/subcommand.rb', line 16

def opts
  @opts
end

#pwdObject (readonly)

The present working directory at startup



21
22
23
# File 'lib/rzo/app/subcommand.rb', line 21

def pwd
  @pwd
end

Class Method Details

.load_rizzo_config(fpath) ⇒ Object

Delegated method to mock with fixture data.



25
26
27
28
29
30
31
32
33
34
# File 'lib/rzo/app/subcommand.rb', line 25

def self.load_rizzo_config(fpath)
  config_file = Pathname.new(fpath).expand_path
  raise ErrorAndExit, "Cannot read config file #{config_file}" unless config_file.readable?

  config = YAML.safe_load(config_file.read)
  log.debug "Loaded #{config_file}"
  config
rescue Psych::SyntaxError => e
  raise ErrorAndExit, "Could not parse rizzo config #{config_file} #{e.message}"
end

Instance Method Details

#load_config!Object (private)

Load rizzo configuration. Populate @config.

Read rizzo configuration by looping through control repos and stopping at first match and merge on top of local, defaults (~/.rizzo.yaml)



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/rzo/app/subcommand.rb', line 65

def load_config!
  config = load_rizzo_config(opts[:config])
  validate_personal_config!(config)
  repos = reorder_repos(config['control_repos'])
  config['control_repos'] = repos
  @config = load_repo_configs(config, repos)
  debug "Merged configuration: \n#{@config.to_yaml}"
  # TODO: Move these validations to an instance method?
  validate_complete_config!(@config)
  # validate_forwarded_ports(@config)
  # validate_ip_addresses(@config)
  @config
end

#load_repo_configs(config = {}, repos = []) ⇒ Hash (private)

Given a list of repository paths, load .rizzo.yaml from the root of each repository and return the result merged onto config. The merging behavior is implemented by deep_merge



91
92
93
94
95
96
97
98
99
100
# File 'lib/rzo/app/subcommand.rb', line 91

def load_repo_configs(config = {}, repos = [])
  repos.each_with_object(config.dup) do |repo, hsh|
    fp = Pathname.new(repo).expand_path + '.rizzo.yaml'
    if readable?(fp.to_s)
      hsh.deep_merge!(load_rizzo_config(fp.to_s))
    else
      log.debug "Skipped #{fp} (it is not readable)"
    end
  end
end

#load_rizzo_config(fpath) ⇒ Hash (private)

Load the base configuration and return it as a hash. This is necessary to get access to the 'control_repos' top level key, which is expected to be an Array of fully qualified paths to control repo base directories.



157
158
159
# File 'lib/rzo/app/subcommand.rb', line 157

def load_rizzo_config(fpath)
  self.class.load_rizzo_config(fpath)
end

#project_dir(path) ⇒ Object (private)

Memoized method to return the fully qualified path to the current rizzo project directory, based on the pwd. The project directory is the dirname of the full path to a .rizzo.yaml config file. Return false if not a project directory. ~/.rizzo.yaml is considered a personal configuration and not a project configuration.

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/rzo/app/subcommand.rb', line 185

def project_dir(path)
  return @project_dir unless @project_dir.nil?

  rizzo_file = Pathname.new("#{path}/.rizzo.yaml")
  personal_config = Pathname.new(File.expand_path('~/.rizzo.yaml'))
  iterations = 0
  while @project_dir.nil? && iterations < 100
    iterations += 1
    if readable?(rizzo_file.to_s) && rizzo_file != personal_config
      @project_dir = rizzo_file.dirname.to_s
    else
      rizzo_file = rizzo_file.dirname.dirname + '.rizzo.yaml'
      @project_dir = false if rizzo_file.dirname.root?
    end
  end
  @project_dir
end

#raise_ip_err(ip, node) ⇒ Object (private)

Helper to raise a duplicate port error

Raises:



141
142
143
144
145
# File 'lib/rzo/app/subcommand.rb', line 141

def raise_ip_err(ip, node)
  raise ErrorAndExit, "host ip #{ip} on node #{node} " \
    'is a duplicate.  IP addresses must be unique.  Check .rizzo.yaml ' \
    'files in each control repository for duplicate ip entries'
end

#raise_port_err(port, node) ⇒ Object (private)

Helper to raise a duplicate port error

Raises:



133
134
135
136
137
# File 'lib/rzo/app/subcommand.rb', line 133

def raise_port_err(port, node)
  raise ErrorAndExit, "host port #{port} on node #{node} " \
    'is a duplicate.  Ports must be unique.  Check .rizzo.yaml ' \
    'files in each control repository for duplicate forwarded_ports entries.'
end

#readable?(path) ⇒ Boolean (private)

helper method to to stub in tests



173
174
175
# File 'lib/rzo/app/subcommand.rb', line 173

def readable?(path)
  File.readable?(path)
end

#reorder_repos(repos = []) ⇒ Object (private)

Given a list of control repositories, determine if the user's runtime pwd is in a control repository. If it is, move that control repository to the top level. If the user is inside a control repository and



208
209
210
211
212
213
214
215
# File 'lib/rzo/app/subcommand.rb', line 208

def reorder_repos(repos = [])
  if path = project_dir(pwd)
    new_repos = repos - [path]
    new_repos.unshift(path)
  else
    repos
  end
end

#runFixnum

Default run method. Override this method in a subcommand sub-class



53
54
55
56
# File 'lib/rzo/app/subcommand.rb', line 53

def run
  error "Implement the run method in subclass #{self.class}"
  1
end

#validate_forwarded_ports(config) ⇒ Object (private)

Check for duplicate forwarded host ports across all hosts and exit non-zero with an error message if found.



105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rzo/app/subcommand.rb', line 105

def validate_forwarded_ports(config)
  host_ports = []
  [*config['nodes']].each do |node|
    [*node['forwarded_ports']].each do |hsh|
      port = hsh['host'].to_i
      raise_port_err(port, node['name']) if host_ports.include?(port)
      host_ports.push(port)
    end
  end
  log.debug "host_ports = #{host_ports}"
end

#validate_ip_addresses(config) ⇒ Object (private)

Check for duplicate forwarded host ports across all hosts and exit non-zero with an error message if found.



120
121
122
123
124
125
126
127
128
129
# File 'lib/rzo/app/subcommand.rb', line 120

def validate_ip_addresses(config)
  ips = []
  [*config['nodes']].each do |node|
    if ip = node['ip']
      raise_ip_err(ip, node['name']) if ips.include?(ip)
      ips.push(ip)
    end
  end
  log.debug "ips = #{ips}"
end

#write_file(filepath) ⇒ Object (private)

Write a file by yielding a file descriptor to the passed block. In the case of opening a file, the FD will automatically be closed.



164
165
166
167
168
169
170
# File 'lib/rzo/app/subcommand.rb', line 164

def write_file(filepath)
  case filepath
  when 'STDOUT' then yield @stdout
  when 'STDERR' then yield @stderr
  else File.open(filepath, 'w') { |fd| yield fd }
  end
end