forj 0.0.34 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/repositories.rb CHANGED
@@ -22,8 +22,8 @@ require 'require_relative'
22
22
 
23
23
  require_relative 'yaml_parse.rb'
24
24
  include YamlParse
25
- require_relative 'log.rb'
26
- include Logging
25
+ #require_relative 'log.rb'
26
+ #include Logging
27
27
 
28
28
  #
29
29
  # Repositories module
@@ -40,18 +40,17 @@ module Repositories
40
40
  if File.directory?(path + 'maestro')
41
41
  FileUtils.rm_r path + 'maestro'
42
42
  end
43
- Logging.info('cloning the maestro repo')
44
43
  Git.clone(maestro_url, 'maestro', :path => path)
45
44
  end
46
45
  rescue => e
46
+ Logging.error("%s\n%s" % [e.message, e.backtrace.join("\n")])
47
47
  puts 'Error while cloning the repo from %s' % [maestro_url]
48
48
  puts 'If this error persist you could clone the repo manually in ~/.forj/'
49
- Logging.error(e.message)
50
49
  end
51
50
  Dir.chdir(current_dir)
52
51
  end
53
-
54
- def create_infra
52
+
53
+ def create_infra(maestro_repo)
55
54
  home = File.expand_path('~')
56
55
  path = home + '/.forj/'
57
56
  infra = path + 'infra/'
@@ -64,9 +63,7 @@ module Repositories
64
63
  command = 'cp -rp ~/.forj/maestro/templates/infra/cloud-init ~/.forj/infra/'
65
64
  Kernel.system(command)
66
65
 
67
- Dir.chdir('build_tmpl')
68
-
69
- fill_template = 'python build-env.py -p ~/.forj/infra --maestro-path ~/.forj/maestro'
66
+ fill_template = 'python %s/build_tmpl/build-env.py -p ~/.forj/infra --maestro-path %s' % [$LIB_PATH, maestro_repo]
70
67
  Kernel.system(fill_template)
71
68
  end
72
69
  end
data/lib/security.rb CHANGED
@@ -18,65 +18,68 @@
18
18
  require 'rubygems'
19
19
  require 'require_relative'
20
20
 
21
- require_relative 'connection.rb'
22
- include Connection
23
- require_relative 'log.rb'
24
- include Logging
25
-
26
21
  #
27
22
  # SecurityGroup module
28
23
  #
29
24
  module SecurityGroup
30
25
 
31
- def get_or_create_security_group(name)
32
- Logging.info('getting or creating security group for %s' % [name])
33
- security_group = get_security_group(name)
34
- if security_group == nil
35
- security_group = create_security_group(name)
36
- end
26
+ def get_or_create_security_group(oFC, name)
27
+ Logging.state("Searching for security group '%s'..." % [name])
28
+ security_group = get_security_group(oFC, name)
29
+ security_group = create_security_group(oFC, name) if not security_group
37
30
  security_group
38
31
  end
39
32
 
40
- def create_security_group(name)
41
- sec_group = nil
33
+ def create_security_group(oFC, name)
34
+ Logging.debug("creating security group '%s'" % [name])
42
35
  begin
