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