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