cyclid 0.2.0

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.
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