Skip to content

Commit d864570

Browse files
committed
Create orchestrator for system dependencies
1 parent 7d4d6a6 commit d864570

11 files changed

Lines changed: 413 additions & 6 deletions

File tree

.gitlab-ci.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,12 @@ boot:docker:
130130
DOCKER_TLS_CERTDIR: /certs
131131
DOCKER_HOST: tcp://docker:2376
132132
script:
133-
- docker compose -f docker-compose.dev.yml up -d
133+
- docker network create sagittarius_default
134134
- >
135135
docker run
136136
--name sagittarius
137137
--network sagittarius_default
138138
--network-alias sagittarius
139-
-e SAGITTARIUS_DATABASE_HOST=postgresql
140-
-e SAGITTARIUS_DATABASE_PORT=5432
141-
-e SAGITTARIUS_REDIS_HOST=redis
142-
-e SAGITTARIUS_REDIS_PORT=6379
143139
-e RAILS_ENV=production
144140
ghcr.io/code0-tech/sagittarius/ci-builds:${CI_COMMIT_SHA} &
145141
- >

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Lint/AmbiguousBlockAssociation:
1717
Metrics/AbcSize:
1818
Enabled: false
1919

20+
Metrics/BlockLength:
21+
Exclude:
22+
- lib/tasks/**/*
23+
2024
Metrics/MethodLength:
2125
Enabled: false
2226

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ gem 'sidekiq', '~> 7.1'
7474
gem 'lograge', '~> 0.14.0'
7575

7676
gem 'declarative_policy', '~> 1.1'
77+
78+
gem 'docker-api', '~> 2.2'

Gemfile.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,13 @@ GEM
102102
declarative_policy (1.1.0)
103103
diff-lcs (1.5.1)
104104
docile (1.4.0)
105+
docker-api (2.2.0)
106+
excon (>= 0.47.0)
107+
multi_json
105108
drb (2.2.1)
106109
erubi (1.12.0)
107110
escape_utils (1.3.0)
111+
excon (0.110.0)
108112
extended-markdown-filter (0.7.0)
109113
html-pipeline (~> 2.9)
110114
factory_bot (6.4.5)
@@ -153,6 +157,7 @@ GEM
153157
mini_mime (1.1.5)
154158
minitest (5.22.3)
155159
msgpack (1.7.2)
160+
multi_json (1.15.0)
156161
mutex_m (0.2.0)
157162
net-imap (0.4.10)
158163
date
@@ -336,6 +341,7 @@ DEPENDENCIES
336341
database_cleaner-active_record (~> 2.1)
337342
debug
338343
declarative_policy (~> 1.1)
344+
docker-api (~> 2.2)
339345
factory_bot_rails (~> 6.2)
340346
graphql (~> 2.1)
341347
graphql-docs (~> 4.0)

bin/docker-entrypoint

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
#!/bin/bash -e
22

