twistlock-control 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rubocop.yml +6 -0
  4. data/Gemfile +16 -0
  5. data/Guardfile +30 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +47 -0
  8. data/Rakefile +1 -0
  9. data/features/provisioning_service_instances.feature +14 -0
  10. data/features/step_definitions/provisioning_service_instances_steps.rb +33 -0
  11. data/features/support/env.rb +22 -0
  12. data/lib/twistlock_control.rb +65 -0
  13. data/lib/twistlock_control/actions.rb +7 -0
  14. data/lib/twistlock_control/actions/container.rb +42 -0
  15. data/lib/twistlock_control/actions/container_instance.rb +28 -0
  16. data/lib/twistlock_control/actions/provisioner.rb +25 -0
  17. data/lib/twistlock_control/actions/service.rb +18 -0
  18. data/lib/twistlock_control/actions/service_instance.rb +40 -0
  19. data/lib/twistlock_control/collections.rb +22 -0
  20. data/lib/twistlock_control/entities.rb +11 -0
  21. data/lib/twistlock_control/entities/composite_service.rb +86 -0
  22. data/lib/twistlock_control/entities/container.rb +66 -0
  23. data/lib/twistlock_control/entities/container_instance.rb +22 -0
  24. data/lib/twistlock_control/entities/provisioner.rb +24 -0
  25. data/lib/twistlock_control/entities/provisioning_configuration.rb +65 -0
  26. data/lib/twistlock_control/entities/service.rb +19 -0
  27. data/lib/twistlock_control/entities/service_instance.rb +122 -0
  28. data/lib/twistlock_control/entity.rb +63 -0
  29. data/lib/twistlock_control/provisioner_api.rb +43 -0
  30. data/lib/twistlock_control/rethinkdb_repository.rb +74 -0
  31. data/lib/twistlock_control/version.rb +4 -0
  32. data/spec/actions/container_spec.rb +19 -0
  33. data/spec/actions/provisioner_spec.rb +37 -0
  34. data/spec/actions/service_instance_spec.rb +47 -0
  35. data/spec/collections_spec.rb +14 -0
  36. data/spec/entities/composite_service_spec.rb +126 -0
  37. data/spec/entities/container_spec.rb +8 -0
  38. data/spec/entities/provisioner_spec.rb +56 -0
  39. data/spec/entities/service_instance_spec.rb +33 -0
  40. data/spec/entities/shared_service_specs.rb +4 -0
  41. data/spec/provisioner_api_spec.rb +35 -0
  42. data/spec/rethinkdb_repository_spec.rb +15 -0
  43. data/spec/spec_helper.rb +25 -0
  44. data/twistlock-control.gemspec +29 -0
  45. 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,11 @@
1
+ require_relative 'entity'
2
+
3
+ %w(
4
+ provisioning_configuration
5
+ service
6
+ provisioner
7
+ composite_service
8
+ service_instance
9
+ container
10
+ container_instance
11
+ ).each { |entity| require_relative("entities/#{entity}") }
@@ -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