cyclid 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +174 -0
  3. data/README.md +54 -0
  4. data/app/cyclid.rb +61 -0
  5. data/app/cyclid/config.rb +38 -0
  6. data/app/cyclid/controllers.rb +123 -0
  7. data/app/cyclid/controllers/auth.rb +34 -0
  8. data/app/cyclid/controllers/auth/token.rb +78 -0
  9. data/app/cyclid/controllers/health.rb +96 -0
  10. data/app/cyclid/controllers/organizations.rb +104 -0
  11. data/app/cyclid/controllers/organizations/collection.rb +134 -0
  12. data/app/cyclid/controllers/organizations/config.rb +128 -0
  13. data/app/cyclid/controllers/organizations/document.rb +135 -0
  14. data/app/cyclid/controllers/organizations/job.rb +266 -0
  15. data/app/cyclid/controllers/organizations/members.rb +145 -0
  16. data/app/cyclid/controllers/organizations/stages.rb +251 -0
  17. data/app/cyclid/controllers/users.rb +47 -0
  18. data/app/cyclid/controllers/users/collection.rb +131 -0
  19. data/app/cyclid/controllers/users/document.rb +133 -0
  20. data/app/cyclid/health_helpers.rb +40 -0
  21. data/app/cyclid/job.rb +3 -0
  22. data/app/cyclid/job/helpers.rb +67 -0
  23. data/app/cyclid/job/job.rb +164 -0
  24. data/app/cyclid/job/runner.rb +275 -0
  25. data/app/cyclid/job/stage.rb +67 -0
  26. data/app/cyclid/log_buffer.rb +104 -0
  27. data/app/cyclid/models.rb +3 -0
  28. data/app/cyclid/models/job_record.rb +25 -0
  29. data/app/cyclid/models/organization.rb +64 -0
  30. data/app/cyclid/models/plugin_config.rb +25 -0
  31. data/app/cyclid/models/stage.rb +42 -0
  32. data/app/cyclid/models/step.rb +29 -0
  33. data/app/cyclid/models/user.rb +60 -0
  34. data/app/cyclid/models/user_permissions.rb +28 -0
  35. data/app/cyclid/monkey_patches.rb +37 -0
  36. data/app/cyclid/plugin_registry.rb +75 -0
  37. data/app/cyclid/plugins.rb +125 -0
  38. data/app/cyclid/plugins/action.rb +48 -0
  39. data/app/cyclid/plugins/action/command.rb +89 -0
  40. data/app/cyclid/plugins/action/email.rb +207 -0
  41. data/app/cyclid/plugins/action/email/html.erb +58 -0
  42. data/app/cyclid/plugins/action/email/text.erb +13 -0
  43. data/app/cyclid/plugins/action/script.rb +90 -0
  44. data/app/cyclid/plugins/action/slack.rb +129 -0
  45. data/app/cyclid/plugins/action/slack/note.erb +5 -0
  46. data/app/cyclid/plugins/api.rb +195 -0
  47. data/app/cyclid/plugins/api/github.rb +111 -0
  48. data/app/cyclid/plugins/api/github/callback.rb +66 -0
  49. data/app/cyclid/plugins/api/github/methods.rb +201 -0
  50. data/app/cyclid/plugins/api/github/status.rb +67 -0
  51. data/app/cyclid/plugins/builder.rb +80 -0
  52. data/app/cyclid/plugins/builder/mist.rb +107 -0
  53. data/app/cyclid/plugins/dispatcher.rb +89 -0
  54. data/app/cyclid/plugins/dispatcher/local.rb +167 -0
  55. data/app/cyclid/plugins/provisioner.rb +40 -0
  56. data/app/cyclid/plugins/provisioner/debian.rb +90 -0
  57. data/app/cyclid/plugins/provisioner/ubuntu.rb +98 -0
  58. data/app/cyclid/plugins/source.rb +39 -0
  59. data/app/cyclid/plugins/source/git.rb +64 -0
  60. data/app/cyclid/plugins/transport.rb +63 -0
  61. data/app/cyclid/plugins/transport/ssh.rb +155 -0
  62. data/app/cyclid/sinatra/api_helpers.rb +66 -0
  63. data/app/cyclid/sinatra/auth_helpers.rb +127 -0
  64. data/app/cyclid/sinatra/warden/strategies/api_token.rb +62 -0
  65. data/app/cyclid/sinatra/warden/strategies/basic.rb +58 -0
  66. data/app/cyclid/sinatra/warden/strategies/hmac.rb +76 -0
  67. data/app/db.rb +51 -0
  68. data/bin/cyclid-db-init +107 -0
  69. data/db/schema.rb +92 -0
  70. data/lib/cyclid/app.rb +4 -0
  71. metadata +407 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # Add two methods to Hash
