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,133 @@
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 all of the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for all User related API endpoints
21
+ module Users
22
+ # API endpoints for a single Organization document
23
+ # @api REST
24
+ module Document
25
+ # @!group Users
26
+
27
+ # @!method get_users_user
28
+ # @overload GET /users/:username
29
+ # @macro rest
30
+ # @param [String] username Username of the user.
31
+ # Get a specific user.
32
+ # @return The requested user.
33
+ # @return [404] The user does not exist
34
+
35
+ # @!method put_users_user(body)
36
+ # @overload PUT /users/:username
37
+ # @macro rest
38
+ # @param [String] username Username of the user.
39
+ # Modify a specific user.
40
+ # @param [JSON] body User information
41
+ # @option body [String] name Users real name
42
+ # @option body [String] email Users new email address
43
+ # @option body [String] password New Bcrypt2 encrypted password
44
+ # @option body [String] new_password New password in plain text, which will be
45
+ # encrypted before being stored in the databaase.
46
+ # @option body [String] secret New HMAC signing secret. This should be a suitably
47
+ # long random string.
48
+ # @return [200] User was modified successfully
49
+ # @return [400] The user definition is invalid
50
+ # @return [404] The user does not exist
51
+
52
+ # @!method delete_users_user
53
+ # @overload DELETE /users/:username
54
+ # @macro rest
55
+ # @param [String] username Username of the user.
56
+ # Delete a specific user.
57
+ # @return [200] User was deleted successfully
58
+ # @return [404] The user does not exist
59
+
60
+ # @!endgroup
61
+
62
+ # Sinatra callback
63
+ # @private
64
+ def self.registered(app)
65
+ include Errors::HTTPErrors
66
+
67
+ # Get a specific user.
68
+ app.get do
69
+ authorized_as!(params[:username], Operations::READ)
70
+
71
+ user = User.find_by(username: params[:username])
72
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
73
+ if user.nil?
74
+
75
+ Cyclid.logger.debug user.organizations
76
+
77
+ # Convert to a Hash and inject the User data
78
+ user_hash = user.serializable_hash
79
+ user_hash['organizations'] = user.organizations.map(&:name)
80
+
81
+ user_hash = sanitize_user(user_hash)
82
+
83
+ return user_hash.to_json
84
+ end
85
+
86
+ # Modify a specific user.
87
+ app.put do
88
+ authorized_as!(params[:username], Operations::WRITE)
89
+
90
+ payload = parse_request_body
91
+ Cyclid.logger.debug payload
92
+
93
+ user = User.find_by(username: params[:username])
94
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
95
+ if user.nil?
96
+
97
+ begin
98
+ user.name = payload['name'] if payload.key? 'name'
99
+ user.email = payload['email'] if payload.key? 'email'
100
+ user.password = payload['password'] if payload.key? 'password'
101
+ user.secret = payload['secret'] if payload.key? 'secret'
102
+ user.new_password = payload['new_password'] if payload.key? 'new_password'
103
+ user.save!
104
+ rescue ActiveRecord::ActiveRecordError => ex
105
+ Cyclid.logger.debug ex.message
106
+ halt_with_json_response(400, INVALID_JSON, ex.message)
107
+ end
108
+
109
+ return json_response(NO_ERROR, "user #{payload['username']} modified")
110
+ end
111
+
112
+ # Delete a specific user.
113
+ app.delete do
114
+ authorized_as!(params[:username], Operations::ADMIN)
115
+
116
+ user = User.find_by(username: params[:username])
117
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
118
+ if user.nil?
119
+
120
+ begin
121
+ user.delete
122
+ rescue ActiveRecord::ActiveRecordError => ex
123
+ Cyclid.logger.debug ex.message
124
+ halt_with_json_response(400, INVALID_JSON, ex.message)
125
+ end
126
+
127
+ return json_response(NO_ERROR, "user #{params['username']} deleted")
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ 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
+ require 'sinatra-health-check'
17
+
18
+ # Top level module for all of the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ module Health
23
+ # Helper methods to isolate the plugins from the implementation details
24
+ # of the healthcheck framework
25
+ module Helpers
26
+ # Health statuses
27
+ STATUSES = {
28
+ ok: SinatraHealthCheck::Status::SEVERITIES[:ok],
29
+ warning: SinatraHealthCheck::Status::SEVERITIES[:warning],
30
+ error: SinatraHealthCheck::Status::SEVERITIES[:error]
31
+ }.freeze
32
+
33
+ # Produce a SinatraHealthCheck object from the given status & message
34
+ def health_status(status, message)
35
+ SinatraHealthCheck::Status.new(status, message)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ # Load all of the job related classes
3
+ require_rel 'job/*.rb'
@@ -0,0 +1,67 @@
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 Job related classes
21
+ module Job
22
+ # Useful methods for dealing with Jobs
23
+ module Helpers
24
+ # Create & dispatch a Job from the job definition
25
+ def job_from_definition(definition, callback = nil, context = {})
26
+ # This function will only ever be called from a Sinatra context
27
+ org = Organization.find_by(name: params[:name])
28
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
29
+ if org.nil?
30
+
31
+ # Create a new JobRecord
32
+ job_record = JobRecord.new
33
+ job_record.started = Time.now.to_s
34
+ job_record.status = Constants::JobStatus::NEW
35
+ job_record.save!
36
+
37
+ org.job_records << job_record
38
+
39
+ # The user may, or may not, be set: if the job has come via. the :organization/jobs
40
+ # endpoint it'll be set (as that's authenticated), if it's come from an API extension the
41
+ # user mat not be set (as it may be unauthenticated, or not using the same authentication
42
+ # as Cyclid)
43
+ user = current_user
44
+ current_user.job_records << job_record if user
45
+
46
+ begin
47
+ job = ::Cyclid::API::Job::JobView.new(definition, context, org)
48
+ Cyclid.logger.debug job.to_hash
49
+
50
+ job_id = Cyclid.dispatcher.dispatch(job, job_record, callback)
51
+ rescue StandardError => ex
52
+ Cyclid.logger.error "job dispatch failed: #{ex}"
53
+
54
+ # We couldn't dispatch the job; record the failure
55
+ job_record.status = Constants::JobStatus::FAILED
56
+ job_record.ended = Time.now.to_s
57
+ job_record.save!
58
+
59
+ raise
60
+ end
61
+
62
+ return job_id
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,164 @@
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 Job related classes
21
+ module Job
22
+ # Non-ActiveRecord class which holds a complete Job, complete with
23
+ # serialised stages and the resolved sequence.
24
+ class JobView
25
+ attr_reader :name, :version
26
+
27
+ def initialize(job, context, org)
28
+ # Job is a hash (converted from JSON or YAML)
29
+ job.symbolize_keys!
30
+
31
+ @name = job[:name]
32
+ @version = job[:version] || '1.0.0'
33
+
34
+ @context = context
35
+ @organization = org.name
36
+ @environment = job[:environment]
37
+ @sources = job[:sources] || []
38
+ @secrets = setec_astronomy(org, (job[:secrets] || {}))
39
+
40
+ # Build a single unified list of StageViews
41
+ @stages, @sequence = build_stage_collection(job, org)
42
+ end
43
+
44
+ # Return everything, serialized into a hash
45
+ def to_hash
46
+ hash = {}
47
+ hash[:name] = @name
48
+ hash[:version] = @version
49
+ hash[:context] = @context
50
+ hash[:organization] = @organization
51
+ hash[:environment] = @environment
52
+ hash[:sources] = @sources
53
+ hash[:secrets] = @secrets
54
+ hash[:stages] = @stages.each_with_object({}) do |(name, stage), h|
55
+ h[name.to_sym] = Oj.dump(stage)
56
+ end
57
+ hash[:sequence] = @sequence
58
+
59
+ return hash
60
+ end
61
+
62
+ private
63
+
64
+ # Too Many Secrets
65
+ def setec_astronomy(org, secrets)
66
+ # Create the RSA private key
67
+ private_key = OpenSSL::PKey::RSA.new(org.rsa_private_key)
68
+
69
+ secrets.hmap do |key, secret|
70
+ { key => private_key.private_decrypt(Base64.decode64(secret)) }
71
+ end
72
+ end
73
+
74
+ # Create the ad-hoc StageViews & combine them with the Stages defined
75
+ # in the Job, returning an array of StageViews & a list of the Stages
76
+ # in the order to be run.
77
+ def build_stage_collection(job, org)
78
+ # Create a JobStage for each ad-hoc stage defined in the job and
79
+ # add it to the list of stages for this job
80
+ stages = {}
81
+ sequence = []
82
+ begin
83
+ job[:stages].each do |stage|
84
+ stage_view = StageView.new(stage)
85
+ stages[stage_view.name.to_sym] = stage_view
86
+ end if job.key? :stages
87
+ rescue StandardError => ex
88
+ # XXX Probably something wrong with the definition; re-raise it? Or
89
+ # maybe we get rid of this block and catch it further up (in the
90
+ # controller?)
91
+ Cyclid.logger.info "ad-hoc stage creation failed: #{ex}"
92
+ raise
93
+ end
94
+
95
+ # For each stage in the job, it's either already in the list of
96
+ # stages because we created on as an ad-hoc stage, or we need to load
97
+ # it from the database, create a JobStage from it, and add it to the
98
+ # list of stages
99
+ job_sequence = job[:sequence]
100
+ job_sequence.each do |job_stage|
101
+ job_stage.symbolize_keys!
102
+
103
+ raise ArgumentError, 'invalid stage definition' \
104
+ unless job_stage.key? :stage
105
+
106
+ # Store the job in the sequence so that we can run the stages in
107
+ # the correct order
108
+ name = job_stage[:stage]
109
+ sequence << name
110
+
111
+ # Try to find the stage
112
+ if stages.key? name.to_sym
113
+ # Ad-hoc stage defined in the job
114
+ stage_view = stages[name.to_sym]
115
+ else
116
+ # Try to find a matching pre-defined stage
117
+ stage = if job_stage.key? :version
118
+ org.stages.find_by(name: name, version: job_stage[:version])
119
+ else
120
+ # If no version given, get the latest
121
+ org.stages.where(name: name).last
122
+ end
123
+
124
+ raise ArgumentError, "stage #{name}:#{version} not found" \
125
+ if stage.nil?
126
+
127
+ stage_view = StageView.new(stage)
128
+ end
129
+
130
+ # Merge in the options specified in this job stage. If the
131
+ # on_success or on_failure stages are not already in the sequence,
132
+ # append them to the end.
133
+ stage_success = { stage: job_stage[:on_success] }
134
+ job_sequence << stage_success \
135
+ unless job_stage[:on_success].nil? or \
136
+ stage?(job_sequence, job_stage[:on_success])
137
+ stage_view.on_success = job_stage[:on_success]
138
+
139
+ stage_failure = { stage: job_stage[:on_failure] }
140
+ job_sequence << stage_failure \
141
+ unless job_stage[:on_failure].nil? or \
142
+ stage?(job_sequence, job_stage[:on_failure])
143
+ stage_view.on_failure = job_stage[:on_failure]
144
+
145
+ # Store the modified StageView
146
+ stages[stage_view.name.to_sym] = stage_view
147
+ end
148
+
149
+ return [stages, sequence]
150
+ end
151
+
152
+ # Search for a stage in the sequence, by name
153
+ def stage?(sequence, name)
154
+ found = false
155
+ sequence.each do |stage|
156
+ found = stage[:stage] == name || stage['stage'] == name
157
+ break if found
158
+ end
159
+ return found
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,275 @@
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 Job related classes
21
+ module Job
22
+ # Run a job
23
+ class Runner
24
+ include Constants::JobStatus
25
+
26
+ def initialize(job_id, job_definition, notifier)
27
+ # The notifier for updating the job status & writing to the log
28
+ # buffer
29
+ @notifier = notifier
30
+
31
+ # Un-serialize the job
32
+ begin
33
+ @job = Oj.load(job_definition, symbol_keys: true)
34
+
35
+ environment = @job[:environment]
36
+ secrets = @job[:secrets]
37
+ rescue StandardError => ex
38
+ Cyclid.logger.error "couldn't un-serialize job for job ID #{job_id}: #{ex}"
39
+ raise 'job failed'
40
+ end
41
+
42
+ # Create an initial job context (more will be added as the job runs)
43
+ @ctx = @job[:context]
44
+
45
+ @ctx[:job_id] = job_id
46
+ @ctx[:job_name] = @job[:name]
47
+ @ctx[:job_version] = @job[:version]
48
+ @ctx[:organization] = @job[:organization]
49
+ @ctx.merge! environment
50
+ @ctx.merge! secrets
51
+
52
+ begin
53
+ # We're off!
54
+ @notifier.status = WAITING
55
+
56
+ # Create a Builder
57
+ @builder = create_builder
58
+
59
+ # Obtain a host to run the job on
60
+ @notifier.write "#{Time.now} : Obtaining build host...\n"
61
+ @build_host = request_build_host(@builder, environment)
62
+
63
+ # We have a build host
64
+ @notifier.status = STARTED
65
+
66
+ # Add some build host details to the build context
67
+ @ctx.merge! @build_host.context_info
68
+
69
+ # Connect a transport to the build host; the notifier is a proxy
70
+ # to the log buffer
71
+ @transport = create_transport(@build_host, @notifier)
72
+
73
+ # Prepare the host
74
+ provisioner = create_provisioner(@build_host)
75
+
76
+ @notifier.write "#{Time.now} : Preparing build host...\n#{'=' * 79}\n"
77
+ provisioner.prepare(@transport, @build_host, environment)
78
+
79
+ # Check out sources
80
+ if @job[:sources].any?
81
+ @notifier.write "#{'=' * 79}\n#{Time.now} : Checking out source...\n"
82
+ checkout_sources(@transport, @ctx, @job[:sources])
83
+ end
84
+ rescue StandardError => ex
85
+ Cyclid.logger.error "job runner failed: #{ex}"
86
+
87
+ @notifier.status = FAILED
88
+ @notifier.ended = Time.now.to_s
89
+
90
+ begin
91
+ @builder.release(@transport, @build_host) if @build_host
92
+ @transport.close if @transport
93
+ rescue ::Net::SSH::Disconnect # rubocop:disable Lint/HandleExceptions
94
+ # Ignored
95
+ end
96
+
97
+ raise # XXX Raise an internal exception
98
+ end
99
+ end
100
+
101
+ # Run the stages.
102
+ #
103
+ # Start with the first stage, and execute all of the steps until
104
+ # either one fails, or there are no more steps. The follow the
105
+ # on_success & on_failure handlers to the next stage. If no
106
+ # handler is defined, stop.
107
+ def run
108
+ status = STARTED
109
+
110
+ @notifier.write "#{'=' * 79}\n#{Time.now} : Job started. " \
111
+ "Context: #{@ctx.stringify_keys}\n"
112
+
113
+ # Run the Job stage actions
114
+ stages = @job[:stages] || []
115
+ sequence = (@job[:sequence] || []).first
116
+
117
+ # Run each stage in the sequence until there are none left
118
+ until sequence.nil?
119
+ # Find the stage
120
+ raise 'stage not found' unless stages.key? sequence.to_sym
121
+
122
+ # Un-serialize the stage into a StageView
123
+ stage_definition = stages[sequence.to_sym]
124
+ stage = Oj.load(stage_definition, symbol_keys: true)
125
+
126
+ @notifier.write "#{'-' * 79}\n#{Time.now} : " \
127
+ "Running stage #{stage.name} v#{stage.version}\n"
128
+
129
+ # Run the stage
130
+ success, rc = run_stage(stage)
131
+
132
+ Cyclid.logger.info "stage #{(success ? 'succeeded' : 'failed')} and returned #{rc}"
133
+
134
+ # Decide which stage to run next depending on the outcome of this
135
+ # one
136
+ if success
137
+ sequence = stage.on_success
138
+ else
139
+ sequence = stage.on_failure
140
+
141
+ # Remember the failure while the failure handlers run
142
+ status = FAILING
143
+ @notifier.status = status
144
+ end
145
+ end
146
+
147
+ # Either all of the stages succeeded, and thus the job suceeded, or
148
+ # (at least one of) the stages failed, and thus the job failed
149
+ if status == FAILING
150
+ @notifier.status = FAILED
151
+ @notifier.ended = Time.now
152
+ success = false
153
+ else
154
+ @notifier.status = SUCCEEDED
155
+ @notifier.ended = Time.now
156
+ success = true
157
+ end
158
+
159
+ # We no longer require the build host & transport
160
+ begin
161
+ @builder.release(@transport, @build_host)
162
+ @transport.close
163
+ rescue ::Net::SSH::Disconnect # rubocop:disable Lint/HandleExceptions
164
+ # Ignored
165
+ end
166
+
167
+ return success
168
+ end
169
+
170
+ private
171
+
172
+ # Create a suitable Builder
173
+ def create_builder
174
+ # Each worker creates a new instance
175
+ builder = Cyclid.builder.new
176
+ raise "couldn't create a builder" \
177
+ unless builder
178
+
179
+ return builder
180
+ end
181
+
182
+ # Acquire a build host from the builder
183
+ def request_build_host(builder, environment)
184
+ # Request a BuildHost
185
+ build_host = builder.get(environment)
186
+ raise "couldn't obtain a build host" unless build_host
187
+
188
+ return build_host
189
+ end
190
+
191
+ # Find a transport that can be used with the build host, create one and
192
+ # connect them together
193
+ def create_transport(build_host, log_buffer)
194
+ # Create a Transport & connect it to the build host
195
+ host, username, password, key = build_host.connect_info
196
+ Cyclid.logger.debug "create_transport: host: #{host} " \
197
+ "username: #{username} " \
198
+ "password: #{password} " \
199
+ "key: #{key}"
200
+
201
+ # Try to match a transport that the host supports, to a transport we know how
202
+ # to create; transports should be listed in the order they're preferred.
203
+ transport_plugin = nil
204
+ build_host.transports.each do |t|
205
+ transport_plugin = Cyclid.plugins.find(t, Cyclid::API::Plugins::Transport)
206
+ end
207
+
208
+ raise "couldn't find a valid transport from #{build_host.transports}" \
209
+ unless transport_plugin
210
+
211
+ # Connect the transport to the build host
212
+ transport = transport_plugin.new(host: host,
213
+ user: username,
214
+ password: password,
215
+ key: key,
216
+ log: log_buffer)
217
+ raise 'failed to connect the transport' unless transport
218
+
219
+ return transport
220
+ end
221
+
222
+ # Find a provisioner that can be used with the build host and create
223
+ # one
224
+ def create_provisioner(build_host)
225
+ distro = build_host[:distro]
226
+
227
+ provisioner_plugin = Cyclid.plugins.find(distro, Cyclid::API::Plugins::Provisioner)
228
+ raise "couldn't find a valid provisioner for #{distro}" \
229
+ unless provisioner_plugin
230
+
231
+ provisioner = provisioner_plugin.new
232
+ raise 'failed to create provisioner' unless provisioner
233
+
234
+ return provisioner
235
+ end
236
+
237
+ # Find and create a suitable source plugin instance for each source and have it check out
238
+ # the given source using the transport.
239
+ def checkout_sources(transport, ctx, sources)
240
+ sources.each do |job_source|
241
+ raise 'no type given in source definition' unless job_source.key? :type
242
+
243
+ source = Cyclid.plugins.find(job_source[:type], Cyclid::API::Plugins::Source)
244
+ raise "can't find a plugin for #{job_source[:type]} source" if source.nil?
245
+
246
+ success = source.new.checkout(transport, ctx, job_source)
247
+ raise 'failed to check out source' unless success
248
+ end
249
+ end
250
+
251
+ # Perform each action defined in the steps of the given stage, until
252
+ # either an action fails or we run out of steps
253
+ def run_stage(stage)
254
+ stage.steps.each do |step|
255
+ begin
256
+ # Un-serialize the Action for this step
257
+ action = Oj.load(step[:action], symbol_keys: true)
258
+ rescue StandardError
259
+ Cyclid.logger.error "couldn't un-serialize action for job ID #{job_id}"
260
+ raise 'job failed'
261
+ end
262
+
263
+ # Run the action
264
+ action.prepare(transport: @transport, ctx: @ctx)
265
+ success, rc = action.perform(@notifier)
266
+
267
+ return [false, rc] unless success
268
+ end
269
+
270
+ return [true, 0]
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end