minfra-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +12 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/CHANGELOG.md +2 -0
  6. data/Dockerfile +12 -0
  7. data/bin/build +20 -0
  8. data/bin/console +16 -0
  9. data/bin/container_exec +9 -0
  10. data/bin/run_tests +74 -0
  11. data/bin/setup.sh +22 -0
  12. data/exe/minfra +6 -0
  13. data/lib/deep_merge.rb +149 -0
  14. data/lib/hash.rb +28 -0
  15. data/lib/minfra/cli/ask.rb +43 -0
  16. data/lib/minfra/cli/command.rb +35 -0
  17. data/lib/minfra/cli/commands/dev.rb +54 -0
  18. data/lib/minfra/cli/commands/kube.rb +279 -0
  19. data/lib/minfra/cli/commands/project/branch.rb +17 -0
  20. data/lib/minfra/cli/commands/project/tag.rb +40 -0
  21. data/lib/minfra/cli/commands/project.rb +113 -0
  22. data/lib/minfra/cli/commands/setup.rb +49 -0
  23. data/lib/minfra/cli/commands/stack/app_template.rb +65 -0
  24. data/lib/minfra/cli/commands/stack/client_template.rb +36 -0
  25. data/lib/minfra/cli/commands/stack/kube_stack_template.rb +94 -0
  26. data/lib/minfra/cli/commands/stack.rb +120 -0
  27. data/lib/minfra/cli/commands/tag.rb +86 -0
  28. data/lib/minfra/cli/common.rb +41 -0
  29. data/lib/minfra/cli/config.rb +111 -0
  30. data/lib/minfra/cli/document.rb +19 -0
  31. data/lib/minfra/cli/hook.rb +65 -0
  32. data/lib/minfra/cli/logging.rb +26 -0
  33. data/lib/minfra/cli/main_command.rb +32 -0
  34. data/lib/minfra/cli/plugins.rb +34 -0
  35. data/lib/minfra/cli/runner.rb +59 -0
  36. data/lib/minfra/cli/templater.rb +63 -0
  37. data/lib/minfra/cli/version.rb +5 -0
  38. data/lib/minfra/cli.rb +80 -0
  39. data/lib/orchparty/ast.rb +53 -0
  40. data/lib/orchparty/cli.rb +69 -0
  41. data/lib/orchparty/context.rb +22 -0
  42. data/lib/orchparty/dsl_parser.rb +229 -0
  43. data/lib/orchparty/dsl_parser_kubernetes.rb +361 -0
  44. data/lib/orchparty/kubernetes_application.rb +305 -0
  45. data/lib/orchparty/plugin.rb +24 -0
  46. data/lib/orchparty/plugins/env.rb +41 -0
  47. data/lib/orchparty/transformations/all.rb +18 -0
  48. data/lib/orchparty/transformations/mixin.rb +73 -0
  49. data/lib/orchparty/transformations/remove_internal.rb +16 -0
  50. data/lib/orchparty/transformations/sort.rb +10 -0
  51. data/lib/orchparty/transformations/variable.rb +56 -0
  52. data/lib/orchparty/transformations.rb +24 -0
  53. data/lib/orchparty/version.rb +3 -0
  54. data/lib/orchparty.rb +59 -0
  55. data/minfra-cli.gemspec +40 -0
  56. data/project.json +7 -0
  57. data/templates/kind.yaml.erb +33 -0
  58. data/templates/kube_config.yaml.erb +7 -0
  59. data/templates/minfra_config.json.erb +26 -0
  60. metadata +196 -0
