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,107 @@
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 'mist/config'
17
+ require 'mist/pool'
18
+ require 'mist/client'
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
+ # Mist build host
27
+ class MistHost < BuildHost
28
+ # SSH is the only acceptable Transport
29
+ def transports
30
+ ['ssh']
31
+ end
32
+ end
33
+
34
+ # Mist builder. Calls out to Mist to obtain a build host instance.
35
+ class Mist < Builder
36
+ def initialize
37
+ mist_config_file = ENV.fetch('MIST_CONFIG', File.join(%w(/ etc mist config)))
38
+ @config = ::Mist::Config.new(mist_config_file)
39
+
40
+ pool = ::Mist::Pool.get(@config.servers)
41
+ @client = ::Mist::Client.new(pool)
42
+ end
43
+
44
+ # Create & return a build host
45
+ def get(args = {})
46
+ args.symbolize_keys!
47
+
48
+ Cyclid.logger.debug "mist: args=#{args}"
49
+
50
+ # If there is one, split the 'os' into a 'distro' and 'release'
51
+ if args.key? :os
52
+ match = args[:os].match(/\A(\w*)_(.*)\Z/)
53
+ distro = match[1] if match
54
+ release = match[2] if match
55
+ else
56
+ # No OS was specified; use the default
57
+ # XXX Defaults should be configurable
58
+ distro = 'ubuntu'
59
+ release = 'trusty'
60
+ end
61
+
62
+ begin
63
+ result = @client.call(:create, distro: distro, release: release)
64
+ Cyclid.logger.debug "mist result=#{result}"
65
+
66
+ raise "failed to create build host: #{result['message']}" \
67
+ unless result['status']
68
+
69
+ buildhost = MistHost.new(name: result['name'],
70
+ host: result['ip'],
71
+ username: result['username'],
72
+ workspace: "/home/#{result['username']}",
73
+ password: nil,
74
+ key: @config.ssh_private_key,
75
+ server: result['server'],
76
+ distro: distro,
77
+ release: release)
78
+ rescue MessagePack::RPC::TimeoutError => ex
79
+ Cyclid.logger.error "Mist create call timedout: #{ex}"
80
+ raise "mist failed: #{ex}"
81
+ rescue StandardError => ex
82
+ Cyclid.logger.error "couldn't get a build host from Mist: #{ex}"
83
+ raise "mist failed: #{ex}"
84
+ end
85
+
86
+ Cyclid.logger.debug "mist buildhost=#{buildhost.inspect}"
87
+ return buildhost
88
+ end
89
+
90
+ # Destroy the build host
91
+ def release(_transport, buildhost)
92
+ name = buildhost[:name]
93
+ server = buildhost[:server]
94
+
95
+ begin
96
+ @client.call(:destroy, name: name, server: server)
97
+ rescue MessagePack::RPC::TimeoutError => ex
98
+ Cyclid.logger.error "Mist destroy timed out: #{ex}"
99
+ end
100
+ end
101
+
102
+ # Register this plugin
103
+ register_plugin 'mist'
104
+ end
105
+ end
106
+ end
107
+ end
@@ -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
+ # Base class for Dispatcher
23
+ class Dispatcher < Base
24
+ # Return the 'human' name for the plugin type
25
+ def self.human_name
26
+ 'dispatcher'
27
+ end
28
+
29
+ # Dispatch a job to a worker and return some opaque data that can be
30
+ # used to identify that job (E.g. an ID, UUID etc.)
31
+ def dispatch(job, record, callback = nil)
32
+ end
33
+ end
34
+
35
+ # A Runner may be running locally (within the API application context)
36
+ # or remotely. A job runner needs to send updates about the job status
37
+ # but obviously, a remote runner can't just update the JobRecord
38
+ # directly: they may put a message on a queue, which a job at the API
39
+ # application would consume and update the JobRecord.
40
+ #
41
+ # A Notifier provides an abstract method to update the JobRecord
42
+ # status and can also proxy LogBuffer writes.
43
+ #
44
+ module Notifier
45
+ # Base class for Notifiers
46
+ class Base
47
+ def initialize(job_id, callback_object)
48
+ end
49
+
50
+ # Update the JobRecord status
51
+ def status=(status)
52
+ end
53
+
54
+ # Update the JobRecord ended field
55
+ def ended=(time)
56
+ end
57
+
58
+ # Notify any callbacks that the job has completed
59
+ def completion(success)
60
+ end
61
+
62
+ # Proxy data to the log buffer
63
+ def write(data)
64
+ end
65
+ end
66
+
67
+ # Plugins may create a Callback instance that contains callbacks which
68
+ # are called by the Notifier when something happens; the Plugin can
69
+ # then take whatever action they need (E.g. updating an external
70
+ # status)
71
+ class Callback
72
+ # Called when the job completes
73
+ def completion(_job_id, _status)
74
+ end
75
+
76
+ # Called whenever the job status changes
77
+ def status_changed(_job_id, _status)
78
+ end
79
+
80
+ # Called whenever any data is written to the job record log
81
+ def log_write(_job_id, _data)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ require_rel 'dispatcher/*.rb'
@@ -0,0 +1,167 @@
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 'sidekiq'
17
+
18
+ # Top level module for the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Module for Cyclid Plugins
23
+ module Plugins
24
+ # Local Sidekiq based dispatcher
25
+ class Local < Dispatcher
26
+ # Queue the job to be run asynchronously.
27
+ def dispatch(job, record, callback = nil)
28
+ Cyclid.logger.debug "dispatching job: #{job}"
29
+
30
+ job_definition = job.to_hash.to_json
31
+
32
+ record.job_name = job.name
33
+ record.job_version = job.version
34
+ record.job = job_definition
35
+ record.save!
36
+
37
+ # The callback instance has to be serailised into JSON to survive the
38
+ # trip through Redis to Sidekiq
39
+ callback_json = callback.nil? ? nil : Oj.dump(callback)
40
+
41
+ # Create a SideKiq worker and pass in the job
42
+ Worker::Local.perform_async(job_definition, record.id, callback_json)
43
+
44
+ # The JobRecord ID is as good a job identifier as anything
45
+ return record.id
46
+ end
47
+
48
+ # Healthcheck; ensure that Sinatra is available and not under duress
49
+ require 'sidekiq/api'
50
+ extend Health::Helpers
51
+
52
+ # Perform a health check; for this plugin that means:
53
+ #
54
+ # Is Sidekiq running?
55
+ # Is the queue size healthy?
56
+ def self.status
57
+ stats = Sidekiq::Stats.new
58
+ if stats.processes_size.zero?
59
+ health_status(:error,
60
+ 'no Sidekiq process is running')
61
+ elsif stats.enqueued > 10
62
+ health_status(:warning,
63
+ "Sidekiq queue length is too high: #{stats.enqueued}")
64
+ elsif stats.default_queue_latency > 60
65
+ health_status(:warning,
66
+ "Sidekiq queue latency is too high: #{stats.default_queue_latency}")
67
+ else
68
+ health_status(:ok,
69
+ 'sidekiq is okay')
70
+ end
71
+ end
72
+
73
+ # Register this plugin
74
+ register_plugin 'local'
75
+ end
76
+
77
+ # A Runner may be running locally (within the API application context)
78
+ # or remotely. A job runner needs to send updates about the job status
79
+ # but obviously, a remote runner can't just update the JobRecord
80
+ # directly: they may put a message on a queue, which a job at the API
81
+ # application would consume and update the JobRecord.
82
+ #
83
+ # A Notifier provides an abstract method to update the JobRecord
84
+ # status and can also proxy LogBuffer writes.
85
+ #
86
+ module Notifier
87
+ # This is a local Notifier, so it can just pass updates directly on to
88
+ # the JobRecord & LogBuffer
89
+ class Local < Base
90
+ def initialize(job_id, callback)
91
+ @job_id = job_id
92
+ @job_record = JobRecord.find(job_id)
93
+
94
+ # Create a LogBuffer
95
+ @log_buffer = LogBuffer.new(@job_record)
96
+
97
+ @callback = callback
98
+ end
99
+
100
+ # Set the JobRecord status
101
+ def status=(status)
102
+ @job_record.status = status
103
+ @job_record.save!
104
+
105
+ # Ping the callback status_changed hook, if required
106
+ @callback.status_changed(@job_id, status) if @callback
107
+ end
108
+
109
+ # Set the JobRecord ended
110
+ def ended=(time)
111
+ @job_record.ended = time
112
+ @job_record.save!
113
+ end
114
+
115
+ # Ping the callback completion hook, if required
116
+ def completion(success)
117
+ @callback.completion(@job_id, success) if @callback
118
+ end
119
+
120
+ # Write data to the log buffer
121
+ def write(data)
122
+ @log_buffer.write data
123
+
124
+ # Ping the callback log_write hook, if required
125
+ @callback.log_write(@job_id, data) if @callback
126
+ end
127
+ end
128
+ end
129
+
130
+ # Namespace for any asyncronous workers
131
+ module Worker
132
+ # Local Sidekiq based worker
133
+ class Local
134
+ include Sidekiq::Worker
135
+
136
+ sidekiq_options retry: false
137
+
138
+ # Run a job Runner asynchronously
139
+ def perform(job, job_id, callback_object)
140
+ begin
141
+ # Unserialize the callback object, if there is one
142
+ callback = callback_object.nil? ? nil : Oj.load(callback_object)
143
+
144
+ notifier = Notifier::Local.new(job_id, callback)
145
+ rescue StandardError => ex
146
+ Cyclid.logger.debug "couldn't create notifier: #{ex}"
147
+ return false
148
+ end
149
+
150
+ begin
151
+ runner = Cyclid::API::Job::Runner.new(job_id, job, notifier)
152
+ success = runner.run
153
+ rescue StandardError => ex
154
+ Cyclid.logger.error "job runner failed: #{ex}"
155
+ success = false
156
+ end
157
+
158
+ # Notify completion
159
+ notifier.completion(success)
160
+
161
+ return success
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,40 @@
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 Provisioner plugins
23
+ class Provisioner < Base
24
+ # Return the 'human' name for the plugin type
25
+ def self.human_name
26
+ 'provisioner'
27
+ end
28
+
29
+ # Process the environment, performing all of the steps necasary to
30
+ # configure the host according to the given environment; this can
31
+ # include adding repositories, installing packages etc.
32
+ def prepare(_transport, _buildhost, _env = {})
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ require_rel 'provisioner/*.rb'
@@ -0,0 +1,90 @@
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
+ # Debian provisioner
23
+ class Debian < Provisioner
24
+ # Prepare a Debian based build host
25
+ def prepare(transport, buildhost, env = {})
26
+ transport.export_env('DEBIAN_FRONTEND' => 'noninteractive')
27
+
28
+ if env.key? :repos
29
+ env[:repos].each do |repo|
30
+ next unless repo.key? :url
31
+
32
+ url = repo[:url]
33
+ match = url.match(/\A(http|https):.*\Z/)
34
+ next unless match
35
+
36
+ case match[1]
37
+ when 'http', 'https'
38
+ add_http_repository(transport, url, repo, buildhost)
39
+ end
40
+ end
41
+
42
+ success = transport.exec 'sudo apt-get update'
43
+ raise 'failed to update repositories' unless success
44
+ end
45
+
46
+ env[:packages].each do |package|
47
+ success = transport.exec \
48
+ "sudo -E apt-get install -y #{package}"
49
+ raise "failed to install package #{package}" unless success
50
+ end if env.key? :packages
51
+ rescue StandardError => ex
52
+ Cyclid.logger.error "failed to provision #{buildhost[:name]}: #{ex}"
53
+ raise
54
+ end
55
+
56
+ private
57
+
58
+ def add_http_repository(transport, url, repo, buildhost)
59
+ raise 'an HTTP repository must provide a list of components' \
60
+ unless repo.key? :components
61
+
62
+ # Create a sources.list.d fragment
63
+ release = buildhost[:release]
64
+ components = repo[:components]
65
+ fragment = "deb #{url} #{release} #{components}"
66
+
67
+ success = transport.exec \
68
+ "echo '#{fragment}' | sudo tee -a /etc/apt/sources.list.d/cyclid.list"
69
+ raise "failed to add repository #{url}" unless success
70
+
71
+ if repo.key? :key_id
72
+ # Import the signing key
73
+ key_id = repo[:key_id]
74
+
75
+ success = transport.exec \
76
+ "gpg --keyserver keyserver.ubuntu.com --recv-keys #{key_id}"
77
+ raise "failed to import key #{key_id}" unless success
78
+
79
+ success = transport.exec \
80
+ "gpg -a --export #{key_id} | sudo apt-key add -"
81
+ raise "failed to add repository key #{key_id}" unless success
82
+ end
83
+ end
84
+
85
+ # Register this plugin
86
+ register_plugin 'debian'
87
+ end
88
+ end
89
+ end
90
+ end