twistlock-control 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +6 -0
- data/Gemfile +16 -0
- data/Guardfile +30 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +1 -0
- data/features/provisioning_service_instances.feature +14 -0
- data/features/step_definitions/provisioning_service_instances_steps.rb +33 -0
- data/features/support/env.rb +22 -0
- data/lib/twistlock_control.rb +65 -0
- data/lib/twistlock_control/actions.rb +7 -0
- data/lib/twistlock_control/actions/container.rb +42 -0
- data/lib/twistlock_control/actions/container_instance.rb +28 -0
- data/lib/twistlock_control/actions/provisioner.rb +25 -0
- data/lib/twistlock_control/actions/service.rb +18 -0
- data/lib/twistlock_control/actions/service_instance.rb +40 -0
- data/lib/twistlock_control/collections.rb +22 -0
- data/lib/twistlock_control/entities.rb +11 -0
- data/lib/twistlock_control/entities/composite_service.rb +86 -0
- data/lib/twistlock_control/entities/container.rb +66 -0
- data/lib/twistlock_control/entities/container_instance.rb +22 -0
- data/lib/twistlock_control/entities/provisioner.rb +24 -0
- data/lib/twistlock_control/entities/provisioning_configuration.rb +65 -0
- data/lib/twistlock_control/entities/service.rb +19 -0
- data/lib/twistlock_control/entities/service_instance.rb +122 -0
- data/lib/twistlock_control/entity.rb +63 -0
- data/lib/twistlock_control/provisioner_api.rb +43 -0
- data/lib/twistlock_control/rethinkdb_repository.rb +74 -0
- data/lib/twistlock_control/version.rb +4 -0
- data/spec/actions/container_spec.rb +19 -0
- data/spec/actions/provisioner_spec.rb +37 -0
- data/spec/actions/service_instance_spec.rb +47 -0
- data/spec/collections_spec.rb +14 -0
- data/spec/entities/composite_service_spec.rb +126 -0
- data/spec/entities/container_spec.rb +8 -0
- data/spec/entities/provisioner_spec.rb +56 -0
- data/spec/entities/service_instance_spec.rb +33 -0
- data/spec/entities/shared_service_specs.rb +4 -0
- data/spec/provisioner_api_spec.rb +35 -0
- data/spec/rethinkdb_repository_spec.rb +15 -0
- data/spec/spec_helper.rb +25 -0
- data/twistlock-control.gemspec +29 -0
- metadata +172 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
# Collections is an interface for querying the collections
|
3
|
+
module Collections
|
4
|
+
class << self
|
5
|
+
def provisioners
|
6
|
+
Entities::Provisioner.repository.table
|
7
|
+
end
|
8
|
+
|
9
|
+
def services
|
10
|
+
Entities::Service.repository.table
|
11
|
+
end
|
12
|
+
|
13
|
+
def service_instances
|
14
|
+
Entities::ServiceInstance.repository.table
|
15
|
+
end
|
16
|
+
|
17
|
+
def container_instances
|
18
|
+
Entities::ContainerInstance.repository.table
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
module Entities
|
3
|
+
# A service link can for example be, the 'MySQL' container exposes a 'mysql' port.
|
4
|
+
# The 'RubyForum' container consumes this service by listening on the 'mysql' port.
|
5
|
+
# The accompanying ServiceLink would be:
|
6
|
+
# {
|
7
|
+
# provider_name: "MySQL",
|
8
|
+
# provider_port_name: "mysql",
|
9
|
+
# consumer_name: "RubyForum",
|
10
|
+
# consumer_port: "mysql"
|
11
|
+
# }
|
12
|
+
class ServiceLink < Entity
|
13
|
+
attribute :provider_name, String
|
14
|
+
attribute :consumer_name, String
|
15
|
+
attribute :provider_port_name, String
|
16
|
+
attribute :consumer_port_name, String
|
17
|
+
end
|
18
|
+
|
19
|
+
# A CompositeService is a service that consists of a number of services working together to
|
20
|
+
# provide a single service. For example a web forum service might consist of a MySQL service,
|
21
|
+
# for persistant storage, and a Ruby HTTP service that serves HTML sites and queries the storage.
|
22
|
+
# In the CompositeService you may choose to only expose the HTTP service, making it only possible
|
23
|
+
# to query the MySQL database through the Ruby application, which might be considered proper
|
24
|
+
# encapsulation.
|
25
|
+
#
|
26
|
+
# Relations between services are described by the links attribute. A link is characterized by
|
27
|
+
# a producer and a consumer, the consumer will connect to the producers provided service.
|
28
|
+
class CompositeService < Service
|
29
|
+
attribute :service_type, Symbol, default: :composite
|
30
|
+
attribute :id, String, default: :generate_id
|
31
|
+
attribute :name, String
|
32
|
+
|
33
|
+
# Link cases:
|
34
|
+
#
|
35
|
+
# 1. Multi-consumer: a webservice might have 10 Ruby frontend apps, all connecting
|
36
|
+
# to the same database. Simply increasing the amount of service instances with
|
37
|
+
# the same links will solve this case. Question: do we want to specify this
|
38
|
+
# possibility in each service link, or can we simply assume all services are
|
39
|
+
# scalable in this way? Not all are, but maybe that's a service property, not
|
40
|
+
# a relation property.
|
41
|
+
# 2. Multi-producer: A MongoDB cluster might have multiple master nodes. A Ruby
|
42
|
+
# frontend app may want to connect to any of these. The problem is that it will
|
43
|
+
# have to know before starting on which ports this potentially infinite number
|
44
|
+
# of servers has, and somehow choose between them. A solution might be to couple
|
45
|
+
# each Ruby app with a random master.
|
46
|
+
attribute :service_relations, Hash[String => String]
|
47
|
+
attribute :links, [ServiceLink]
|
48
|
+
|
49
|
+
# A provided service basically means this composite service exposes a port of one
|
50
|
+
# of its services. In the forum example, it could be the port 80 http service of
|
51
|
+
# the RubyForum container.
|
52
|
+
#
|
53
|
+
# {
|
54
|
+
# http: { RubyForum: 'http' }
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
attribute :provided_services, Hash[String => String]
|
58
|
+
|
59
|
+
def services
|
60
|
+
Service.find_with_ids(service_relations.values)
|
61
|
+
end
|
62
|
+
|
63
|
+
def containers
|
64
|
+
result = []
|
65
|
+
services = self.services.map(&:service)
|
66
|
+
composites = services.select { |s| s.service_type == :composite }
|
67
|
+
containers = services.select { |s| s.service_type == :container }
|
68
|
+
result += containers
|
69
|
+
composites.each do |c|
|
70
|
+
result += c.containers
|
71
|
+
end
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
def serialize
|
76
|
+
super.merge! links: links.map(&:attributes)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def generate_id
|
82
|
+
name.downcase.gsub(' ', '-')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module TwistlockControl
|
7
|
+
module Entities
|
8
|
+
# A RelatedServiceDescription is used to describe a provided
|
9
|
+
# or consumed service.
|
10
|
+
class RelatedServiceDescription < Entity
|
11
|
+
attribute :port
|
12
|
+
attribute :description
|
13
|
+
end
|
14
|
+
|
15
|
+
# A ContainerDescription represents the container description
|
16
|
+
# file that's used to describe properties of containers.
|
17
|
+
class ContainerDescription < Entity
|
18
|
+
attribute :name, String
|
19
|
+
attribute :description, String
|
20
|
+
attribute :provided_services, Hash[String => RelatedServiceDescription]
|
21
|
+
attribute :consumed_services, Hash[String => RelatedServiceDescription]
|
22
|
+
|
23
|
+
def serialize
|
24
|
+
provided_services = (provided_services || {}).inject({}) { |r, (k, v)| r[k] = v.attributes }
|
25
|
+
consumed_services = (consumed_services || {}).inject({}) { |r, (k, v)| r[k] = v.attributes }
|
26
|
+
attributes.dup.merge!(
|
27
|
+
provided_services: provided_services,
|
28
|
+
consumed_services: consumed_services
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A container is a service that can be provisioned on a Twistlock provisioner node.
|
34
|
+
class Container < Service
|
35
|
+
attribute :service_type, Symbol, default: :container
|
36
|
+
attribute :id, String, default: :generate_id
|
37
|
+
attribute :url, String
|
38
|
+
attribute :name, String
|
39
|
+
attribute :description, ContainerDescription
|
40
|
+
|
41
|
+
# The network services provided by this service. Each service is
|
42
|
+
# identified with a name, for example: offers HTTP on port 80.
|
43
|
+
def provided_services
|
44
|
+
description.provided_services
|
45
|
+
end
|
46
|
+
|
47
|
+
# The service can depend on any other services. For example it might
|
48
|
+
# require a MySQL service to be linked in on port 3047.
|
49
|
+
def consumed_services
|
50
|
+
description.consumed_services
|
51
|
+
end
|
52
|
+
|
53
|
+
def serialize
|
54
|
+
super.merge!(
|
55
|
+
description: description ? description.serialize : nil
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def generate_id
|
62
|
+
Digest::SHA256.hexdigest(url)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
module Entities
|
3
|
+
# A container instance represents a container currently
|
4
|
+
# running on a provisioner.
|
5
|
+
class ContainerInstance < PersistedEntity
|
6
|
+
repository RethinkDBRepository['container_instances']
|
7
|
+
|
8
|
+
attribute :id, String, default: :generate_id
|
9
|
+
|
10
|
+
# Attributes as dictated by provisioner
|
11
|
+
attribute :container_id
|
12
|
+
attribute :ip_address
|
13
|
+
attribute :provisioner_id
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def generate_id
|
18
|
+
Digest::SHA256.hexdigest("#{container_id}-#{provisioner_id}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module TwistlockControl
|
4
|
+
module Entities
|
5
|
+
# A provisioner is a machine capable of provisioning containers
|
6
|
+
class Provisioner < PersistedEntity
|
7
|
+
repository RethinkDBRepository['provisioners']
|
8
|
+
|
9
|
+
attribute :id, String, default: :generate_id
|
10
|
+
attribute :name, String
|
11
|
+
attribute :url, String
|
12
|
+
|
13
|
+
def self.api
|
14
|
+
@api ||= ProvisionerAPI.new(url)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate_id
|
20
|
+
Digest::SHA256.hexdigest(url)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
module Entities
|
3
|
+
# ProvisioningConfiguration holds service instance configuration that
|
4
|
+
# pertains to the provisioning of containers.
|
5
|
+
class ProvisioningConfiguration < Entity
|
6
|
+
attribute :service_id
|
7
|
+
|
8
|
+
def self.new(attrs)
|
9
|
+
if attrs['configurations'] || attrs[:configurations]
|
10
|
+
obj = CompositeConfiguration.allocate
|
11
|
+
else
|
12
|
+
obj = ContainerConfiguration.allocate
|
13
|
+
end
|
14
|
+
obj.send :initialize, attrs
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Maybe we want ContainerConfiguration to be an entity with its
|
19
|
+
# own repository, so we can simply refer to it by id.
|
20
|
+
# That will make getting events from the provisioner easier
|
21
|
+
class ContainerConfiguration < ProvisioningConfiguration
|
22
|
+
attribute :provisioner_id
|
23
|
+
attribute :container_instance_id
|
24
|
+
|
25
|
+
attribute :mount_points
|
26
|
+
attribute :environment_variables
|
27
|
+
|
28
|
+
def provisioner
|
29
|
+
@provisioner ||= Provisioner.find_by_id(provisioner_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def provisioner=(provisioner)
|
33
|
+
@provisioner = provisioner
|
34
|
+
@provisioner_id = provisioner.id
|
35
|
+
end
|
36
|
+
|
37
|
+
def container
|
38
|
+
@container ||= Container.find_by_id(service_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def container_instance
|
42
|
+
@container_instance ||= ContainerInstance.find_by_id(container_instance_id)
|
43
|
+
end
|
44
|
+
|
45
|
+
def container_configurations
|
46
|
+
[self]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Configuration for a composite service
|
51
|
+
class CompositeConfiguration < ProvisioningConfiguration
|
52
|
+
attribute :configurations, [ProvisioningConfiguration]
|
53
|
+
|
54
|
+
def serialize
|
55
|
+
serialized = super
|
56
|
+
serialized[:configurations] = configurations.map(&:serialize)
|
57
|
+
serialized
|
58
|
+
end
|
59
|
+
|
60
|
+
def container_configurations
|
61
|
+
configurations.flat_map(&:container_configurations)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
module Entities
|
3
|
+
# A Service class describes a provisionable network service.
|
4
|
+
class Service < PersistedEntity
|
5
|
+
repository RethinkDBRepository['services']
|
6
|
+
|
7
|
+
def self.deserialize(attrs)
|
8
|
+
return nil if attrs.nil?
|
9
|
+
|
10
|
+
case attrs['service_type']
|
11
|
+
when 'container' then Container.new(attrs)
|
12
|
+
when 'composite' then CompositeService.new(attrs)
|
13
|
+
else
|
14
|
+
fail "Unknown service_type: #{attrs[:service_type]}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module TwistlockControl
|
2
|
+
module Entities
|
3
|
+
# A service instance is an entity that represents an instance of a service
|
4
|
+
# that can be started and stopped. For example, an operator might define a Forum
|
5
|
+
# service and then spawn a Forum service instance for each of his customers.
|
6
|
+
# Each of the Forum services can be referenced by name and stopped and started
|
7
|
+
# independantly, and consist of separate container instances.
|
8
|
+
#
|
9
|
+
# A service instance has all runtime configuration such as mount points and
|
10
|
+
# environment variables.
|
11
|
+
#
|
12
|
+
# An operator should be able to assign containers to provisioners, and configure
|
13
|
+
# their runtime configuration.
|
14
|
+
#
|
15
|
+
# The configuration has a tree structure. For each composite service there will
|
16
|
+
# be a branch element, for every container a leaf.
|
17
|
+
class ServiceInstance < PersistedEntity
|
18
|
+
repository RethinkDBRepository['service_instances']
|
19
|
+
|
20
|
+
attribute :id, String, default: :generate_id
|
21
|
+
attribute :name, String
|
22
|
+
attribute :service_id, String
|
23
|
+
attribute :configuration, ProvisioningConfiguration
|
24
|
+
|
25
|
+
# We want to tell all containers how they are linked to eachother.
|
26
|
+
# Composite services have the information about which links exist.
|
27
|
+
# How many instances there are of a container should be configured
|
28
|
+
# at runtime. Can we just do it by adding ContainerConfigurations
|
29
|
+
# to a CompositeConfiguration? That would mean the build_configuration
|
30
|
+
# method would have to only build composite configurations, leaving
|
31
|
+
# the filling in of container configurations to the interactive
|
32
|
+
# resource allocation process. I.E. the user would create a composite
|
33
|
+
# configuration, then for each container needed of each composite
|
34
|
+
# service they would select on which machine(s) any containers will
|
35
|
+
# be ran. When a container configuration is created it can be
|
36
|
+
# determined to which other container configuration it is linked.
|
37
|
+
#
|
38
|
+
# So the next step is to change build_configuration to reflect that,
|
39
|
+
# then we add methods to CompositeConfiguration that allow to convenient
|
40
|
+
# addition of ContainerConfigurations. Including a way to enumerate
|
41
|
+
# which containers are needed.
|
42
|
+
#
|
43
|
+
# We also need to think about the linking, at the moment the provisioner
|
44
|
+
# can link a container to any ip address. When the containers are
|
45
|
+
# on separate machines, we can not usually link the containers directly
|
46
|
+
# on ip, a link would first have to be established. I envisioned this
|
47
|
+
# would ideally be through a simple TLS tunnel established by an ambassador
|
48
|
+
# container.
|
49
|
+
#
|
50
|
+
# If we would go for the ambassador approach the Twistlock system would
|
51
|
+
# have to be aware of this as it would have to provision ambassador nodes
|
52
|
+
# and use the ip addresses of the ambassador nodes to connect across machines.
|
53
|
+
#
|
54
|
+
# Alternatively, we could assume all machines in the cluster are in the
|
55
|
+
# same IP space and simply link them together. This would move the encryption
|
56
|
+
# and network management to a separate level and would ideally be a superior
|
57
|
+
# architecture, but in practice there is no simple way of achieving this
|
58
|
+
# in a way that is compatible with all container providers and all hosts.
|
59
|
+
# Since we want Twistlock to be an easy to deploy integrated solution,
|
60
|
+
# Twistlock would have to supply an automatic way of configuring such a
|
61
|
+
# datacenter without messing with existing architecture too much. A complex
|
62
|
+
# task that's not guaranteed to have a perfect solution.
|
63
|
+
#
|
64
|
+
# We could also for now simply assume a flat ip space, and work on the
|
65
|
+
# ambassador system later. A downside of that is that we might miss some
|
66
|
+
# architectural decision would enable the ambassador system to be more neatly
|
67
|
+
# integrated. So let's thing about the ambassador approach first.
|
68
|
+
#
|
69
|
+
# During the provisioning assignment step, it would become clear on which
|
70
|
+
# machine a process is going to be provisioned. If a linked container is
|
71
|
+
# on a different machine, the system detects it and links the container
|
72
|
+
# into an ambassador.
|
73
|
+
#
|
74
|
+
# Easiest will be to have a single ambassador per host. We could inform the
|
75
|
+
# ambassador of a cross-machine link via a HTTP POST, to which it would respond
|
76
|
+
# with the port numbers it will use for the link.
|
77
|
+
# Nice thing about this approach is that we can simply assume the ambassador
|
78
|
+
# is always there, so no complex logic for spawning it. Also it would be really
|
79
|
+
# easy to disable it and work with a flat ip space.
|
80
|
+
#
|
81
|
+
# So now the provisioning assignment step will be like this, for each container:
|
82
|
+
# - pick a machine it will run on
|
83
|
+
# - select any mounts
|
84
|
+
# - configure any environment variables
|
85
|
+
#
|
86
|
+
# Then the provisioning preparation process will be:
|
87
|
+
# - make sure all hosts have container descriptions/images
|
88
|
+
# - determine all cross-machine links
|
89
|
+
# - inform ambassadors of links
|
90
|
+
#
|
91
|
+
# Then the provisioning process itself:
|
92
|
+
# - start each container on its host
|
93
|
+
# - fill in ip-addresses of container instances
|
94
|
+
# - whenever a container is live, determine if any of its links can be
|
95
|
+
# established, and if so establish them
|
96
|
+
#
|
97
|
+
# Basically the only thing we need to make sure in the architecture is that it can
|
98
|
+
# be deduced from the link information whether the link is remote or local, so
|
99
|
+
# the system can decide if it has to go through an ambassador.
|
100
|
+
|
101
|
+
def container_configurations
|
102
|
+
configuration.container_configurations
|
103
|
+
end
|
104
|
+
|
105
|
+
def service
|
106
|
+
Service.find_by_id(service_id)
|
107
|
+
end
|
108
|
+
|
109
|
+
def serialize
|
110
|
+
serialized = attributes.dup
|
111
|
+
serialized[:configuration] = configuration.serialize
|
112
|
+
serialized
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def generate_id
|
118
|
+
name
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|