3
+ class Hash
4
+ # http://chrisholtz.com/blog/lets-make-a-ruby-hash-map-method-that-returns-a-hash-instead-of-an-array/
5
+ def hmap
6
+ inject({}) do |hash, (k, v)|
7
+ hash.merge(yield(k, v))
8
+ end
9
+ end
10
+
11
+ # Interpolate the data in the ctx hash into any String values
12
+ def interpolate(ctx)
13
+ hmap do |key, value|
14
+ if value.is_a? String
15
+ { key => value % ctx }
16
+ else
17
+ { key => value }
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Add a method to Array
24
+ class Array
25
+ # Interpolate the data in the ctx hash for each String & Hash item
26
+ def interpolate(ctx)
27
+ map do |entry|
28
+ if entry.is_a? Hash
29
+ entry.interpolate ctx
30
+ elsif entry.is_a? String
31
+ entry % @ctx
32
+ else
33
+ entry
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for Cyclid Plugins
21
+ module Plugins
22
+ # Intelligent system-wide registry of available plugins with helper
23
+ # methods to find them again
24
+ class Registry
25
+ def initialize
26
+ @plugins = []
27
+ @types = []
28
+ end
29
+
30
+ # Add a plugin to the registry
31
+ def register(plugin)
32
+ # XXX Perform sanity checks
33
+ @plugins << plugin
34
+
35
+ # Maintain a human<->type mapping
36
+ human_name = plugin.human_name
37
+ @types << { human: human_name, type: plugin.superclass }
38
+ end
39
+
40
+ # Find a plugin from the registry
41
+ def find(name, type)
42
+ object_type = nil
43
+
44
+ # Convert a human name to a type, if required
45
+ if type.is_a? String
46
+ @types.each do |registered_type|
47
+ next unless registered_type[:human] == type
48
+
49
+ object_type = registered_type[:type]
50
+ break
51
+ end
52
+ else
53
+ object_type = type
54
+ end
55
+
56
+ raise "couldn't map plugin type #{type}" if object_type.nil?
57
+
58
+ @plugins.each do |plugin|
59
+ return plugin if plugin.name == name && plugin.superclass == object_type
60
+ end
61
+ return nil
62
+ end
63
+
64
+ # Return a list of all plugins of a certain type
65
+ def all(type = nil)
66
+ list = []
67
+ @plugins.each do |plugin|
68
+ list << plugin if plugin.superclass == type or type.nil?
69
+ end
70
+ return list
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'require_all'
17
+ require 'active_support/core_ext'
18
+
19
+ require_relative 'health_helpers'
20
+
21
+ # Top level module for the core Cyclid code.
22
+ module Cyclid
23
+ # Module for the Cyclid API
24
+ module API
25
+ # Module for Cyclid Plugins
26
+ module Plugins
27
+ # Base class for Plugins
28
+ class Base
29
+ class << self
30
+ attr_reader :name
31
+
32
+ # Returns the 'human' name for the plugin type
33
+ def human_name
34
+ 'base'
35
+ end
36
+
37
+ # Add the (derived) plugin to the plugin registry
38
+ def register_plugin(name)
39
+ @name = name
40
+ Cyclid.plugins.register(self)
41
+ end
42
+
43
+ # Get the configuration for the given org
44
+ def get_config(org)
45
+ # If the organization was passed by name, convert it into an Organization object
46
+ org = Organization.find_by(name: org) if org.is_a? String
47
+ raise 'organization does not exist' if org.nil?
48
+
49
+ # XXX Plugins of different types can have the same name; we need to
50
+ # add a 'type' field and also find by the type.
51
+ config = org.plugin_configs.find_by(plugin: @name)
52
+ if config.nil?
53
+ # No config currently exists; create a new default config
54
+ config = PluginConfig.new(plugin: @name,
55
+ version: '1.0.0',
56
+ config: Oj.dump(default_config.stringify_keys))
57
+ config.save!
58
+
59
+ org.plugin_configs << config
60
+ end
61
+
62
+ # Convert the model to a hash, add the config schema, and convert the JSON config
63
+ # blob back into a hash
64
+ config_hash = config.serializable_hash
65
+ config_hash['schema'] = config_schema
66
+ config_hash['config'] = Oj.load(config.config)
67
+
68
+ return config_hash
69
+ rescue StandardError => ex
70
+ Cyclid.logger.error "couldn't get/create plugin config for #{@name}: #{ex}"
71
+ raise
72
+ end
73
+
74
+ # Set the configuration for the given org
75
+ def set_config(new_config, org)
76
+ new_config.stringify_keys!
77
+
78
+ config = org.plugin_configs.find_by(plugin: @name)
79
+ if config.nil?
80
+ # No config currently exists; create a new default config
81
+ config = PluginConfig.new(plugin: @name,
82
+ version: '1.0.0',
83
+ config: Oj.dump(default_config.stringify_keys))
84
+ config.save!
85
+
86
+ org.plugin_configs << config
87
+ end
88
+
89
+ # Let the plugin validate & merge the changes into the config hash
90
+ config_hash = config.serializable_hash
91
+ current_config = config_hash['config']
92
+ Cyclid.logger.debug "current_config=#{current_config}"
93
+ merged_config = update_config(Oj.load(current_config), new_config)
94
+
95
+ raise 'plugin rejected the configuration' if merged_config == false
96
+
97
+ Cyclid.logger.debug "merged_config=#{merged_config}"
98
+
99
+ # Update the stored configuration
100
+ config.config = Oj.dump(merged_config.stringify_keys)
101
+ config.save!
102
+ end
103
+
104
+ # Validite the given configuration items and merge them into the correct configuration,
105
+ # returning an updated complete configuration that can be stored.
106
+ def update_config(_current, _new)
107
+ return false
108
+ end
109
+
110
+ # Provide the default configuration state that should be used when creating a new config
111
+ def default_config
112
+ {}
113
+ end
114
+
115
+ # Get the schema for the configuration data that the plugin stores
116
+ def config_schema
117
+ {}
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ require_rel 'plugins/*.rb'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for Cyclid Plugins
21
+ module Plugins
22
+ # Base class for Action plugins
23
+ class Action < Base
24
+ def initialize(args = {})
25
+ end
26
+
27
+ # Return the 'human' name for the plugin type
28
+ def self.human_name
29
+ 'action'
30
+ end
31
+
32
+ # Provide any additional run-time data, such as the transport &
33
+ # context, that the plugin will require for perform() but didn't get
34
+ # during initialize.
35
+ def prepare(args = {})
36
+ @transport = args[:transport]
37
+ @ctx = args[:ctx]
38
+ end
39
+
40
+ # Run the Action.
41
+ def perform(log)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ require_rel 'action/*.rb'
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for Cyclid Plugins
21
+ module Plugins
22
+ # Command plugin
23
+ class Command < Action
24
+ Cyclid.logger.debug 'in the Command plugin'
25
+
26
+ def initialize(args = {})
27
+ args.symbolize_keys!
28
+
29
+ # At a bare minimum there has to be a command to execute.
30
+ raise 'a command action requires a command' unless args.include? :cmd
31
+
32
+ # The command & arguments can either be passed seperately, with the
33
+ # args as an array, or as a single string which we then split into
34
+ # a command & array of args.
35
+ if args.include? :args
36
+ @cmd = args[:cmd]
37
+ @args = args[:args]
38
+ else
39
+ cmd_args = args[:cmd].split
40
+ @cmd = cmd_args.shift
41
+ @args = cmd_args
42
+ end
43
+
44
+ Cyclid.logger.debug "cmd: '#{@cmd}' args: #{@args}"
45
+
46
+ @env = args[:env] if args.include? :env
47
+ @path = args[:path] if args.include? :path
48
+ end
49
+
50
+ # Note that we don't need to explicitly use the log for transport
51
+ # related tasks as the transport will take of writing any data from the
52
+ # commands into the log. The log is still passed in to perform() so that
53
+ # plugins can write their own data to it, as we do here by writing out
54
+ # the (optional) path & command that is being run.
55
+ def perform(log)
56
+ begin
57
+ # Export the environment data to the build host, if necesary
58
+ env = @env.interpolate(@ctx) if @env
59
+ @transport.export_env(env)
60
+
61
+ # Log the command being run (and the working directory, if one is
62
+ # set)
63
+ cmd_args = "#{@cmd} #{@args.join(' ')}"
64
+ log.write(@path.nil? ? "$ #{cmd_args}\n" : "$ #{@path} : #{cmd_args}\n")
65
+
66
+ # Interpolate any data from the job context
67
+ cmd_args = cmd_args % @ctx
68
+
69
+ # Interpolate the path if one is set
70
+ path = @path
71
+ path = path % @ctx unless path.nil?
72
+
73
+ # Run the command
74
+ success = @transport.exec(cmd_args, path)
75
+ rescue KeyError => ex
76
+ # Interpolation failed
77
+ log.write "#{ex.message}\n"
78
+ success = false
79
+ end
80
+
81
+ [success, @transport.exit_code]
82
+ end
83
+
84
+ # Register this plugin
85
+ register_plugin 'command'
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'mail'
17
+ require 'erb'
18
+ require 'premailer'
19
+
20
+ # Top level module for the core Cyclid code.
21
+ module Cyclid
22
+ # Module for the Cyclid API
23
+ module API
24
+ # Module for Cyclid Plugins
25
+ module Plugins
26
+ # Email notification plugin
27
+ class Email < Action
28
+ def initialize(args = {})
29
+ args.symbolize_keys!
30
+
31
+ raise 'an email action requires a message' unless args.include? :message
32
+ @message = args[:message]
33
+
34
+ raise 'an email action requires a recipient' unless args.include? :to
35
+ @to = args[:to]
36
+
37
+ @subject = args[:subject] || 'Cyclid notification'
38
+ @color = args[:color] || 'dodgerblue'
39
+ end
40
+
41
+ # Send an email to the configured recipient; the message is rendered
42
+ # into text & HTML via. an ERB template which inserts additional
43
+ # information from the context
44
+ def perform(log)
45
+ begin
46
+ # Retrieve the server-wide email configuration
47
+ config = Cyclid.config.plugins
48
+ email_config = load_email_config(config)
49
+
50
+ Cyclid.logger.debug "sending via. #{email_config[:server]}:#{email_config[:port]} " \
51
+ "as #{email_config[:from]}"
52
+
53
+ # Add the job context
54
+ to = @to % @ctx
55
+ subject = @subject % @ctx
56
+ message = @message % @ctx
57
+
58
+ # Create a binding for the text & HTML ERB templates
59
+ info = { color: @color, title: subject }
60
+
61
+ bind = binding
62
+ bind.local_variable_set(:info, info)
63
+ bind.local_variable_set(:ctx, @ctx)
64
+ bind.local_variable_set(:message, message)
65
+
66
+ # Generate text email from a template
67
+ template_path = File.expand_path(File.join(__FILE__, '..', 'email', 'text.erb'))
68
+ template = ERB.new(File.read(template_path), nil, '%<>-')
69
+
70
+ text_body = template.result(bind)
71
+
72
+ # Generate the HTML email from a template
73
+ template_path = File.expand_path(File.join(__FILE__, '..', 'email', 'html.erb'))
74
+ template = ERB.new(File.read(template_path), nil, '%<>-')
75
+
76
+ html = template.result(bind)
77
+
78
+ # Run the HTML through Premailer to inline the styles
79
+ premailer = Premailer.new(html,
80
+ with_html_string: true,
81
+ warn_level: Premailer::Warnings::SAFE)
82
+ html_body = premailer.to_inline_css
83
+
84
+ # Create the email
85
+ mail = Mail.new
86
+ mail.from = email_config[:from]
87
+ mail.to = to
88
+ mail.subject = subject
89
+ mail.text_part do
90
+ body text_body
91
+ end
92
+ mail.html_part do
93
+ content_type 'text/html; charset=UTF8'
94
+ body html_body
95
+ end
96
+
97
+ # Deliver the email via. the configured server, using
98
+ # authentication if a username & password were provided.
99
+ log.write("sending email to #{@to}")
100
+
101
+ mail.delivery_method :smtp, address: email_config[:server],
102
+ port: email_config[:port],
103
+ user_name: email_config[:username],
104
+ password: email_config[:password]
105
+ mail.deliver
106
+
107
+ success = true
108
+ rescue StandardError => ex
109
+ log.write "#{ex.message}\n"
110
+ success = false
111
+ end
112
+
113
+ [success, 0]
114
+ end
115
+
116
+ # Register this plugin
117
+ register_plugin 'email'
118
+
119
+ private
120
+
121
+ # Load the config for the email plugin and set defaults if they're not
122
+ # in the config
123
+ def load_email_config(config)
124
+ config.symbolize_keys!
125
+
126
+ server_config = config[:email] || {}
127
+ Cyclid.logger.debug "config=#{server_config}"
128
+
129
+ server_config.symbolize_keys!
130
+
131
+ # Set defaults where no server configuration is set
132
+ server_config[:server] ||= 'localhost'
133
+ server_config[:port] ||= 587
134
+ server_config[:from] ||= 'cyclid@cyclid.io'
135
+
136
+ server_config[:username] ||= nil
137
+ server_config[:password] ||= nil
138
+
139
+ # Load any organization specific configuration
140
+ plugin_data = self.class.get_config(@ctx[:organization])
141
+ Cyclid.logger.debug "using plugin config #{plugin_data}"
142
+ plugin_config = plugin_data['config']
143
+
144
+ # Merge the plugin configuration onto the server configuration (I.e.
145
+ # plugin configuration over-rides server configuration) to produce
146
+ # the final configuration
147
+ email_config = {}
148
+ email_config[:server] = plugin_config[:server] || server_config[:server]
149
+ email_config[:port] = plugin_config[:port] || server_config[:port]
150
+ email_config[:from] = plugin_config[:from] || server_config[:from]
151
+
152
+ email_config[:username] = plugin_config[:username] || server_config[:username]
153
+ email_config[:password] = plugin_config[:password] || server_config[:password]
154
+
155
+ return email_config
156
+ end
157
+
158
+ # Static methods for handling plugin config data
159
+ class << self
160
+ # Update the plugin configuration
161
+ def update_config(current, new)
162
+ current.merge! new
163
+ end
164
+
165
+ # Default configuration for the email plugin
166
+ def default_config
167
+ config = {}
168
+ config['server'] = nil
169
+ config['port'] = nil
170
+ config['from'] = nil
171
+ config['username'] = nil
172
+ config['password'] = nil
173
+
174
+ return config
175
+ end
176
+
177
+ # Config schema for the email plugin
178
+ def config_schema
179
+ schema = []
180
+ schema << { name: 'server',
181
+ type: 'string',
182
+ description: 'SMTP server for outgoing emails',
183
+ default: nil }
184
+ schema << { name: 'port',
185
+ type: 'integer',
186
+ description: 'SMTP server port to connect to',
187
+ default: nil }
188
+ schema << { name: 'from',
189
+ type: 'string',
190
+ description: 'Sender email address',
191
+ default: nil }
192
+ schema << { name: 'username',
193
+ type: 'string',
194
+ description: 'SMTP server username',
195
+ default: nil }
196
+ schema << { name: 'password',
197
+ type: 'string',
198
+ description: 'SMTP server password',
199
+ default: nil }
200
+
201
+ return schema
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end