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