3-
# If running the rails server then create or migrate existing database
43
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
4+
# orchestrate dependencies
5+
bundle exec rake orchestrator:start[postgresql] \
6+
orchestrator:start[redis] \
7+
orchestrator:await_healthy[postgresql] \
8+
orchestrator:await_healthy[redis]
9+
10+
# create connection environment variables
11+
eval $(bundle exec orchestrator:create_connection_environment[postgresql,redis])
12+
13+
# prepare database
514
bundle exec rake db:prepare
615
FILTER=01_application_settings bundle exec rake db:seed_fu
716
fi
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
module Sagittarius
4+
module Orchestrator
5+
class Container
6+
attr_accessor :internal_container
7+
8+
def image
9+
raise NotImplementedError
10+
end
11+
12+
def name
13+
raise NotImplementedError
14+
end
15+
16+
def cmd
17+
nil
18+
end
19+
20+
def environment_variables
21+
[]
22+
end
23+
24+
def volumes
25+
[]
26+
end
27+
28+
def ports
29+
nil
30+
end
31+
32+
def healthy?
33+
return false if internal_container.nil?
34+
35+
internal_container.refresh!
36+
internal_container.info['State']['Status'] == 'running'
37+
end
38+
39+
def self.[](container)
40+
Containers.const_get(container.capitalize).new
41+
end
42+
end
43+
end
44+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module Sagittarius
4+
module Orchestrator
5+
module Containers
6+
class Postgresql < Container
7+
def image
8+
'postgres:16.1'
9+
end
10+
11+
def name
12+
'postgresql'
13+
end
14+
15+
def environment_variables
16+
config = ActiveRecord::Base.connection_db_config.configuration_hash
17+
18+
%W[
19+
POSTGRES_USER=#{config[:username]}
20+
POSTGRES_PASSWORD=#{config[:password]}
21+
POSTGRES_DB=#{config[:database]}
22+
]
23+
end
24+
25+
def volumes
26+
{
27+
data: '/var/lib/postgresql/data/',
28+
}
29+
end
30+
31+
def ports
32+
{
33+
'5432/tcp' => [{ 'HostIp' => '127.0.0.1', 'HostPort' => '5433' }],
34+
}
35+
end
36+
37+
def healthy?
38+
# exec returns [[stdout], [], exit_code]
39+
super && internal_container.exec(%w[pg_isready]).last.zero?
40+
end
41+
42+
def orchestrator_connection_details
43+
return {} unless healthy?
44+
45+
{
46+
SAGITTARIUS_DATABASE_HOST: internal_container.info['Name'].delete_prefix('/'),
47+
SAGITTARIUS_DATABASE_PORT: 5432,
48+
}
49+
end
50+
end
51+
end
52+
end
53+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
module Sagittarius
4+
module Orchestrator
5+
module Containers
6+
class Redis < Container
7+
def image
8+
'redis:7.2.3-alpine'
9+
end
10+
11+
def name
12+
'redis'
13+
end
14+
15+
def cmd
16+
%w[redis-server --save 1 1 --loglevel warning]
17+
end
18+
19+
def volumes
20+
{
21+
data: '/data',
22+
}
23+
end
24+
25+
def ports
26+
{
27+
'6379/tcp' => [{ 'HostIp' => '127.0.0.1', 'HostPort' => '6380' }],
28+
}
29+
end
30+
31+
def orchestrator_connection_details
32+
return {} unless healthy?
33+
34+
{
35+
SAGITTARIUS_REDIS_HOST: internal_container.info['Name'].delete_prefix('/'),
36+
SAGITTARIUS_REDIS_PORT: 6379,
37+
}
38+
end
39+
end
40+
end
41+
end
42+
end
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
module Sagittarius
4+
module Orchestrator
5+
class Operator
6+
ORCHESTRATOR_LABEL_PREFIX = 'tech.code0.sagittarius.orchestrator'
7+
VOLUME_NAME_LABEL = "#{ORCHESTRATOR_LABEL_PREFIX}.volume.name".freeze
8+
VOLUME_CONTAINER_LABEL = "#{ORCHESTRATOR_LABEL_PREFIX}.volume.container".freeze
9+
10+
class << self
11+
def ensure_self_connected!
12+
ensure_network!
13+
14+
self_container_id = State.self_container_id
15+
return if self_container_id.nil?
16+
17+
network.connect(State.self_container_id)
18+
end
19+
20+
def ensure_container_up!(container)
21+
ensure_network!
22+
create_container!(container) if State[container.name].internal_container.nil?
23+
24+
container.internal_container.refresh!
25+
26+
unless container.internal_container.info['NetworkSettings']['Networks'].key?(network.info['Name'])
27+
network.connect(container.internal_container.id)
28+
end
29+
container.internal_container.start
30+
end
31+
32+
def ensure_container_down!(container)
33+
destroy_container!(container) unless State[container.name].internal_container.nil?
34+
end
35+
36+
private
37+
38+
def ensure_network!
39+
return unless network.nil?
40+
41+
Docker::Network.create(
42+
unique_name('code0'),
43+
'Labels' => { ORCHESTRATOR_LABEL_PREFIX => 'network' }
44+
)
45+
end
46+
47+
def network
48+
Docker::Network.all(filters: JSON.dump({ 'label' => ["#{ORCHESTRATOR_LABEL_PREFIX}=network"] })).first
49+
end
50+
51+
def ensure_volumes!(container)
52+
container.volumes.each_key do |name|
53+
next if State.volumes[container.name]&.key?(name.to_s)
54+
55+
Docker::Volume.create(
56+
unique_name("#{container.name}_#{name}"),
57+
'Labels' => { VOLUME_NAME_LABEL => name, VOLUME_CONTAINER_LABEL => container.name }
58+
)
59+
end
60+
61+
State.build_volumes!
62+
end
63+
64+
def create_container!(container)
65+
ensure_volumes!(container)
66+
volumes = container.volumes.map do |name, path|
67+
"#{State.volumes[container.name][name.to_s].info['Name']}:#{path}"
68+
end
69+
70+
Docker::Image.create('fromImage' => container.image)
71+
container.internal_container = Docker::Container.create(
72+
'name' => unique_name(container.name),
73+
'Image' => container.image,
74+
'Labels' => { ORCHESTRATOR_LABEL_PREFIX => container.name },
75+
'Cmd' => container.cmd,
76+
'Env' => container.environment_variables,
77+
'HostConfig' => {
78+
'Binds' => volumes,
79+
'PortBindings' => container.ports,
80+
}
81+
)
82+
end
83+
84+
def destroy_container!(container)
85+
container.internal_container.stop
86+
container.internal_container.delete
87+
container.internal_container = nil
88+
end
89+
90+
def unique_name(component)
91+
"#{component}_#{SecureRandom.hex}"
92+
end
93+
end
94+
end
95+
end
96+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module Sagittarius
4+
module Orchestrator
5+
class State
6+
class << self
7+
attr_accessor :containers, :volumes
8+
9+
def build!
10+
build_containers!
11+
build_volumes!
12+
end
13+
14+
def build_containers!
15+
docker_containers = Docker::Container.all(
16+
all: true,
17+
filters: JSON.dump({ 'label' => [Operator::ORCHESTRATOR_LABEL_PREFIX] })
18+
).to_h do |c|
19+
label = c.info['Labels'][Operator::ORCHESTRATOR_LABEL_PREFIX]
20+
next [nil, nil] if label.nil?
21+
22+
[label, c]
23+
end
24+
25+
@containers = Container.descendants.to_h do |clazz|
26+
container = clazz.new
27+
container.internal_container = docker_containers[container.name]
28+
[container.name, container]
29+
end
30+
end
31+
32+
def build_volumes!
33+
@volumes = Docker::Volume.all(
34+
filters: JSON.dump({ 'label' => [Operator::VOLUME_NAME_LABEL] })
35+
).each_with_object({}) do |v, obj|
36+
name_label = v.info.dig('Labels', Operator::VOLUME_NAME_LABEL)
37+
container_label = v.info.dig('Labels', Operator::VOLUME_CONTAINER_LABEL)
38+
next if name_label.nil? || container_label.nil?
39+
40+
obj[container_label] ||= {}
41+
obj[container_label][name_label] = v
42+
end
43+
end
44+
45+
def [](container_name)
46+
@containers[container_name]
47+
end
48+
49+
def self_container_id
50+
container_id_from_cgroup
51+
end
52+
53+
private
54+
55+
def container_id_from_cgroup
56+
File.readlines('/proc/self/cgroup').find { |line| line.include?('docker') }&.split('/')&.last&.chomp
57+
rescue Errno::ENOENT
58+
nil
59+
end
60+
end
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)