envirobly-orchestra 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 27540543de48daec3396d69ad5ce05079fefd25a0f76a092c914a8e2873a80e7
4
+ data.tar.gz: 96d0c5e27e028b8c0cc06df90c81bc111a5efe4d591c1926d201773443bc9e11
5
+ SHA512:
6
+ metadata.gz: 803c22a7247aa5e69a133436924146cb94d2d7d7f56258e446154fac304fcfdba4d4b60f84a1b9407af1505d12eb40946bb515ce19f1a6354b6ae73f178fc82a
7
+ data.tar.gz: d16b2d572fbb2acd1357c733af6083ea5020cc82eee376c771ffcab3ec5a795bccd9cb948f558247fcec5c27e02950a44add0d7247d0f497da8bb1f856d6bb55
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Robert Starsi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/orchestra ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "orchestra"
4
+ require "securerandom"
5
+
6
+ Thread.current[:invocation] = SecureRandom.hex 4
7
+
8
+ Orchestra::Cli::Main.start(ARGV)
@@ -0,0 +1,44 @@
1
+ require "thor"
2
+
3
+ class Orchestra::Base < Thor
4
+ def self.exit_on_failure?
5
+ true
6
+ end
7
+
8
+ private
9
+ LOCKFILE_PATH = "/tmp/orchestra.lock"
10
+ LOCK_RETRY_INTERVAL = 2
11
+ def with_lock
12
+ File.open(LOCKFILE_PATH, File::CREAT | File::EXCL | File::WRONLY) do
13
+ yield
14
+ end
15
+ rescue Errno::EEXIST
16
+ log "Execution lock in place, retrying in #{LOCK_RETRY_INTERVAL}s"
17
+
18
+ sleep LOCK_RETRY_INTERVAL
19
+
20
+ retry
21
+ ensure
22
+ if File.exist? LOCKFILE_PATH
23
+ File.delete LOCKFILE_PATH
24
+
25
+ log "lock released"
26
+ end
27
+ end
28
+
29
+ def log(text)
30
+ Orchestra::Logger.instance.log "inv-#{Thread.current[:invocation]} #{text}"
31
+ end
32
+
33
+ def execute(cmd, exit_on_failure: true)
34
+ stdout_and_err, status = Open3.capture2e *cmd
35
+
36
+ log stdout_and_err unless stdout_and_err.blank?
37
+
38
+ if exit_on_failure && status.to_i > 0
39
+ exit 1
40
+ end
41
+
42
+ status.to_i == 0
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ require "open3"
2
+ require "pathname"
3
+ require "benchmark"
4
+
5
+ class Orchestra::Cli::Images < Orchestra::Base
6
+ desc "build", "Build and push a Docker image"
7
+ method_option :git_url, type: :string, required: true
8
+ method_option :commit_id, type: :string, required: true
9
+ method_option :cache_bucket, type: :string, required: true
10
+ method_option :cache_region, type: :string, required: true
11
+ method_option :image_uri, type: :string, required: true
12
+ method_option :dockerfile, type: :string, required: true
13
+ method_option :build_context, type: :string, required: true
14
+ def build
15
+ status = 1
16
+
17
+ puts "Checking out commit #{options.commit_id}..."
18
+
19
+ checkout_time = Benchmark.realtime do
20
+ output, status =
21
+ Open3.capture2e "envirobly-git-checkout-commit", git_checkout_path.to_s, options.git_url, options.commit_id
22
+
23
+ puts output
24
+ end
25
+
26
+ puts "Checkout finished in #{checkout_time.to_i}s\n"
27
+
28
+ $stdout.flush
29
+
30
+ exit 1 if status.to_i > 0
31
+
32
+ exec *buildx_build_cmd
33
+ end
34
+
35
+ private
36
+ def buildx_build_cmd
37
+ [
38
+ "docker", "buildx", "build",
39
+ "--progress=plain",
40
+ "--cache-from=type=s3,region=#{options.cache_region},bucket=#{options.cache_bucket},name=app",
41
+ "--cache-to=type=s3,region=#{options.cache_region},bucket=#{options.cache_bucket},name=app,mode=max",
42
+ "-t", options.image_uri, "--push",
43
+ "-f", dockerfile_path.to_s,
44
+ build_context.to_s
45
+ ]
46
+ end
47
+
48
+ def git_checkout_path
49
+ Pathname.new "/tmp/build"
50
+ end
51
+
52
+ def build_context
53
+ git_checkout_path.join options.build_context
54
+ end
55
+
56
+ def dockerfile_path
57
+ git_checkout_path.join options.dockerfile
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ class Orchestra::Cli::Main < Orchestra::Base
2
+ desc "version", "Show Orchestra version"
3
+ def version
4
+ puts Orchestra::VERSION
5
+ end
6
+
7
+ desc "docker-version", "Show Docker version"
8
+ def docker_version
9
+ puts `docker -v`
10
+ end
11
+
12
+ desc "services", "Manage services"
13
+ subcommand "services", Orchestra::Cli::Services
14
+
15
+ desc "images", "Docker images"
16
+ subcommand "images", Orchestra::Cli::Images
17
+
18
+ desc "stack", "Manage stack"
19
+ subcommand "stack", Orchestra::Cli::Stack
20
+ end
@@ -0,0 +1,171 @@
1
+ require "open3"
2
+ # require "httpx"
3
+ require "pathname"
4
+
5
+ class Orchestra::Cli::Services < Orchestra::Base
6
+ desc "up", "Start services based on config synced from config bucket"
7
+ method_option :config_dir, type: :string, required: true
8
+ method_option :config_bucket, type: :string, required: true
9
+ method_option :config_region, type: :string, required: true
10
+ def up
11
+ with_lock do
12
+ log "services up"
13
+
14
+ execute sync_config_files_cmd
15
+ execute compose_prepare_cmd
16
+ initialize_data_volumes
17
+ execute compose_up_cmd
18
+ end
19
+ end
20
+
21
+ desc "down", "Stop services defined in supplied compose file"
22
+ method_option :config_dir, type: :string, required: true
23
+ def down
24
+ with_lock do
25
+ log "services down"
26
+
27
+ execute compose_down_cmd
28
+ end
29
+ end
30
+
31
+ desc "lock_test", "Test locking"
32
+ def lock_test
33
+ with_lock do
34
+ log "Executing something with lock for 15 seconds..."
35
+ sleep 15
36
+ log "Lock test done."
37
+ end
38
+ end
39
+
40
+ private
41
+ # def list_containers
42
+ # http = HTTPX.with(transport: "unix", addresses: ["/var/run/docker.sock"])
43
+
44
+ # response = http.get("http://localhost/v1.43/containers/json")
45
+
46
+ # response.body
47
+ # end
48
+
49
+ # https://docs.docker.com/engine/reference/commandline/compose_up/
50
+ def compose_up_cmd
51
+ [
52
+ "docker", "compose",
53
+ "-f", compose_file_path,
54
+ "up",
55
+ "--quiet-pull",
56
+ "--remove-orphans",
57
+ "--detach",
58
+ "--wait"
59
+ ]
60
+ end
61
+
62
+ def compose_prepare_cmd
63
+ [
64
+ "docker", "compose",
65
+ "-f", compose_file_path,
66
+ "up",
67
+ "--quiet-pull",
68
+ "--detach",
69
+ "--no-start"
70
+ ]
71
+ end
72
+
73
+ def compose_down_cmd
74
+ [
75
+ "docker", "compose",
76
+ "-f", compose_file_path,
77
+ "down",
78
+ "--remove-orphans"
79
+ ]
80
+ end
81
+
82
+ def compose_file_path
83
+ Pathname.new(options.config_dir).join("compose.yml").to_s
84
+ end
85
+
86
+ def sync_config_files_cmd
87
+ [
88
+ "aws", "s3",
89
+ "cp", "--recursive", "--no-progress",
90
+ "--region", options.config_region,
91
+ "s3://#{options.config_bucket}",
92
+ options.config_dir
93
+ ]
94
+ end
95
+
96
+ def compose_definition
97
+ @compose_definition ||= YAML.load ERB.new(File.read(compose_file_path)).result, aliases: true
98
+ end
99
+
100
+ def initialize_data_volumes
101
+ compose_definition["services"].each do |_, service|
102
+ next if service.dig("labels", "envirobly.data-volume.pool").blank?
103
+
104
+ zpool = service["labels"]["envirobly.data-volume.pool"]
105
+ mountpoint = service["labels"]["envirobly.data-volume.pool.mountpoint"]
106
+ zfs_dataset = service["labels"]["envirobly.data-volume.dataset"]
107
+ dataset_mountpoint = service["labels"]["envirobly.data-volume.dataset.mountpoint"]
108
+ device = service["labels"]["envirobly.data-volume.device"]
109
+
110
+ if zfs_dataset_exist?(zfs_dataset)
111
+ log " ZFS dataset exists"
112
+ next
113
+ end
114
+
115
+ log "Looking for block device '#{device}'..."
116
+
117
+ wait_for_block_device device
118
+
119
+ log "Block device '#{device}' found"
120
+
121
+ if execute zpool_import_cmd(zpool), exit_on_failure: false
122
+ log "ZFS pool '#{zpool}' successfully imported"
123
+ else
124
+ execute zpool_create_cmd(zpool, device, mountpoint:)
125
+ execute set_zpool_compression_cmd(zpool)
126
+ execute create_zfs_dataset_cmd(zfs_dataset)
127
+ chmod_zfs_dataset_mountpoint(dataset_mountpoint)
128
+
129
+ log "Created new ZFS pool and dataset '#{zfs_dataset}'"
130
+ end
131
+ end
132
+ end
133
+
134
+ def zfs_dataset_exist?(dataset)
135
+ execute ["zfs", "list", dataset], exit_on_failure: false
136
+ end
137
+
138
+ def wait_for_block_device(device)
139
+ counter = 1
140
+
141
+ while !File.blockdev?(device) do
142
+ if counter > 15
143
+ log "Block device '#{device}' not found within 15s. Aborting"
144
+ exit 1
145
+ end
146
+
147
+ counter += 1
148
+ sleep 1
149
+ end
150
+ end
151
+
152
+ def zpool_import_cmd(pool)
153
+ ["zpool", "import", pool]
154
+ end
155
+
156
+ def zpool_create_cmd(pool, device, mountpoint:)
157
+ ["zpool", "create", "-m", mountpoint, pool, device]
158
+ end
159
+
160
+ def set_zpool_compression_cmd(pool)
161
+ ["zfs", "set", "compression=lz4", pool]
162
+ end
163
+
164
+ def create_zfs_dataset_cmd(dataset)
165
+ ["zfs", "create", dataset]
166
+ end
167
+
168
+ def chmod_zfs_dataset_mountpoint(path)
169
+ FileUtils.chmod 0777, path
170
+ end
171
+ end
@@ -0,0 +1,6 @@
1
+ class Orchestra::Cli::Stack < Orchestra::Base
2
+ desc "prune", "Stop inactive services and clean up their data volumes"
3
+ def prune
4
+ # docker compose up -d --quiet-pull --no-recreate --remove-orphans --no-start
5
+ end
6
+ end
@@ -0,0 +1 @@
1
+ module Orchestra::Cli; end
@@ -0,0 +1,28 @@
1
+ require "singleton"
2
+
3
+ class Orchestra::Logger
4
+ include Singleton
5
+
6
+ LOG_FILE_PATH = "/var/log/orchestra.log"
7
+ TIMESTAMP_FORMAT = "%Y%m%dT%H%M%SZ"
8
+
9
+ def initialize
10
+ @log_file = File.open(LOG_FILE_PATH, "a")
11
+ end
12
+
13
+ def log(text)
14
+ timestamp = Time.now.strftime(TIMESTAMP_FORMAT)
15
+ log_message = "#{timestamp} #{text}"
16
+
17
+ # Append to the log file
18
+ @log_file.puts(log_message)
19
+ @log_file.flush
20
+
21
+ # Print to stdout
22
+ puts log_message
23
+ end
24
+
25
+ def close_log_file
26
+ @log_file.close
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Orchestra
2
+ VERSION = "0.4.1"
3
+ end
data/lib/orchestra.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Orchestra
2
+ end
3
+
4
+ require "active_support"
5
+ require "active_support/core_ext"
6
+ require "zeitwerk"
7
+
8
+ loader = Zeitwerk::Loader.for_gem
9
+ loader.setup
10
+ loader.eager_load
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: envirobly-orchestra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Starsi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: debug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ description:
70
+ email: klevo@klevo.sk
71
+ executables:
72
+ - orchestra
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - bin/orchestra
78
+ - lib/orchestra.rb
79
+ - lib/orchestra/base.rb
80
+ - lib/orchestra/cli.rb
81
+ - lib/orchestra/cli/images.rb
82
+ - lib/orchestra/cli/main.rb
83
+ - lib/orchestra/cli/services.rb
84
+ - lib/orchestra/cli/stack.rb
85
+ - lib/orchestra/logger.rb
86
+ - lib/orchestra/version.rb
87
+ homepage: https://klevo.sk
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.5.4
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Container orchestration agent.
110
+ test_files: []