43
- sec_groups = get_security_group(name)
44
- if sec_groups.length >= 1
45
- sec_group = sec_groups[0]
46
- else
47
- description = 'Security group for blueprint %s' % [name]
48
- Logging.info(description)
49
- sec_group = Connection.network.security_groups.create(
36
+ description = "Security group for blueprint '%s'" % [name]
37
+ oFC.oNetwork.security_groups.create(
50
38
  :name => name,
51
39
  :description => description
52
40
  )
53
- end
54
41
  rescue => e
55
- Logging.error(e.message)
42
+ Logging.error("%s\n%s" % [e.message, e.backtrace.join("\n")])
56
43
  end
57
- sec_group
58
44
  end
59
45
 
60
- def get_security_group(name)
46
+ def get_security_group(oFC, name)
47
+ Logging.state("Searching for security group '%s'" % [name])
48
+ oSSLError=SSLErrorMgt.new
61
49
  begin
62
- Connection.network.security_groups.all({:name => name})[0]
50
+ sgroups = oFC.oNetwork.security_groups.all({:name => name})
63
51
  rescue => e
64
- Logging.error(e.message)
52
+ if not oSSLError.ErrorDetected(e.message,e.backtrace)
53
+ retry
54
+ end
55
+ end
56
+ case sgroups.length()
57
+ when 0
58
+ Logging.debug("No security group '%s' found" % [name] )
59
+ nil
60
+ when 1
61
+ Logging.debug("Found security group '%s'" % [sgroups[0].name])
62
+ sgroups[0]
65
63
  end
66
64
  end
67
65
 
68
- def delete_security_group(security_group)
66
+ def delete_security_group(oFC, security_group)
67
+ oSSLError=SSLErrorMgt.new
69
68
  begin
70
- sec_group = get_security_group(security_group)
71
- Connection.network.security_groups.get(sec_group.id).destroy
69
+ sec_group = get_security_group(oFC, security_group)
70
+ oFC.oNetwork.security_groups.get(sec_group.id).destroy
72
71
  rescue => e
73
- Logging.error(e.message)
72
+ if not oSSLError.ErrorDetected(e.message,e.backtrace)
73
+ retry
74
+ end
74
75
  end
75
76
  end
76
77
 
77
- def create_security_group_rule(security_group_id, protocol, port_min, port_max)
78
+ def create_security_group_rule(oFC, security_group_id, protocol, port_min, port_max)
79
+ Logging.debug("Creating ingress rule '%s:%s - %s to 0.0.0.0/0'" % [protocol, port_min, port_max])
80
+ oSSLError=SSLErrorMgt.new
78
81
  begin
79
- Connection.network.security_group_rules.create(
82
+ oFC.oNetwork.security_group_rules.create(
80
83
  :security_group_id => security_group_id,
81
84
  :direction => 'ingress',
82
85
  :protocol => protocol,
@@ -85,33 +88,49 @@ module SecurityGroup
85
88
  :remote_ip_prefix => '0.0.0.0/0'
86
89
  )
87
90
  rescue StandardError => e
91
+ if not oSSLError.ErrorDetected(e.message,e.backtrace)
92
+ retry
93
+ end
88
94
  msg = 'error creating the rule for port %s' % [port_min]
89
- puts msg
90
- Logging.error(e.message)
95
+ Logging.error msg
91
96
  end
92
97
  end
93
98
 
94
- def delete_security_group_rule(rule_id)
99
+ def delete_security_group_rule(oFC, rule_id)
100
+ oSSLError=SSLErrorMgt.new
95
101
  begin
96
- Connection.network.security_group_rules.get(rule_id).destroy
102
+ oFC.oNetwork.security_group_rules.get(rule_id).destroy
97
103
  rescue => e
98
- Logging.error(e.message)
104
+ if not oSSLError.ErrorDetected(e.message,e.backtrace)
105
+ retry
106
+ end
99
107
  end
100
108
  end
101
109
 
102
- def get_security_group_rule(port)
110
+ def get_security_group_rule(oFC, security_group_id, port_min, port_max)
111
+ Logging.state("Searching for rule '%s - %s'" % [ port_min, port_max])
112
+ oSSLError = SSLErrorMgt.new
103
113
  begin
104
- Connection.network.security_group_rules.all({:port_range_min => port, :port_range_max => port})[0]
105
- rescue => e
106
- Logging.error(e.message)
114
+ sgroups = oFC.oNetwork.security_group_rules.all({:port_range_min => port_min, :port_range_max => port_max, :security_group_id => security_group_id})
115
+ case sgroups.length()
116
+ when 0
117
+ Logging.debug("No security rule '%s - %s' found" % [ port_min, port_max ] )
118
+ nil
119
+ else
120
+ Logging.debug("Found security rule '%s - %s'." % [ port_min, port_max ])
121
+ sgroups
122
+ end
123
+ rescue => e
124
+ if not oSSLError.ErrorDetected(e.message,e.backtrace)
125
+ retry
126
+ end
107
127
  end
108
128
  end
109
129
 
110
- def get_or_create_rule(security_group_id, protocol, port_min, port_max)
111
- Logging.info('getting or creating rule %s' % [port_min])
112
- rule = get_security_group_rule(port_min)
113
- if rule == nil
114
- rule = create_security_group_rule(security_group_id, protocol, port_min, port_max)
130
+ def get_or_create_rule(oFC, security_group_id, protocol, port_min, port_max)
131
+ rule = get_security_group_rule(oFC, security_group_id, port_min, port_max)
132
+ if not rule
133
+ rule = create_security_group_rule(oFC, security_group_id, protocol, port_min, port_max)
115
134
  end
116
135
  rule
117
136
  end
data/lib/setup.rb CHANGED
@@ -28,54 +28,92 @@ include Helpers
28
28
  # Setup module call the hpcloud functions
29
29
  #
30
30
  module Setup
31
- def setup
32
- # delegate the initial configuration to hpcloud (unix_cli)
33
- Kernel.system('hpcloud account:setup')
34
- setup_credentials
35
- save_cloud_fog
36
- #Kernel.system('hpcloud keypairs:add nova')
31
+ def setup(sProvider, oConfig, options )
32
+ begin
33
+
34
+ raise 'No provider specified.' if not sProvider
35
+
36
+ sAccountName = sProvider # By default, the account name uses the same provider name.
37
+ sAccountName = options[:account_name] if options[:account_name]
38
+
39
+ if sProvider != 'hpcloud'
40
+ raise "forj setup support only hpcloud. '%s' is currently not supported." % sProvider
41
+ end
42
+
43
+ # TODO: Support of multiple providers thanks to fog.
44
+ # TODO: Replace this code by our own forj account setup, inspired/derived from hpcloud account::setup
45
+
46
+ # delegate the initial configuration to hpcloud (unix_cli)
47
+ hpcloud_data=File.expand_path('~/.hpcloud/accounts')
48
+ if File.exists?(File.join(hpcloud_data, 'hp')) and not File.exists?(File.join(hpcloud_data, sAccountName)) and sAccountName != 'hp'
49
+ Logging.info("hpcloud: Copying 'hp' account setup to '%s'" % sAccountName)
50
+ Kernel.system('hpcloud account:copy hp %s' % [sAccountName])
51
+ end
52
+
53
+ case Kernel.system('hpcloud account:setup %s' % [sAccountName] )
54
+ when false
55
+ raise "Unable to setup your hpcloud account"
56
+ when nil
57
+ raise "Unable to execute 'hpcloud' cli. Please check hpcloud installation."
58
+ end
59
+
60
+ if not oConfig.yConfig['default'].has_key?('account')
61
+ oConfig.LocalSet('account',sAccountName)
62
+ oConfig.SaveConfig
63
+ end
64
+
65
+ # Implementation of simple credential encoding for build.sh/maestro
66
+ save_maestro_creds(sAccountName)
67
+ rescue RuntimeError => e
68
+ Logging.fatal(1,e.message)
69
+ rescue => e
70
+ Logging.fatal(1,"%s\n%s" % [e.message,e.backtrace.join("\n")])
71
+ end
37
72
  end
38
73
  end
39
74
 
40
- def setup_credentials
41
- hpcloud_os_user = ask('Enter hpcloud username: ')
42
- hpcloud_os_key = ask('Enter hpcloud password: ') { |q| q.echo = '*'}
75
+ def save_maestro_creds(sAccountName)
43
76
 
44
- home = File.expand_path('~')
45
- Helpers.create_directory('%s/.cache/forj/' % [home])
46
- creds = '%s/.cache/forj/creds' % [home]
47
-
48
- values = {:credentials => {:hpcloud_os_user=> hpcloud_os_user, :hpcloud_os_key=> hpcloud_os_key}}
77
+ # TODO Be able to load the previous username if the g64 file exists.
78
+ hpcloud_os_user = ask('Enter hpcloud username: ') do |q|
79
+ q.validate = /\w+/
80
+ q.default = ''
81
+ end
49
82
 
50
- YamlParse.dump_values(values, creds)
83
+ hpcloud_os_key = ask('Enter hpcloud password: ') do |q|
84
+ q.echo = '*'
85
+ q.validate = /.+/
86
+ end
51
87
 
52
- end
88
+ add_creds = {:credentials => {:hpcloud_os_user=> hpcloud_os_user, :hpcloud_os_key=> hpcloud_os_key}}
53
89
 
90
+ sForjCache=File.expand_path('~/.cache/forj/')
91
+ cloud_fog = '%s/%s.g64' % [sForjCache, sAccountName]
54
92
 
55
- def save_cloud_fog
56
- home = File.expand_path('~')
57
93
 
58
- cloud_fog = '%s/.cache/forj/master.forj-13.5' % [home]
59
- local_creds = '%s/.cache/forj/creds' % [home]
94
+ Helpers.create_directory(sForjCache) if not File.directory?(sForjCache)
60
95
 
61
- creds = '%s/.hpcloud/accounts/hp' % [home]
62
- template = YAML.load_file(creds)
63
- local_template = YAML.load_file(local_creds)
96
+ # Security fix: Remove old temp file with clear password.
97
+ old_file = '%s/master.forj-13.5' % [sForjCache]
98
+ File.delete(old_file) if File.exists?(old_file)
99
+ old_file = '%s/creds' % [sForjCache]
100
+ File.delete(old_file) if File.exists?(old_file)
64
101
 
102
+ hpcloud_creds = File.expand_path('~/.hpcloud/accounts/%s' % [sAccountName])
103
+ creds = YAML.load_file(hpcloud_creds)
65
104
 
66
- access_key = template[:credentials][:account_id]
67
- secret_key = template[:credentials][:secret_key]
105
+ access_key = creds[:credentials][:account_id]
106
+ secret_key = creds[:credentials][:secret_key]
68
107
 
69
- os_user = local_template[:credentials][:hpcloud_os_user]
70
- os_key = local_template[:credentials][:hpcloud_os_key]
108
+ os_user = add_creds[:credentials][:hpcloud_os_user]
109
+ os_key = add_creds[:credentials][:hpcloud_os_key]
71
110
 
72
- File.open(cloud_fog, 'w') {|file|
73
- file.write('HPCLOUD_OS_USER=%s' % [os_user] + "\n")
74
- file.write('HPCLOUD_OS_KEY=%s' % [os_key] + "\n")
75
- file.write('DNS_KEY=%s' % [access_key] + "\n")
76
- file.write('DNS_SECRET=%s' % [secret_key])
111
+ IO.popen('gzip -c | base64 -w0 > %s' % [cloud_fog], 'r+') {|pipe|
112
+ pipe.puts('HPCLOUD_OS_USER=%s' % [os_user] )
113
+ pipe.puts('HPCLOUD_OS_KEY=%s' % [os_key] )
114
+ pipe.puts('DNS_KEY=%s' % [access_key] )
115
+ pipe.puts('DNS_SECRET=%s' % [secret_key])
116
+ pipe.close_write
77
117
  }
78
-
79
- command = 'cat %s | gzip -c | base64 -w0 > %s.g64' % [cloud_fog, cloud_fog]
80
- Kernel.system(command)
81
- end
118
+ Logging.info("'%s' written." % cloud_fog)
119
+ end
data/lib/ssh.rb CHANGED
@@ -18,8 +18,8 @@
18
18
  require 'rubygems'
19
19
  require 'require_relative'
20
20
 
21
- require_relative 'log.rb'
22
- include Logging
21
+ #require_relative 'log.rb'
22
+ #include Logging
23
23
 
24
24
  #
25
25
  # ssh module
@@ -40,4 +40,4 @@ module Ssh
40
40
  # connect to the server
41
41
  Kernel.system(connection)
42
42
  end
43
- end
43
+ end
data/lib/yaml_parse.rb CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  require 'rubygems'
19
19
  require 'yaml'
20
- require_relative 'log.rb'
21
- include Logging
20
+ #require_relative 'log.rb'
21
+ #include Logging
22
22
 
23
23
  #
24
24
  # YamlParse module
@@ -29,7 +29,7 @@ module YamlParse
29
29
  Logging.info('getting values from defaults.yaml, this will be a service catalog.forj.io')
30
30
  YAML.load_file(path_to_yaml)
31
31
  rescue => e
32
- Logging.error(e.message)
32
+ Logging.error("%s\n%s" % [e.message, e.backtrace.join("\n")])
33
33
  end
34
34
  end
35
35
 
@@ -39,7 +39,7 @@ module YamlParse
39
39
  YAML.dump(string, out)
40
40
  end
41
41
  rescue => e
42
- Logging.error(e.message)
42
+ Logging.error("%s\n%s" % [e.message, e.backtrace.join("\n")])
43
43
  end
44
44
  end
45
45
  end
@@ -20,37 +20,29 @@ require 'spec_helper'
20
20
 
21
21
  require 'fog'
22
22
 
23
- require_relative '../lib/connection.rb'
24
- include Connection
23
+ $APP_PATH = File.dirname(__FILE__)
24
+ $LIB_PATH = File.expand_path(File.join(File.dirname($APP_PATH),'lib'))
25
+ $FORJ_DATA_PATH= File.expand_path('~/.forj')
25
26
 
26
- class TestClass
27
- end
27
+ $LOAD_PATH << './lib'
28
28
 
29
- describe 'network' do
30
- it 'is connecting to hpcloud' do
31
- @test_class = TestClass.new
32
- @test_class.extend(Connection)
29
+ require 'forj-config.rb' # Load class ForjConfig
30
+ require 'log.rb' # Load default loggers
33
31
 
34
- Fog.mock!
32
+ include Logging
35
33
 
36
- conn = @test_class.network
37
- expect(conn).to be
34
+ $FORJ_LOGGER=ForjLog.new('forj-rspec.log', Logger::FATAL)
38
35
 
39
- Fog::Mock.reset
40
- end
41
- end
36
+ require 'connection.rb' # Load class ForjConnection
42
37
 
38
+ describe 'Module: forj-connection' do
43
39
 
44
- describe 'compute' do
45
- it 'is connecting to hpcloud' do
46
- @test_class = TestClass.new
47
- @test_class.extend(Connection)
40
+ it 'should connect to hpcloud (smoke test)' do
48
41
 
49
42
  Fog.mock!
50
-
51
- conn = @test_class.compute
43
+ conn = ForjConnection.new(ForjConfig.new())
52
44
  expect(conn).to be
53
45
 
54
46
  Fog::Mock.reset
55
47
  end
56
- end
48
+ end
@@ -15,17 +15,21 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
- class TestClass
19
- end
20
-
21
- require_relative '../lib/forj-config.rb'
22
18
  $APP_PATH = File.dirname(__FILE__)
23
19
  $LIB_PATH = File.expand_path(File.join(File.dirname($APP_PATH),'lib'))
24
20
  $FORJ_DATA_PATH= File.expand_path('~/.forj')
25
21
 
26
- describe 'forj cli' do
27
- describe ".forj-config" do
28
- context "new instance" do
22
+ $LOAD_PATH << './lib'
23
+
24
+ require 'forj-config.rb' # Load class ForjConfig
25
+ require 'log.rb' # Load default loggers
26
+
27
+ include Logging
28
+
29
+ $FORJ_LOGGER=ForjLog.new('forj-rspec.log', Logger::FATAL)
30
+
31
+ describe "class: forj-config" do
32
+ context "when creating a new instance" do
29
33
 
30
34
  it 'should be loaded' do
31
35
  @test_config=ForjConfig.new()
@@ -34,46 +38,46 @@ describe 'forj cli' do
34
38
 
35
39
  end
36
40
 
37
- context "Config in memory" do
41
+ context "when starting, forj-config" do
38
42
  before(:all) do
39
43
  @config=ForjConfig.new()
40
44
  end
41
45
 
42
46
  it 'should be able to create a key/value in local config' do
43
- @config.ConfigSet('test1','value')
44
- @config.yConfig['default']['test1'].should == 'value'
47
+ @config.LocalSet('test1','value')
48
+ expect(@config.yConfig['default']['test1']).to eq('value')
45
49
  end
46
50
 
47
51
  it 'should be able to remove the previously created key/value from local config' do
48
- @config.ConfigDel('test1')
49
- @config.yConfig['default'].key?('test1').should == false
52
+ @config.LocalDel('test1')
53
+ expect(@config.yConfig['default'].key?('test1')).to equal(false)
50
54
  end
51
55
  end
52
56
 
53
57
 
54
- context "Updating local config file" do
58
+ context "while updating local config file, forj-config" do
55
59
  before(:all) do
56
60
  @config=ForjConfig.new()
57
61
  end
58
62
 
59
63
  after(:all) do
60
- @config.ConfigDel('test1')
64
+ @config.LocalDel('test1')
61
65
  @config.SaveConfig()
62
66
  end
63
67
 
64
68
  it 'should save a key/value in local config' do
65
- @config.ConfigSet('test1','value')
66
- @config.SaveConfig().should == true
69
+ @config.LocalSet('test1','value')
70
+ expect(@config.SaveConfig()).to equal(true)
67
71
  end
68
72
 
69
73
  it 'should get the saved value from local config' do
70
74
  oConfig=ForjConfig.new()
71
- @config.yConfig['default']['test1'].should == 'value'
75
+ expect(@config.yConfig['default']['test1']).to eq('value')
72
76
  end
73
77
 
74
78
  end
75
79
 
76
- context "Updating another config file from .forj" do
80
+ context "With another config file - test1.yaml, forj-config" do
77
81
 
78
82
  before(:all) do
79
83
  if File.exists?('~/.forj/test.yaml')
@@ -88,25 +92,26 @@ describe 'forj cli' do
88
92
  File.delete(File.expand_path('~/.forj/test1.yaml'))
89
93
  end
90
94
 
91
- it 'If file do not exist, warning! and file is not created.' do
92
- File.exists?(File.expand_path('~/.forj/test.yaml')).should == false
95
+ it 'won\'t create a new file If we request to load \'test.yaml\'' do
96
+ expect(File.exists?(File.expand_path('~/.forj/test.yaml'))).to equal(false)
93
97
  end
94
98
 
95
- it 'Then, default config is loaded.' do
96
- File.basename(@config.sConfigName).should == 'config.yaml'
99
+ it 'will load the default config file if we request to load \'test.yaml\'' do
100
+ expect(File.basename(@config.sConfigName)).to eq('config.yaml')
97
101
  end
98
102
 
99
- it 'test1.yaml config is loaded.' do
100
- File.basename(@config2.sConfigName).should == 'test1.yaml'
103
+ it 'will confirm \'test1.yaml\' config to be loaded.' do
104
+ expect(File.basename(@config2.sConfigName)).to eq('test1.yaml')
101
105
  end
102
106
 
103
- it 'should save a key/value in test2 config' do
104
- @config2.ConfigSet('test2','value')
105
- @config2.SaveConfig().should == true
107
+ it 'can save \'test2=value\'' do
108
+ @config2.LocalSet('test2','value')
109
+ expect(@config2.SaveConfig()).to equal(true)
110
+ config3=ForjConfig.new('test1.yaml')
111
+ expect(config3.yConfig['default']['test2']).to eq('value')
106
112
  end
107
113
 
108
114
 
109
115
  end
110
- end
111
116
 
112
117
  end
@@ -18,6 +18,7 @@
18
18
  require 'rubygems'
19
19
  require 'spec_helper'
20
20
 
21
+ require 'ansi'
21
22
  require 'fog'
22
23
 
23
24
  require_relative '../lib/repositories.rb'
@@ -31,7 +32,7 @@ describe 'repositories' do
31
32
  @test_class = TestClass.new
32
33
  @test_class.extend(Repositories)
33
34
 
34
- repo = @test_class.clone_repo('https://github.com/forj-oss/maestro')
35
+ repo = @test_class.clone_repo('https://github.com/forj-oss/cli')
35
36
  expect(repo).to be
36
37
  end
37
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forj
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.34
4
+ version: 0.0.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - forj team
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ~>
109
109
  - !ruby/object:Gem::Version
110
110
  version: 1.6.21
111
+ - !ruby/object:Gem::Dependency
112
+ name: ansi
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.4.3
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.4.3
111
125
  description: forj command line
112
126
  email:
113
127
  - forj@forj.io