@@ -0,0 +1,120 @@
1
+ require 'fileutils'
2
+ require_relative 'stack/app_template'
3
+ require_relative 'stack/client_template'
4
+ require_relative 'stack/kube_stack_template'
5
+
6
+ module Minfra
7
+ module Cli
8
+ class Stack < Command
9
+
10
+ desc "describe","get information about a stack"
11
+ option :environment, aliases: ['-e']
12
+ def describe
13
+ pp @minfra_config.describe(options["environment"])
14
+ end
15
+
16
+ desc "dashboard <stack_name>", "openening a dashboard for a stack"
17
+ option :environment, aliases: ['-e']
18
+ option :deployment, aliases: ['-d']
19
+ option :cluster, aliases: ['-c']
20
+ def dashboard(stack_name='all')
21
+ kube.dashboard(stack_name, options[:environment], options[:deployment], options[:cluster])
22
+ end
23
+
24
+ desc "deploy <stack_name> '<message> (optional)'", "deploy a complete stack"
25
+ option :environment, aliases: ['-e']
26
+ # option :deployment, aliases: ['-d']
27
+ option :cluster, aliases: ['-c']
28
+ option :dev, type: :boolean # currently, about to be changed
29
+ option :explain, type: :boolean
30
+ option :install, type: :boolean
31
+ option :test, type: :boolean
32
+ option :opts
33
+ def deploy(stack_name, message='')
34
+ kube.deploy(stack_name, message)
35
+ end
36
+
37
+ desc "rollback <extraargs>", "rollback a deployment"
38
+ option :environment, aliases: ['-e']
39
+ option :deployment, aliases: ['-d']
40
+ option :cluster, aliases: ['-c']
41
+ def rollback(*args)
42
+ STDERR.puts "needs implementation"
43
+ exit 1
44
+ #kube.rollback(stack_name, options[:environment], options[:deployment], options[:cluster], args)
45
+ end
46
+
47
+ desc "destroy", "remove the whole stack"
48
+ option :environment, aliases: ['-e']
49
+ option :cluster, aliases: ['-c']
50
+ def destroy(stack_name)
51
+ kube.destroy(stack_name)
52
+ end
53
+
54
+ desc "list", "list all stacks in an environment"
55
+ option :environment, aliases: ['-e']
56
+ option :cluster, aliases: ['-c']
57
+ def list
58
+ kube.list
59
+ end
60
+
61
+ desc "app", "show the app a stack provides"
62
+ option :environment, aliases: ['-e']
63
+ def app(stack_name)
64
+ cluster=nil
65
+ deployment=nil
66
+ template = Minfra::Cli::StackM::AppTemplate.new(stack_name, minfra_config)
67
+ template.read
68
+ puts "Template: #{template.app_path}\n#{template.to_s}"
69
+ apps = AppResource.all(filter: {identifier: template.app.identifier} ).data
70
+ if apps.empty?
71
+ puts "Auth: app uninstalled"
72
+ atts = {
73
+ identifier: template.app.identifier,
74
+ name: template.app.name,
75
+ short_name: template.app.short_name,
76
+ description: template.app.description,
77
+ native: template.app.native,
78
+ start_url: template.app.start_url,
79
+ public: template.app.public
80
+ }
81
+ app_res=AppResource.build( {data: {attributes: atts, type: 'apps'}} )
82
+ app_res.save
83
+ app = app_res.data
84
+ else
85
+ app = apps.first
86
+ end
87
+ puts "Auth: app installed #{app.id}"
88
+
89
+ clients = OauthClientResource.all(filter: {app_id: app.id}).data
90
+ if clients.empty?
91
+ puts "Auth: client not registered"
92
+ atts= {redirect_uris: template.client.redirect_uris.map { |r| "#{app.start_url}#{r}" },
93
+ native: template.client.native, ppid: template.client.ppid, name: template.client.name, app_id: app.id}
94
+ client_res=OauthClientResource.build( {data: {attributes: atts, type: 'oauth_clients'}} )
95
+ client_res.save
96
+ client=client_res.data
97
+ else
98
+ client = clients.first
99
+ end
100
+ puts "Auth: client registered #{client.id}"
101
+
102
+ client_template=Minfra::Cli::StackM::ClientTemplate.new(stack_name, client.name, minfra_config)
103
+ unless client_template.exist?
104
+ File.write(client_template.path.to_s, { name: client.name, identifier: client.identifier, secret: client.secret }.to_json)
105
+ end
106
+
107
+
108
+ # TODO: create app configuration
109
+ # TODO: create provider
110
+ end
111
+
112
+ private
113
+ def kube
114
+ Kube.new(options, @minfra_config)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ Minfra::Cli.register("stack", "dealing wit stacks", Minfra::Cli::Stack)
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minfra
4
+ module Cli
5
+ class Tag
6
+ include Minfra::Cli::Logging
7
+
8
+ def initialize
9
+ @now = Time.now.utc
10
+ @format = '%Y_%m_%dT%H_%M_%SZ'
11
+ @tags_folder = '.tags'
12
+ end
13
+
14
+ def tag_current_commit_for_deploy(message, format)
15
+ @format = format if format
16
+
17
+ info 'Creating tag.'
18
+ debug "Using .tags folder..."
19
+ write_tag_folder_file(message)
20
+ run_cmd(cmd_add_tag_info("#{@tags_folder}/#{tag_name}"), :system)
21
+ run_cmd(cmd_create_tag_commit, :system)
22
+ run_cmd(cmd_tag_commit(message), :system)
23
+ info 'Pushing tag to remote.'
24
+ run_cmd(cmd_push, :system)
25
+ run_cmd(cmd_push_tag, :system)
26
+ end
27
+
28
+ def ensure_commit_is_pushed
29
+ info 'Checking that the current commit is present on the remote.'
30
+ output = run_cmd(cmd_ensure_commit_is_pushed)
31
+
32
+ if output.empty?
33
+ exit_error "The current commit is not present on the remote.\n" \
34
+ 'Please push your changes to origin and try again.'
35
+ end
36
+ end
37
+
38
+ def cmd_ensure_commit_is_pushed
39
+ 'git branch -r --contains $(git rev-list --max-count=1 HEAD)'
40
+ end
41
+
42
+ def cmd_add_tag_info(file)
43
+ "git add #{file}"
44
+ end
45
+
46
+ def cmd_create_tag_commit
47
+ "git commit -m '#{tag_name}'"
48
+ end
49
+
50
+ def cmd_tag_commit(message)
51
+ "git tag -a #{tag_name} -m '#{message}'"
52
+ end
53
+
54
+ def cmd_push
55
+ "git push"
56
+ end
57
+
58
+ def cmd_push_tag
59
+ "git push origin #{tag_name}"
60
+ end
61
+
62
+ def git_current_branch
63
+ `git rev-parse --abbrev-ref HEAD`.strip
64
+ end
65
+
66
+ #TBD: this should be more flexible
67
+ def tag_name
68
+ "#{git_current_branch}-REL-#{@now.strftime(@format)}"
69
+ end
70
+
71
+ def write_tag_folder_file(message)
72
+ File.write("#{@tags_folder}/#{tag_name}", "#{message}\n")
73
+ end
74
+
75
+ def write_tag_file(_message)
76
+ File.write(@tags_file.to_s, "#{tag_name}\n")
77
+ end
78
+
79
+ def run_cmd(cmd, _how = :system)
80
+ Runner.run(cmd)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ #Minfra::Cli.register("tag", "creating tags", Minfra::Cli::Tag)
@@ -0,0 +1,41 @@
1
+ require 'pathname'
2
+
3
+ module Minfra
4
+ module Cli
5
+ module Common
6
+
7
+ def exit_error(msg)
8
+ STDERR.puts("ERROR: #{msg}" )
9
+ exit 1
10
+ end
11
+
12
+ def run_cmd(cmd, type = :non_system, silence: false)
13
+ puts cmd unless silence
14
+ case type
15
+ when :exec
16
+ Kernel.exec(cmd)
17
+ when :bash
18
+ res = system(%{bash -c "#{Array.new(cmd).join(' && ')}"})
19
+ exit_error("failed!") unless res
20
+ nil # no output!
21
+ when :system
22
+ res = system(cmd)
23
+ exit_error("failed!") unless res
24
+ nil # no output!
25
+ else
26
+ `#{cmd}` # with output!
27
+ end
28
+ end
29
+
30
+ def parse_cmd(cmd, silence: false)
31
+ reply = JSON.parse(run_cmd(cmd, silence: silence))
32
+ rescue JSON::ParserError, TypeError
33
+ error "ERROR: #{$ERROR_INFO.message}"
34
+ error reply
35
+ error "command was: #{cmd}"
36
+ exit 1
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,111 @@
1
+ require 'pathname'
2
+ require 'hashie/mash'
3
+ require 'json'
4
+ require_relative 'templater'
5
+ module Minfra
6
+ module Cli
7
+ # responsible the read the config file(s) and add a small abstraction layer on top of it
8
+ class Config
9
+ include Logging
10
+
11
+ class ConfigNotFoundError < StandardError
12
+ end
13
+ class EnvironmentNotFoundError < StandardError
14
+ end
15
+
16
+ attr_reader :base_path
17
+ attr_reader :config_path
18
+ attr_reader :stacks_path
19
+ attr_reader :me_path
20
+ attr_reader :kube_path
21
+ attr_reader :kube_config_path
22
+ attr_reader :kind_config_path
23
+
24
+ attr_reader :orch_env
25
+ attr_reader :orch_env_config
26
+ attr_reader :config
27
+ attr_reader :project
28
+
29
+ def self.load(orch_env, base_path_str = nil)
30
+ new(base_path_str).load(orch_env)
31
+ end
32
+
33
+ def initialize(base_path_str=nil)
34
+ init!(base_path_str)
35
+ end
36
+
37
+ def init!(base_path_str=nil)
38
+ debug( "Config: initializing" )
39
+ @base_path = Pathname.new(base_path_str || ENV["MINFRA_PATH"]).expand_path
40
+ @me_path = @base_path.join('me')
41
+ @project_config_path=@base_path.join("config","project.json")
42
+ @config_path = @me_path.join('config.json')
43
+ @stacks_path = @base_path.join('stacks')
44
+ @kube_path=@me_path.join('kube')
45
+ @kube_config_path=@kube_path.join('config')
46
+ @kind_config_path=@me_path.join("kind.yaml.erb")
47
+ @project_minfrarc_path = @base_path.join("config",'minfrarc.rb')
48
+ require @project_minfrarc_path if @project_minfrarc_path.exist?
49
+ @me_minfrarc_path = @me_path.join('minfrarc.rb')
50
+ require @me_minfrarc_path if @me_minfrarc_path.exist?
51
+ if config_path.exist?
52
+ @config = Hashie::Mash.new(JSON.parse(Minfra::Cli::Templater.render(File.read(config_path),{})))
53
+ else
54
+ warn("personal minfra configuration file '#{config_path}' not found, you might have to run 'minfra setup dev'")
55
+ @config = Hashie::Mash.new({})
56
+ end
57
+ @project = Hashie::Mash.new(JSON.parse(Minfra::Cli::Templater.render(File.read(@project_config_path),{})))
58
+ end
59
+
60
+ def load(orch_env)
61
+ debug( "loading config env: #{orch_env} #{@orch_env}" )
62
+ return self if defined?(@orch_env)
63
+ @orch_env = orch_env
64
+ @orch_env_config = @config.environments[@orch_env] || raise(EnvironmentNotFoundError.new("Configuration for orchestration environment '#{@orch_env}' not found. Available orechstration environments: #{@config.environments.keys.inspect}"))
65
+ @project= @project.
66
+ deep_merge(@project.environments[@orch_env]).
67
+ deep_merge(@config).
68
+ deep_merge(@orch_env_config)
69
+ @orch_env_config['env']=@orch_env
70
+ self
71
+ end
72
+
73
+ def name
74
+ @project.name
75
+ end
76
+
77
+ def describe(environment)
78
+ {
79
+ env: {
80
+ minfra_name: ENV["MINFRA_NAME"],
81
+ minfra_path: ENV["MINFRA_PATH"],
82
+ },
83
+ base_path: base_path.to_s,
84
+ me_path: me_path.to_s,
85
+ kube_path: kube_path.to_s,
86
+ config_path: config_path.to_s,
87
+ config: @config.to_h,
88
+ env_config: @orch_env_config.to_h,
89
+ project: @project
90
+ }
91
+ end
92
+ def dev?
93
+ @orch_env=='dev'
94
+ end
95
+
96
+ def email
97
+ @config.identity.email
98
+ end
99
+
100
+ def api_key
101
+ @project.account_api_key
102
+ end
103
+
104
+ def endpoint(name)
105
+ Hashie::Mash.new({"api_key": api_key}).deep_merge(@project.endpoints[name])
106
+ rescue
107
+ raise("endpoint #{name} is undefinded please add <env>:endpoints:#{name} to you #{config_path} file ")
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,19 @@
1
+ module Minfra
2
+ module Cli
3
+ class Document
4
+ def self.document(config, message)
5
+ new(config).document(message)
6
+ end
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def document(message)
13
+ return true if @config.dev?
14
+ puts "TBD: calling documentation hooks"
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,65 @@
1
+ # no support for around hooks yet
2
+
3
+ module Minfra
4
+ module Cli
5
+ module Hook
6
+ class Hook
7
+ def initialize(type, name, block)
8
+ @type=type
9
+ @name=name
10
+ @block=block
11
+ end
12
+
13
+ def match?(type, name)
14
+ @type == type && @name == name
15
+ end
16
+
17
+ def exec (obj)
18
+ obj.instance_eval(&@block)
19
+ end
20
+
21
+ end
22
+
23
+ class Hooker
24
+ def initialize(klass)
25
+ @klass=klass
26
+ @hooks=[]
27
+ end
28
+
29
+ def register_before(name, block)
30
+ @hooks << Hook.new(:before, name, block)
31
+ end
32
+
33
+ def register_after(name, block)
34
+ @hooks << Hook.new(:after, name, block)
35
+ end
36
+
37
+ def call(obj, name, &block)
38
+ @hooks.select do |h| h.match?(:before, name) end.each do |h| h.exec(obj) end
39
+ obj.instance_eval(&block)
40
+ @hooks.select do |h| h.match?(:after, name) end.each do |h| h.exec(obj) end
41
+ end
42
+ end
43
+
44
+ def self.included(klass)
45
+ klass.extend(ClassMethods)
46
+ end
47
+
48
+ module ClassMethods
49
+ def hooks
50
+ @hooker||=Hooker.new(self)
51
+ end
52
+ def after_hook(name,&block)
53
+ hooks.register_after(name, block)
54
+ end
55
+ def before_hook(name, &block)
56
+ hooks.register_before(name, block)
57
+ end
58
+ end
59
+
60
+ def with_hook(hook_name, &block)
61
+ self.class.hooks.call(self, hook_name, &block)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ module Minfra
2
+ module Cli
3
+ module Logging
4
+ def error(str)
5
+ STDERR.puts "Error: #{str}"
6
+ end
7
+
8
+ def exit_error(str)
9
+ error str
10
+ exit 1
11
+ end
12
+
13
+ def info(str)
14
+ STDOUT.puts str
15
+ end
16
+
17
+ def debug(str)
18
+ STDOUT.puts "Debug: #{str}"
19
+ end
20
+
21
+ def deprecated(comment)
22
+ puts "DEPRECATED: #{comment}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ module Minfra
2
+ module Cli
3
+ class Main < Command
4
+
5
+ desc 'kube', 'kubectl wrapper and other features'
6
+ long_desc '
7
+ '
8
+ option :environment, required: false, aliases: ['-e']
9
+ option :cluster
10
+ def kube(*args)
11
+ kube.kubectl_command(args)
12
+ end
13
+
14
+ # tbd: move this to project
15
+ desc 'tag', 'tag current commit for deployment - triggers CI'
16
+ option :message, default: 'release', aliases: ['-m']
17
+ option :format, required: false, aliases: ['-f']
18
+ def tag
19
+ Tag.new.tag_current_commit_for_deploy(options[:message], options[:format])
20
+ end
21
+
22
+ desc 'version', 'prints version of the cli'
23
+ def version
24
+ puts Minfra::Cli::VERSION
25
+ end
26
+
27
+ def self.exit_on_failure?
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module Minfra
2
+ module Cli
3
+ class Plugins
4
+ def self.load
5
+ [Pathname.new(ENV["MINFRA_PATH"]).join("config","minfra_plugins.json"),
6
+ Pathname.new(ENV["MINFRA_PATH"]).join("me","minfra_plugins.json")].each do |file|
7
+
8
+ next unless File.exist?(file)
9
+
10
+ plugins=JSON.parse(File.read(file))
11
+ plugins["plugins"].each do |spec|
12
+ opts=spec["opts"] || {}
13
+ opts.merge(require: false)
14
+ if opts["path"]
15
+ begin
16
+ $LOAD_PATH.unshift opts["path"]+"/lib"
17
+ require spec["name"]
18
+ rescue Gem::Requirement::BadRequirementError
19
+ STDERR.puts("Can't load plugin: #{spec["name"]}")
20
+ end
21
+ else
22
+ begin
23
+ Gem::Specification.find_by_name(spec["name"])
24
+ gem spec["name"], spec["version"]
25
+ rescue Gem::MissingSpecError
26
+ STDERR.puts("Can't load plugin: #{spec["name"]}, #{spec["version"]}; run 'minfra plugin setup'")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ require 'open3'
2
+
3
+ module Minfra
4
+ module Cli
5
+ class Runner
6
+ class Result
7
+ attr_reader :stdout, :stderr
8
+ def initialize(stdout,stderr, status)
9
+ @stderr=stderr
10
+ @stdout=stdout
11
+ @status=status
12
+ end
13
+
14
+ def success?
15
+ @status.success?
16
+ end
17
+
18
+ def error?
19
+ !success?
20
+ end
21
+
22
+ def to_s
23
+ @stdout.to_s
24
+ end
25
+ end
26
+
27
+ include Logging
28
+ def self.run(cmd, **args)
29
+ new(cmd, **args).run
30
+ end
31
+
32
+ attr_reader :exit_on_error
33
+ def initialize(cmd, exit_on_error: true)
34
+ @cmd=cmd
35
+ @exit_on_error = exit_on_error
36
+ end
37
+
38
+ def run
39
+ debug(@cmd)
40
+ res=nil
41
+ begin
42
+ res=Result.new(*Open3.capture3(@cmd))
43
+ rescue
44
+ end
45
+ if res&.error?
46
+ STDERR.puts "command failed: #{@cmd}"
47
+ STDERR.puts res.stdout
48
+ STDERR.puts res.stderr
49
+ end
50
+ if exit_on_error && res&.error?
51
+ STDERR.puts "exiting on error"
52
+ exit 1
53
+ end
54
+ res
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,63 @@
1
+ require 'erb'
2
+
3
+ module Minfra
4
+ module Cli
5
+ class Templater # not threadsafe!
6
+ def self.read(path, params: {}, fallback: nil)
7
+ p=Pathname.new(path)
8
+ if p.exist?
9
+ content=File.read(path)
10
+ else
11
+ if fallback
12
+ content=fallback
13
+ else
14
+ raise "file #{path} not found"
15
+ end
16
+ end
17
+ render(template, params)
18
+ end
19
+
20
+ def self.render(template, params)
21
+ new(template).render(params)
22
+ end
23
+
24
+ def initialize(template)
25
+ @erb = ERB.new(template)
26
+ @check_mode=false
27
+ @check_missing=[]
28
+ end
29
+
30
+ def missing?
31
+ !check_missing.empty?
32
+ end
33
+
34
+ def check_missing(&block)
35
+ begin
36
+ @check_mode = true
37
+ @check_block = block
38
+ @check_missing = []
39
+ @erb.result(binding)
40
+ ensure
41
+ @check_block = nil
42
+ @check_mode = false
43
+ end
44
+ @check_missing
45
+ end
46
+
47
+ def render(params)
48
+ @erb.result_with_hash(params)
49
+ end
50
+
51
+ def method_missing(name)
52
+ if @check_mode
53
+ if @check_block
54
+ @check_block.call(name)
55
+ end
56
+ @check_missing << name
57
+ else
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ module Minfra
2
+ module Cli
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end