cyclid 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +174 -0
- data/README.md +54 -0
- data/app/cyclid.rb +61 -0
- data/app/cyclid/config.rb +38 -0
- data/app/cyclid/controllers.rb +123 -0
- data/app/cyclid/controllers/auth.rb +34 -0
- data/app/cyclid/controllers/auth/token.rb +78 -0
- data/app/cyclid/controllers/health.rb +96 -0
- data/app/cyclid/controllers/organizations.rb +104 -0
- data/app/cyclid/controllers/organizations/collection.rb +134 -0
- data/app/cyclid/controllers/organizations/config.rb +128 -0
- data/app/cyclid/controllers/organizations/document.rb +135 -0
- data/app/cyclid/controllers/organizations/job.rb +266 -0
- data/app/cyclid/controllers/organizations/members.rb +145 -0
- data/app/cyclid/controllers/organizations/stages.rb +251 -0
- data/app/cyclid/controllers/users.rb +47 -0
- data/app/cyclid/controllers/users/collection.rb +131 -0
- data/app/cyclid/controllers/users/document.rb +133 -0
- data/app/cyclid/health_helpers.rb +40 -0
- data/app/cyclid/job.rb +3 -0
- data/app/cyclid/job/helpers.rb +67 -0
- data/app/cyclid/job/job.rb +164 -0
- data/app/cyclid/job/runner.rb +275 -0
- data/app/cyclid/job/stage.rb +67 -0
- data/app/cyclid/log_buffer.rb +104 -0
- data/app/cyclid/models.rb +3 -0
- data/app/cyclid/models/job_record.rb +25 -0
- data/app/cyclid/models/organization.rb +64 -0
- data/app/cyclid/models/plugin_config.rb +25 -0
- data/app/cyclid/models/stage.rb +42 -0
- data/app/cyclid/models/step.rb +29 -0
- data/app/cyclid/models/user.rb +60 -0
- data/app/cyclid/models/user_permissions.rb +28 -0
- data/app/cyclid/monkey_patches.rb +37 -0
- data/app/cyclid/plugin_registry.rb +75 -0
- data/app/cyclid/plugins.rb +125 -0
- data/app/cyclid/plugins/action.rb +48 -0
- data/app/cyclid/plugins/action/command.rb +89 -0
- data/app/cyclid/plugins/action/email.rb +207 -0
- data/app/cyclid/plugins/action/email/html.erb +58 -0
- data/app/cyclid/plugins/action/email/text.erb +13 -0
- data/app/cyclid/plugins/action/script.rb +90 -0
- data/app/cyclid/plugins/action/slack.rb +129 -0
- data/app/cyclid/plugins/action/slack/note.erb +5 -0
- data/app/cyclid/plugins/api.rb +195 -0
- data/app/cyclid/plugins/api/github.rb +111 -0
- data/app/cyclid/plugins/api/github/callback.rb +66 -0
- data/app/cyclid/plugins/api/github/methods.rb +201 -0
- data/app/cyclid/plugins/api/github/status.rb +67 -0
- data/app/cyclid/plugins/builder.rb +80 -0
- data/app/cyclid/plugins/builder/mist.rb +107 -0
- data/app/cyclid/plugins/dispatcher.rb +89 -0
- data/app/cyclid/plugins/dispatcher/local.rb +167 -0
- data/app/cyclid/plugins/provisioner.rb +40 -0
- data/app/cyclid/plugins/provisioner/debian.rb +90 -0
- data/app/cyclid/plugins/provisioner/ubuntu.rb +98 -0
- data/app/cyclid/plugins/source.rb +39 -0
- data/app/cyclid/plugins/source/git.rb +64 -0
- data/app/cyclid/plugins/transport.rb +63 -0
- data/app/cyclid/plugins/transport/ssh.rb +155 -0
- data/app/cyclid/sinatra/api_helpers.rb +66 -0
- data/app/cyclid/sinatra/auth_helpers.rb +127 -0
- data/app/cyclid/sinatra/warden/strategies/api_token.rb +62 -0
- data/app/cyclid/sinatra/warden/strategies/basic.rb +58 -0
- data/app/cyclid/sinatra/warden/strategies/hmac.rb +76 -0
- data/app/db.rb +51 -0
- data/bin/cyclid-db-init +107 -0
- data/db/schema.rb +92 -0
- data/lib/cyclid/app.rb +4 -0
- 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
|