odysseus-core 0.1.0
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/CHANGELOG.md +5 -0
- data/LICENSE.txt +8 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/lib/odysseus/builder/client.rb +292 -0
- data/lib/odysseus/caddy/client.rb +338 -0
- data/lib/odysseus/config/parser.rb +225 -0
- data/lib/odysseus/core/version.rb +7 -0
- data/lib/odysseus/core.rb +10 -0
- data/lib/odysseus/deployer/executor.rb +389 -0
- data/lib/odysseus/deployer/ssh.rb +143 -0
- data/lib/odysseus/docker/client.rb +333 -0
- data/lib/odysseus/errors.rb +27 -0
- data/lib/odysseus/host_providers/aws_asg.rb +91 -0
- data/lib/odysseus/host_providers/base.rb +27 -0
- data/lib/odysseus/host_providers/static.rb +24 -0
- data/lib/odysseus/host_providers.rb +49 -0
- data/lib/odysseus/orchestrator/accessory_deploy.rb +309 -0
- data/lib/odysseus/orchestrator/job_deploy.rb +176 -0
- data/lib/odysseus/orchestrator/web_deploy.rb +253 -0
- data/lib/odysseus/secrets/encrypted_file.rb +125 -0
- data/lib/odysseus/secrets/loader.rb +56 -0
- data/lib/odysseus/validators/config.rb +85 -0
- data/lib/odysseus/version.rb +5 -0
- data/lib/odysseus.rb +26 -0
- data/sig/odysseus/core.rbs +6 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 917c4ba0f885774f77c2bb74ce396300be9f9ae077f5ddc8d54bd96e4766d6d8
|
|
4
|
+
data.tar.gz: ea50eb29cd561fa1ae3de2f6f81df06d33f4cb21bd4a36f3a0a60acc94cfd0f4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1eef3bc36f17dce94de204c1b4ddec3bdbc9f2d3882ec2dc2b7fd56cfe90f56a53856c8bcfb90df3ee10dce4e9236f5a748efc7b7b88d5babea919a02f636772
|
|
7
|
+
data.tar.gz: 2a3b7d72b6ddbc2e6210839e803a8c9173d41d952775695dea80a1172a919d22308f40e17b0f79ae37668d0b7f109b1e4591b3445ea0fab2a47cde8b9e570caa
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) Imfiny SAS / Wa Systems SAS
|
|
2
|
+
|
|
3
|
+
Odysseus is an Open Source project licensed under the terms of
|
|
4
|
+
the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
|
|
5
|
+
for license text.
|
|
6
|
+
|
|
7
|
+
Odysseus Pro and Odysseus Enterprise have a commercial-friendly license.
|
|
8
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Odysseus::Core
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/odysseus/core`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/odysseus-core. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/odysseus-core/blob/trunk/CODE_OF_CONDUCT.md).
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
40
|
+
|
|
41
|
+
## Code of Conduct
|
|
42
|
+
|
|
43
|
+
Everyone interacting in the Odysseus::Core project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/odysseus-core/blob/trunk/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# lib/odysseus/builder/client.rb
|
|
2
|
+
|
|
3
|
+
module Odysseus
|
|
4
|
+
module Builder
|
|
5
|
+
class Client
|
|
6
|
+
# @param config [Hash] builder config from deploy.yml
|
|
7
|
+
# @param ssh_config [Hash] SSH config (user, keys)
|
|
8
|
+
# @param logger [Object] logger
|
|
9
|
+
# @param verbose [Boolean] show commands being executed
|
|
10
|
+
def initialize(config:, ssh_config: {}, logger: nil, verbose: false)
|
|
11
|
+
@config = normalize_config(config)
|
|
12
|
+
@ssh_config = ssh_config
|
|
13
|
+
@logger = logger || default_logger
|
|
14
|
+
@verbose = verbose
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Build Docker image
|
|
18
|
+
# @param context_path [String] path to build context (directory with Dockerfile)
|
|
19
|
+
# @param image [String] full image name with tag (e.g., "registry/app:v1.0.0")
|
|
20
|
+
# @return [Hash] build result with :success, :image, :strategy
|
|
21
|
+
def build(context_path:, image:)
|
|
22
|
+
@logger.info("Building image: #{image}")
|
|
23
|
+
|
|
24
|
+
case strategy
|
|
25
|
+
when :local
|
|
26
|
+
build_local(context_path: context_path, image: image)
|
|
27
|
+
when :remote
|
|
28
|
+
build_remote(context_path: context_path, image: image)
|
|
29
|
+
else
|
|
30
|
+
raise BuildError, "Unknown build strategy: #{strategy}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Build and push to registry
|
|
35
|
+
# @param context_path [String] path to build context
|
|
36
|
+
# @param image [String] full image name with tag
|
|
37
|
+
# @param registry [Hash] registry config (server, username, password)
|
|
38
|
+
# @return [Hash] build and push result
|
|
39
|
+
def build_and_push(context_path:, image:, registry: nil)
|
|
40
|
+
result = build(context_path: context_path, image: image)
|
|
41
|
+
return result unless result[:success]
|
|
42
|
+
|
|
43
|
+
if @config[:push] || registry
|
|
44
|
+
push_result = push(image: image, registry: registry)
|
|
45
|
+
result.merge(pushed: push_result[:success], push_output: push_result[:output])
|
|
46
|
+
else
|
|
47
|
+
result.merge(pushed: false)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Push image to registry
|
|
52
|
+
# @param image [String] full image name with tag
|
|
53
|
+
# @param registry [Hash] registry config (server, username, password)
|
|
54
|
+
# @return [Hash] push result
|
|
55
|
+
def push(image:, registry: nil)
|
|
56
|
+
@logger.info("Pushing image to registry: #{image}")
|
|
57
|
+
|
|
58
|
+
executor = build_executor
|
|
59
|
+
|
|
60
|
+
# Login if credentials provided
|
|
61
|
+
if registry && registry[:username]
|
|
62
|
+
login_cmd = build_login_command(registry)
|
|
63
|
+
executor.call(login_cmd)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
output = executor.call("docker push #{image}")
|
|
67
|
+
{ success: true, output: output }
|
|
68
|
+
rescue SSHCommandError, BuildError => e
|
|
69
|
+
@logger.error("Push failed: #{e.message}")
|
|
70
|
+
{ success: false, error: e.message }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Push image directly to remote host via SSH (using docker pussh/unregistry)
|
|
74
|
+
# @param image [String] full image name with tag
|
|
75
|
+
# @param host [String] target host to push to
|
|
76
|
+
# @param user [String] SSH user (default: from ssh_config)
|
|
77
|
+
# @return [Hash] pussh result
|
|
78
|
+
def pussh(image:, host:, user: nil)
|
|
79
|
+
ssh_user = user || @ssh_config[:user] || 'root'
|
|
80
|
+
target = "#{ssh_user}@#{host}"
|
|
81
|
+
|
|
82
|
+
@logger.info("Pushing image via SSH to #{target}: #{image}")
|
|
83
|
+
|
|
84
|
+
# docker pussh uses: docker pussh IMAGE [USER@]HOST
|
|
85
|
+
cmd = "docker pussh #{image} #{target}"
|
|
86
|
+
@logger.debug(cmd) if @logger.respond_to?(:debug)
|
|
87
|
+
|
|
88
|
+
output = execute_local_command(cmd)
|
|
89
|
+
{ success: true, output: output, host: host }
|
|
90
|
+
rescue BuildError => e
|
|
91
|
+
@logger.error("Pussh failed: #{e.message}")
|
|
92
|
+
{ success: false, error: e.message, host: host }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Push image to multiple hosts via SSH
|
|
96
|
+
# @param image [String] full image name with tag
|
|
97
|
+
# @param hosts [Array<String>] list of target hosts
|
|
98
|
+
# @param user [String] SSH user (default: from ssh_config)
|
|
99
|
+
# @return [Hash] results for each host
|
|
100
|
+
def pussh_to_hosts(image:, hosts:, user: nil)
|
|
101
|
+
results = {}
|
|
102
|
+
|
|
103
|
+
hosts.each do |host|
|
|
104
|
+
@logger.info("Pushing to #{host}...")
|
|
105
|
+
results[host] = pussh(image: image, host: host, user: user)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
success: results.values.all? { |r| r[:success] },
|
|
110
|
+
results: results
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if image exists (locally or on build host)
|
|
115
|
+
# @param image [String] full image name with tag
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def image_exists?(image)
|
|
118
|
+
executor = build_executor
|
|
119
|
+
output = executor.call("docker images -q #{image} 2>/dev/null || echo ''")
|
|
120
|
+
!output.strip.empty?
|
|
121
|
+
rescue
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get configured build strategy
|
|
126
|
+
# @return [Symbol] :local or :remote
|
|
127
|
+
def strategy
|
|
128
|
+
@config[:strategy]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get configured build host (for remote strategy)
|
|
132
|
+
# @return [String, nil]
|
|
133
|
+
def build_host
|
|
134
|
+
@config[:host]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def normalize_config(config)
|
|
140
|
+
{
|
|
141
|
+
strategy: (config[:strategy] || config['strategy'] || 'local').to_sym,
|
|
142
|
+
host: config[:host] || config['host'],
|
|
143
|
+
dockerfile: config[:dockerfile] || config['dockerfile'] || 'Dockerfile',
|
|
144
|
+
context: config[:context] || config['context'] || '.',
|
|
145
|
+
arch: config[:arch] || config['arch'],
|
|
146
|
+
platforms: config[:platforms] || config['platforms'] || [],
|
|
147
|
+
build_args: config[:build_args] || config['build_args'] || {},
|
|
148
|
+
cache: config.key?(:cache) ? config[:cache] : (config.key?('cache') ? config['cache'] : true),
|
|
149
|
+
push: config[:push] || config['push'] || false,
|
|
150
|
+
multiarch: config[:multiarch] || config['multiarch'] || false
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_local(context_path:, image:)
|
|
155
|
+
@logger.info("Building locally...")
|
|
156
|
+
|
|
157
|
+
cmd = build_docker_command(context_path: context_path, image: image)
|
|
158
|
+
@logger.debug(cmd) if @logger.respond_to?(:debug)
|
|
159
|
+
|
|
160
|
+
# For local builds, execute directly
|
|
161
|
+
output = execute_local(cmd, context_path)
|
|
162
|
+
|
|
163
|
+
{ success: true, image: image, strategy: :local, output: output }
|
|
164
|
+
rescue => e
|
|
165
|
+
@logger.error("Local build failed: #{e.message}")
|
|
166
|
+
{ success: false, strategy: :local, error: e.message }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def build_remote(context_path:, image:)
|
|
170
|
+
@logger.info("Building on remote host: #{@config[:host]}")
|
|
171
|
+
|
|
172
|
+
unless @config[:host]
|
|
173
|
+
raise BuildError, "Remote build strategy requires 'host' to be configured"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
ssh = connect_to_build_host
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
# Create remote build directory
|
|
180
|
+
remote_dir = "/tmp/odysseus-build-#{Time.now.to_i}"
|
|
181
|
+
ssh.execute("mkdir -p #{remote_dir}")
|
|
182
|
+
|
|
183
|
+
# Upload build context
|
|
184
|
+
@logger.info("Uploading build context...")
|
|
185
|
+
ssh.upload(context_path, remote_dir)
|
|
186
|
+
|
|
187
|
+
# Determine the actual context directory on remote
|
|
188
|
+
context_name = File.basename(context_path)
|
|
189
|
+
remote_context = "#{remote_dir}/#{context_name}"
|
|
190
|
+
|
|
191
|
+
# Build on remote
|
|
192
|
+
cmd = build_docker_command(context_path: remote_context, image: image)
|
|
193
|
+
@logger.debug(cmd) if @logger.respond_to?(:debug)
|
|
194
|
+
|
|
195
|
+
output = ssh.execute(cmd)
|
|
196
|
+
|
|
197
|
+
# Cleanup remote directory
|
|
198
|
+
ssh.execute("rm -rf #{remote_dir}")
|
|
199
|
+
|
|
200
|
+
{ success: true, image: image, strategy: :remote, output: output }
|
|
201
|
+
rescue SSHCommandError => e
|
|
202
|
+
@logger.error("Remote build failed: #{e.message}")
|
|
203
|
+
{ success: false, strategy: :remote, error: e.message }
|
|
204
|
+
ensure
|
|
205
|
+
ssh.close
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_docker_command(context_path:, image:)
|
|
210
|
+
parts = []
|
|
211
|
+
|
|
212
|
+
if @config[:multiarch] && @config[:platforms].any?
|
|
213
|
+
# Use buildx for multi-platform builds
|
|
214
|
+
parts << 'docker buildx build'
|
|
215
|
+
parts << "--platform #{@config[:platforms].join(',')}"
|
|
216
|
+
parts << '--push' if @config[:push]
|
|
217
|
+
else
|
|
218
|
+
parts << 'docker build'
|
|
219
|
+
# Single architecture build
|
|
220
|
+
parts << "--platform linux/#{@config[:arch]}" if @config[:arch]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
parts << "-t #{image}"
|
|
224
|
+
parts << "-f #{context_path}/#{@config[:dockerfile]}"
|
|
225
|
+
parts << '--no-cache' unless @config[:cache]
|
|
226
|
+
|
|
227
|
+
# Add build args
|
|
228
|
+
@config[:build_args].each do |key, value|
|
|
229
|
+
parts << "--build-arg #{key}=#{value}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
parts << context_path
|
|
233
|
+
|
|
234
|
+
parts.join(' ')
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def build_login_command(registry)
|
|
238
|
+
server = registry[:server] || ''
|
|
239
|
+
"echo '#{registry[:password]}' | docker login #{server} -u #{registry[:username]} --password-stdin"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def execute_local(cmd, working_dir)
|
|
243
|
+
# Execute command locally using system
|
|
244
|
+
Dir.chdir(working_dir) do
|
|
245
|
+
output = `#{cmd} 2>&1`
|
|
246
|
+
unless $?.success?
|
|
247
|
+
raise BuildError, "Build command failed: #{output}"
|
|
248
|
+
end
|
|
249
|
+
output
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def build_executor
|
|
254
|
+
case strategy
|
|
255
|
+
when :local
|
|
256
|
+
# Return a proc that executes locally
|
|
257
|
+
->(cmd) { execute_local_command(cmd) }
|
|
258
|
+
when :remote
|
|
259
|
+
# Return a proc that executes via SSH
|
|
260
|
+
ssh = connect_to_build_host
|
|
261
|
+
->(cmd) { ssh.execute(cmd) }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def execute_local_command(cmd)
|
|
266
|
+
output = `#{cmd} 2>&1`
|
|
267
|
+
unless $?.success?
|
|
268
|
+
raise BuildError, "Command failed: #{output}"
|
|
269
|
+
end
|
|
270
|
+
output
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def connect_to_build_host
|
|
274
|
+
Odysseus::Deployer::SSH.new(
|
|
275
|
+
host: @config[:host],
|
|
276
|
+
user: @ssh_config[:user] || 'root',
|
|
277
|
+
keys: @ssh_config[:keys] || [],
|
|
278
|
+
verbose: @verbose
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def default_logger
|
|
283
|
+
Object.new.tap do |l|
|
|
284
|
+
l.define_singleton_method(:info) { |msg| puts msg }
|
|
285
|
+
l.define_singleton_method(:warn) { |msg| puts "[WARN] #{msg}" }
|
|
286
|
+
l.define_singleton_method(:error) { |msg| puts "[ERROR] #{msg}" }
|
|
287
|
+
l.define_singleton_method(:debug) { |msg| puts " > #{msg}" if @verbose }
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|