twistlock-control 0.0.1
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/.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
|