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,34 @@
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_rel 'auth/*.rb'
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
+ # Controller for all Auth related API endpoints
23
+ class AuthController < ControllerBase
24
+ register Sinatra::Namespace
25
+
26
+ namespace '/token' do
27
+ register Auth::Token
28
+ end
29
+ end
30
+
31
+ # Register this controller
32
+ Cyclid.controllers << AuthController
33
+ end
34
+ end
@@ -0,0 +1,78 @@
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 'jwt'
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 for all Auth related API endpoints
23
+ module Auth
24
+ # API endpoints for managing API tokens
25
+ # @api REST
26
+ module Token
27
+ # @!group Tokens
28
+
29
+ # @!method post_token_username
30
+ # @overload POST /token/:username
31
+ # @param [String] username Username of the user to generate a token for.
32
+ # @macro rest
33
+ # Generate a JSON Web Token for use with the Token authentication scheme.
34
+ # The user must authenticate using one of the other available methods
35
+ # (HTTP Basic or HMAC) to obtain a token.
36
+ # @return A JWT token.
37
+ # @return [404] The user does not exist
38
+
39
+ # @!endgroup
40
+
41
+ # Sinatra callback
42
+ # @private
43
+ def self.registered(app)
44
+ include Errors::HTTPErrors
45
+
46
+ app.post '/:username' do
47
+ authorized_as!(params[:username], Operations::READ)
48
+
49
+ payload = parse_request_body
50
+ Cyclid.logger.debug payload
51
+
52
+ user = User.find_by(username: params[:username])
53
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
54
+ if user.nil?
55
+
56
+ # Create a JSON Web Token. Use the provided payload as the intial
57
+ # set of claims but remove some of the standard claims we don't
58
+ # want users to be able to set.
59
+ #
60
+ # Requests MAY set 'exp' and 'nbf' if they wish.
61
+ payload.delete_if{ |k, _v| %w(iss aud jti iat sub).include? k }
62
+
63
+ # If 'exp' was not set, set it now. Default is +6 hours.
64
+ payload['exp'] = Time.now.to_i + 21_600_000 unless payload.key? 'exp'
65
+ # Subject is this user
66
+ payload['sub'] = params[:username]
67
+
68
+ # Create the token; use the users HMAC key as the signing key
69
+ token = JWT.encode payload, user.secret, 'HS256'
70
+
71
+ token_hash = { token: token }
72
+ return token_hash.to_json
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,96 @@
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
+ # Controller for all Health related API endpoints
23
+ # @api REST
24
+ class HealthController < ControllerBase
25
+ include Errors::HTTPErrors
26
+
27
+ def initialize(_app)
28
+ super
29
+ @checker = SinatraHealthCheck::Checker.new(logger: Cyclid.logger,
30
+ timeout: 0)
31
+
32
+ # Add internal health checks
33
+ @checker.systems[:database] = Cyclid::API::Health::Database
34
+
35
+ # Add each plugin, which can choose to provide a healthcheck by
36
+ # implementing #status
37
+ Cyclid.plugins.all.each do |plugin|
38
+ name = "#{plugin.human_name}_#{plugin.name}".to_sym
39
+ @checker.systems[name] = plugin
40
+ end
41
+ end
42
+
43
+ # @!group Health
44
+
45
+ # @!method get_health_status
46
+ # @overload GET /health/status
47
+ # @macro rest
48
+ # Return either 200 (healthy) or 503 (unhealthy) based on the status of
49
+ # the healthchecks. This is intended to be used by things like load
50
+ # balancers and active monitors.
51
+ # @return [200] Application is healthy.
52
+ # @return [503] Application is unhealthy.
53
+ get '/health/status' do
54
+ @checker.healthy? ? 200 : 503
55
+ end
56
+
57
+ # @!method get_health_info
58
+ # @overload GET /health/info
59
+ # @macro rest
60
+ # Return verbose information on the status of the individual checks;
61
+ # note that this method always returns 200 with a message body, so it is
62
+ # not suitable for general health checks unless the caller intends to
63
+ # parse the message body for the health status.
64
+ # @return JSON description of the individual health check statuses.
65
+ get '/health/info' do
66
+ @checker.status.to_json
67
+ end
68
+
69
+ # @!endgroup
70
+ end
71
+
72
+ # Register this controller
73
+ Cyclid.controllers << HealthController
74
+
75
+ # Healthchecks
76
+ module Health
77
+ # Internal database connection health check
78
+ module Database
79
+ # Check that ActiveRecord can connect to the database
80
+ def self.status
81
+ connected = begin
82
+ ActiveRecord::Base.connection_pool.with_connection(&:active?)
83
+ rescue
84
+ false
85
+ end
86
+
87
+ if connected
88
+ SinatraHealthCheck::Status.new(:ok, 'database connection is okay')
89
+ else
90
+ SinatraHealthCheck::Status.new(:error, 'database is not connected')
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,104 @@
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_rel 'organizations/*.rb'
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
+ # Controller for all Organization related API endpoints
23
+ class OrganizationController < ControllerBase
24
+ helpers do
25
+ # Clean up stage data
26
+ def sanitize_stage(stage)
27
+ stage.delete_if do |key, _value|
28
+ key == 'organization_id' || key == 'id'
29
+ end
30
+ end
31
+
32
+ # Clean up step data
33
+ def sanitize_step(step)
34
+ Cyclid.logger.debug step.inspect
35
+
36
+ # Re-factor the step definition back into the original. The internal
37
+ # representation carries a lot of extra metadata that users don't
38
+ # need to worry about; the original definition is actually the
39
+ # 'action' object inside of the step. The original 'action' key can
40
+ # be reversed from the Action object name, without having to
41
+ # unserialize the object
42
+ #
43
+ # Note that we use JSON and not Oj; if we try to use Oj it will
44
+ # attempt to unserialise the object, which is not what we want.
45
+ action = JSON.parse(step['action'])
46
+
47
+ # Reverse the action name from the object name and remove the object
48
+ # name
49
+ action['action'] = action['^o'].split('::').last.downcase
50
+ action.delete('^o')
51
+
52
+ return action
53
+ end
54
+
55
+ # Remove sensitive data from the organization data
56
+ def sanitize_organization(org)
57
+ org.delete_if do |key, _value|
58
+ key == 'rsa_private_key' || key == 'rsa_public_key' || key == 'salt'
59
+ end
60
+ end
61
+ end
62
+
63
+ register Sinatra::Namespace
64
+
65
+ namespace '/organizations' do
66
+ register Organizations::Collection
67
+
68
+ namespace '/:name' do
69
+ register Organizations::Document
70
+
71
+ namespace '/members' do
72
+ register Organizations::Members
73
+ end
74
+
75
+ namespace '/stages' do
76
+ register Organizations::Stages
77
+ end
78
+
79
+ namespace '/jobs' do
80
+ register Organizations::Jobs
81
+ end
82
+
83
+ namespace '/configs/:type' do
84
+ register Organizations::Configs
85
+ end
86
+
87
+ namespace '/plugins' do
88
+ Cyclid.plugins.all(Cyclid::API::Plugins::Api).each do |plugin|
89
+ Cyclid.logger.debug "Registering API extension plugin #{plugin.name}"
90
+
91
+ # Create a namespace for this plugin and register it
92
+ namespace "/#{plugin.name}" do
93
+ ctrl = plugin.controller
94
+ register ctrl
95
+ helpers ctrl.plugin_methods
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ Cyclid.controllers << OrganizationController
103
+ end
104
+ end
@@ -0,0 +1,134 @@
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 Organization related API endpoints
21
+ module Organizations
22
+ # API endpoints for the Organization collection
23
+ # @api REST
24
+ module Collection
25
+ # @!group Organizations
26
+
27
+ # @!method get_organizations
28
+ # @overload GET /organizations
29
+ # @macro rest
30
+ # Get all of the organizations.
31
+ # @return List of organizations
32
+ # @example Get a list of organizations
33
+ # GET /organizations => [{"id": 1, "name": "example", "owner_email": "admin@example.com"}]
34
+ # @see get_organizations_organization
35
+
36
+ # @!method post_organizations(body)
37
+ # @overload POST /organizations
38
+ # @macro rest
39
+ # Create a new organization.
40
+ # @param [JSON] body New organization
41
+ # @option body [String] name Name of the new organization
42
+ # @option body [String] owner_email Email address of the organization owner
43
+ # @option body [Array<String>] users ([]) List of users to add to the organization
44
+ # @return [200] Organization was created successfully
45
+ # @return [404] A user in the list of members does not exist
46
+ # @return [409] An organization with that name already exists
47
+ # @example Create a new organization with user1 & user2 as members
48
+ # POST /organizations <= {"name": "example",
49
+ # "owner_email": "admin@example.com",
50
+ # "users": ["user1", "user2"]}
51
+ # ***
52
+ # @example Create a new organization with no users as members
53
+ # POST /organizations <= {"name": "example",
54
+ # "owner_email": "admin@example.com"}
55
+ # ***
56
+
57
+ # @!endgroup
58
+
59
+ # Sinatra callback
60
+ # @private
61
+ def self.registered(app)
62
+ include Errors::HTTPErrors
63
+ include Constants
64
+
65
+ # Get all of the organizations.
66
+ app.get do
67
+ authorized_admin!(Operations::READ)
68
+
69
+ # Retrieve the organization data in a form we can more easily
70
+ # manipulate so that we can sanitize it
71
+ orgs = Organization.all_as_hash
72
+
73
+ # Remove any sensitive data
74
+ orgs.map! do |org|
75
+ sanitize_organization(org)
76
+ end
77
+
78
+ return orgs.to_json
79
+ end
80
+
81
+ # Create a new organization.
82
+ app.post do
83
+ authorized_admin!(Operations::ADMIN)
84
+
85
+ payload = parse_request_body
86
+ Cyclid.logger.debug payload
87
+
88
+ begin
89
+ halt_with_json_response(409, \
90
+ DUPLICATE, \
91
+ 'An organization with that name already exists') \
92
+ if Organization.exists?(name: payload['name'])
93
+
94
+ org = Organization.new
95
+ org['name'] = payload['name']
96
+ org['owner_email'] = payload['owner_email']
97
+
98
+ # Generate an RSA key-pair and a Salt
99
+ key = OpenSSL::PKey::RSA.new(RSA_KEY_LENGTH)
100
+
101
+ org['rsa_private_key'] = key.to_der
102
+ org['rsa_public_key'] = key.public_key.to_der
103
+
104
+ org['salt'] = SecureRandom.hex(32)
105
+
106
+ # Add each provided user to the Organization
107
+ users = payload['users'] || []
108
+
109
+ org.users = users.map do |username|
110
+ user = User.find_by(username: username)
111
+
112
+ halt_with_json_response(404, \
113
+ INVALID_USER, \
114
+ "user #{user} does not exist") \
115
+ if user.nil?
116
+
117
+ user
118
+ end
119
+
120
+ org.save!
121
+ rescue ActiveRecord::ActiveRecordError, \
122
+ ActiveRecord::UnknownAttributeError => ex
123
+
124
+ Cyclid.logger.debug ex.message
125
+ halt_with_json_response(400, INVALID_JSON, ex.message)
126
+ end
127
+
128
+ return json_response(NO_ERROR, "organization #{payload['name']} created")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end