floe 0.20.1 → 0.21.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 +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/floe/container_runner/docker.rb +19 -5
- data/lib/floe/container_runner/kubernetes/host_path_volume_handler.rb +159 -0
- data/lib/floe/container_runner/kubernetes/persistent_volume_handler.rb +41 -0
- data/lib/floe/container_runner/kubernetes.rb +75 -30
- data/lib/floe/container_runner/podman.rb +5 -1
- data/lib/floe/version.rb +1 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17d64b12761527067ed26cda1737c6f4bd20c8d1c9affbb5edf78b176b4713c7
|
|
4
|
+
data.tar.gz: 22de6a27ac450b71d1467fb5b5fa304c6802e155f956bf2d0a72f6e097717aaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e13a84b4568e4de952db7c05f5a29e6fc8fabaa0c944dc8b571a1514b5d9aae1118b94a4332e2e5456cdb0c776a629d1a19ef0d4a9803dd23e0844dc228cc03
|
|
7
|
+
data.tar.gz: 5512f1229704cb82a6e354388fc1873fe2eaf7b91d0410931a7e2e9baaadd5cf53815faaa3d4cee4cc4994d5ce8713b52a8ab451d241c3e5c857fc0bfc36f5a5
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.21.0] - 2026-09-16
|
|
8
|
+
### Added
|
|
9
|
+
- Add volumes: option to run_async! for all container runners ([#347](https://github.com/ManageIQ/floe/pull/347))
|
|
10
|
+
- Add entrypoint and command overrides to container runners ([#349](https://github.com/ManageIQ/floe/pull/349))
|
|
11
|
+
|
|
7
12
|
## [0.20.1] - 2026-09-15
|
|
8
13
|
### Fixed
|
|
9
14
|
- Fix Execution Context not passed to Parallel branches ([#348](https://github.com/ManageIQ/floe/pull/348))
|
|
@@ -351,7 +356,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
351
356
|
### Added
|
|
352
357
|
- Initial release
|
|
353
358
|
|
|
354
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
|
359
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.21.0...HEAD
|
|
360
|
+
[0.21.0]: https://github.com/ManageIQ/floe/compare/v0.20.1...v0.21.0
|
|
355
361
|
[0.20.1]: https://github.com/ManageIQ/floe/compare/v0.20.0...v0.20.1
|
|
356
362
|
[0.20.0]: https://github.com/ManageIQ/floe/compare/v0.19.2...v0.20.0
|
|
357
363
|
[0.19.2]: https://github.com/ManageIQ/floe/compare/v0.19.1...v0.19.2
|
|
@@ -18,8 +18,10 @@ module Floe
|
|
|
18
18
|
@pull_policy = options["pull-policy"]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def run_async!(resource, env, secrets, context)
|
|
21
|
+
def run_async!(resource, env, secrets, context, volumes: [], entrypoint: nil, command: nil)
|
|
22
22
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
23
|
+
raise ArgumentError, "entrypoint must be a String" if entrypoint && !entrypoint.kind_of?(String)
|
|
24
|
+
raise ArgumentError, "command must be an Array" if command && !command.kind_of?(Array)
|
|
23
25
|
|
|
24
26
|
image = resource.sub("docker://", "")
|
|
25
27
|
execution_id = context.execution["Id"]
|
|
@@ -30,7 +32,7 @@ module Floe
|
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
begin
|
|
33
|
-
runner_context["container_ref"] = run_container(image, env, execution_id, runner_context["secrets_ref"], context.logger)
|
|
35
|
+
runner_context["container_ref"] = run_container(image, env, execution_id, runner_context["secrets_ref"], context.logger, volumes, entrypoint, command)
|
|
34
36
|
runner_context
|
|
35
37
|
rescue AwesomeSpawn::CommandResultError => err
|
|
36
38
|
cleanup(runner_context)
|
|
@@ -128,8 +130,8 @@ module Floe
|
|
|
128
130
|
|
|
129
131
|
attr_reader :network
|
|
130
132
|
|
|
131
|
-
def run_container(image, env, execution_id, secrets_file, logger)
|
|
132
|
-
params = run_container_params(image, env, execution_id, secrets_file)
|
|
133
|
+
def run_container(image, env, execution_id, secrets_file, logger, volumes, entrypoint, command)
|
|
134
|
+
params = run_container_params(image, env, execution_id, secrets_file, volumes, entrypoint, command)
|
|
133
135
|
|
|
134
136
|
logger.debug("Running #{AwesomeSpawn.build_command_line(self.class::DOCKER_COMMAND, params)}")
|
|
135
137
|
|
|
@@ -137,7 +139,7 @@ module Floe
|
|
|
137
139
|
result.output.chomp
|
|
138
140
|
end
|
|
139
141
|
|
|
140
|
-
def run_container_params(image, env, execution_id, secrets_file)
|
|
142
|
+
def run_container_params(image, env, execution_id, secrets_file, volumes, entrypoint, command)
|
|
141
143
|
params = ["run"]
|
|
142
144
|
params << :detach
|
|
143
145
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
|
@@ -146,8 +148,20 @@ module Floe
|
|
|
146
148
|
params << [:net, "host"] if @network == "host"
|
|
147
149
|
params << [:label, "execution_id=#{execution_id}"]
|
|
148
150
|
params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
|
|
151
|
+
params += volumes.map { |v| [:v, volume_to_flag(v)] }
|
|
152
|
+
params << [:entrypoint, entrypoint] if entrypoint
|
|
149
153
|
params << [:name, container_name(image)]
|
|
150
154
|
params << image
|
|
155
|
+
params.concat(command) if command
|
|
156
|
+
params
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def volume_to_flag(volume)
|
|
160
|
+
options = volume.fetch(:options, "z")
|
|
161
|
+
source = volume[:volume_name] || volume[:host_path]
|
|
162
|
+
flag = "#{source}:#{volume[:container_path]}"
|
|
163
|
+
flag += ":#{options}" if options && !options.empty?
|
|
164
|
+
flag
|
|
151
165
|
end
|
|
152
166
|
|
|
153
167
|
def wait_params(until_timestamp)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Floe
|
|
4
|
+
class ContainerRunner
|
|
5
|
+
class Kubernetes
|
|
6
|
+
# Mixin that handles host_path volumes for Kubernetes by staging the
|
|
7
|
+
# host-side directory to S3 for download by an init container.
|
|
8
|
+
module HostPathVolumeHandler
|
|
9
|
+
DEFAULT_INIT_IMAGE = "curlimages/curl:latest"
|
|
10
|
+
INIT_CONTAINER_NAME = "floe-init"
|
|
11
|
+
|
|
12
|
+
def init_host_path_volume_options(options)
|
|
13
|
+
@s3_endpoint = options["s3_endpoint"]
|
|
14
|
+
@s3_bucket = options["s3_bucket"]
|
|
15
|
+
@s3_access_key = options["s3_access_key"]
|
|
16
|
+
@s3_secret_key = options["s3_secret_key"]
|
|
17
|
+
@init_image = options.fetch("init_image", DEFAULT_INIT_IMAGE)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Deletes every S3 object represented by staged_volumes, suppressing
|
|
21
|
+
# errors for each deletion.
|
|
22
|
+
def cleanup_staged_volumes(staged_volumes)
|
|
23
|
+
Array(staged_volumes).each do |volume|
|
|
24
|
+
key = volume[:s3_key] || volume["s3_key"]
|
|
25
|
+
Floe.logger.debug("Deleting s3://#{s3_bucket}/#{key}...")
|
|
26
|
+
delete_s3_object(key)
|
|
27
|
+
Floe.logger.debug("Deleting s3://#{s3_bucket}/#{key}...Complete")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :s3_endpoint, :s3_bucket, :s3_access_key, :s3_secret_key, :init_image
|
|
34
|
+
|
|
35
|
+
def host_path_volume_instance_variables_to_hide
|
|
36
|
+
%i[@s3_access_key @s3_client @s3_secret_key]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Upload each volume's host_path as a .tar.gz to S3, returning an array
|
|
40
|
+
# of hashes with :volume, :s3_key, :presigned_input_url.
|
|
41
|
+
def stage_host_path_volumes(volumes, execution_id, logger)
|
|
42
|
+
raise ArgumentError, "S3 must be configured (s3_endpoint, s3_bucket, s3_access_key, s3_secret_key) to use volumes with Kubernetes" unless s3_configured?
|
|
43
|
+
|
|
44
|
+
volumes.map do |volume|
|
|
45
|
+
s3_key = "floe/#{execution_id}/#{File.basename(volume[:container_path])}.tar.gz"
|
|
46
|
+
logger.debug("Staging volume #{volume[:host_path]} -> s3://#{s3_bucket}/#{s3_key}...")
|
|
47
|
+
|
|
48
|
+
tarball = create_tarball(volume[:host_path])
|
|
49
|
+
upload_to_s3(s3_key, tarball)
|
|
50
|
+
logger.debug("Staging volume #{volume[:host_path]} -> s3://#{s3_bucket}/#{s3_key}...Complete")
|
|
51
|
+
presigned_url = presign_s3_get(s3_key)
|
|
52
|
+
|
|
53
|
+
{:volume => volume, :s3_key => s3_key, :presigned_input_url => presigned_url}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete_s3_object(key)
|
|
58
|
+
s3_client.delete_object(:bucket => s3_bucket, :key => key)
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Mutates spec in-place to add the emptyDir shared volumes and init
|
|
64
|
+
# container for staged volumes.
|
|
65
|
+
def add_host_path_volumes_to_spec!(spec, staged_volumes)
|
|
66
|
+
spec[:spec][:volumes] ||= []
|
|
67
|
+
primary = spec[:spec][:containers][0]
|
|
68
|
+
primary[:volumeMounts] ||= []
|
|
69
|
+
|
|
70
|
+
init_cmds = []
|
|
71
|
+
init_volume_mounts = []
|
|
72
|
+
|
|
73
|
+
staged_volumes.each_with_index do |sv, idx|
|
|
74
|
+
volume = sv[:volume]
|
|
75
|
+
share_name = "floe-volume-#{idx}"
|
|
76
|
+
|
|
77
|
+
# emptyDir shared between init container and primary
|
|
78
|
+
spec[:spec][:volumes] << {:name => share_name, :emptyDir => {}}
|
|
79
|
+
|
|
80
|
+
primary[:volumeMounts] << {
|
|
81
|
+
:name => share_name,
|
|
82
|
+
:mountPath => volume[:container_path]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
init_volume_mounts << {
|
|
86
|
+
:name => share_name,
|
|
87
|
+
:mountPath => volume[:container_path]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Init container downloads and unpacks each volume's tarball
|
|
91
|
+
init_cmds << "curl -fsSL '#{sv[:presigned_input_url]}' | tar -xzf - -C '#{volume[:container_path]}'"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Init container: runs to completion before the primary starts, populating
|
|
95
|
+
# all shared volumes from S3. Primary command/args are left untouched.
|
|
96
|
+
spec[:spec][:initContainers] ||= []
|
|
97
|
+
spec[:spec][:initContainers] << {
|
|
98
|
+
:name => INIT_CONTAINER_NAME,
|
|
99
|
+
:image => init_image,
|
|
100
|
+
:command => ["sh", "-c", init_cmds.join(" && ")],
|
|
101
|
+
:volumeMounts => init_volume_mounts
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def s3_configured?
|
|
106
|
+
!!(s3_endpoint && s3_bucket && s3_access_key && s3_secret_key)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def s3_client
|
|
110
|
+
require "aws-sdk-s3"
|
|
111
|
+
|
|
112
|
+
@s3_client ||= Aws::S3::Client.new(
|
|
113
|
+
:endpoint => s3_endpoint,
|
|
114
|
+
:region => "us-east-1", # required by SDK even for non-AWS endpoints
|
|
115
|
+
:access_key_id => s3_access_key,
|
|
116
|
+
:secret_access_key => s3_secret_key,
|
|
117
|
+
:force_path_style => true # required for MinIO and other non-AWS endpoints
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def presign_s3_get(key)
|
|
122
|
+
require "aws-sdk-s3"
|
|
123
|
+
|
|
124
|
+
presigner = Aws::S3::Presigner.new(:client => s3_client)
|
|
125
|
+
presigner.presigned_url(:get_object, :bucket => s3_bucket, :key => key, :expires_in => 3600)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_tarball(source_path)
|
|
129
|
+
require "rubygems/package"
|
|
130
|
+
require "zlib"
|
|
131
|
+
|
|
132
|
+
gz_buffer = StringIO.new
|
|
133
|
+
Zlib::GzipWriter.wrap(gz_buffer) do |gz|
|
|
134
|
+
Gem::Package::TarWriter.new(gz) do |tar|
|
|
135
|
+
Dir.glob("#{source_path}/**/*", File::FNM_DOTMATCH).sort.each do |file|
|
|
136
|
+
relative = file.sub("#{source_path}/", "")
|
|
137
|
+
next if relative == "." || relative.empty?
|
|
138
|
+
|
|
139
|
+
stat = File.stat(file)
|
|
140
|
+
if stat.directory?
|
|
141
|
+
tar.mkdir(relative, stat.mode)
|
|
142
|
+
else
|
|
143
|
+
tar.add_file_simple(relative, stat.mode, stat.size) do |io|
|
|
144
|
+
File.open(file, "rb") { |f| IO.copy_stream(f, io) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
gz_buffer.string
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def upload_to_s3(key, data)
|
|
154
|
+
s3_client.put_object(:bucket => s3_bucket, :key => key, :body => data)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Floe
|
|
4
|
+
class ContainerRunner
|
|
5
|
+
class Kubernetes
|
|
6
|
+
# Mixin that attaches persistent volumes (e.g. pre-created docker volumes or
|
|
7
|
+
# PersistentVolumeClaims) directly to a pod spec without any staging.
|
|
8
|
+
# Each volume entry must have a :volume_name key (the PVC claim name) and
|
|
9
|
+
# a :container_path key. An optional :read_only key sets readOnly on the
|
|
10
|
+
# mount.
|
|
11
|
+
module PersistentVolumeHandler
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Mutates spec in-place to attach persistent volumes as PVCs. Each entry
|
|
15
|
+
# must have :volume_name (the PVC claim name) and :container_path.
|
|
16
|
+
def add_persistent_volumes_to_spec!(spec, persistent_volumes)
|
|
17
|
+
spec[:spec][:volumes] ||= []
|
|
18
|
+
primary = spec[:spec][:containers][0]
|
|
19
|
+
primary[:volumeMounts] ||= []
|
|
20
|
+
|
|
21
|
+
persistent_volumes.each_with_index do |vol, idx|
|
|
22
|
+
vol_name = "floe-persistent-volume-#{idx}"
|
|
23
|
+
|
|
24
|
+
spec[:spec][:volumes] << {
|
|
25
|
+
:name => vol_name,
|
|
26
|
+
:persistentVolumeClaim => {:claimName => vol[:volume_name]}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
volume_mount = {
|
|
30
|
+
:name => vol_name,
|
|
31
|
+
:mountPath => vol[:container_path]
|
|
32
|
+
}
|
|
33
|
+
volume_mount[:readOnly] = true if vol[:read_only]
|
|
34
|
+
|
|
35
|
+
primary[:volumeMounts] << volume_mount
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -5,6 +5,12 @@ module Floe
|
|
|
5
5
|
class Kubernetes < Floe::Runner
|
|
6
6
|
include Floe::ContainerRunner::DockerMixin
|
|
7
7
|
|
|
8
|
+
require_relative "kubernetes/host_path_volume_handler"
|
|
9
|
+
include Floe::ContainerRunner::Kubernetes::HostPathVolumeHandler
|
|
10
|
+
|
|
11
|
+
require_relative "kubernetes/persistent_volume_handler"
|
|
12
|
+
include Floe::ContainerRunner::Kubernetes::PersistentVolumeHandler
|
|
13
|
+
|
|
8
14
|
TOKEN_FILE = "/run/secrets/kubernetes.io/serviceaccount/token"
|
|
9
15
|
CA_CERT_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
|
10
16
|
RUNNING_PHASES = %w[Pending Running].freeze
|
|
@@ -42,11 +48,15 @@ module Floe
|
|
|
42
48
|
@pull_policy = options["pull-policy"]
|
|
43
49
|
@task_service_account = options["task_service_account"]
|
|
44
50
|
|
|
51
|
+
init_host_path_volume_options(options)
|
|
52
|
+
|
|
45
53
|
super
|
|
46
54
|
end
|
|
47
55
|
|
|
48
|
-
def run_async!(resource, env, secrets, context)
|
|
56
|
+
def run_async!(resource, env, secrets, context, volumes: [], entrypoint: nil, command: nil)
|
|
49
57
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
|
58
|
+
raise ArgumentError, "entrypoint must be a String" if entrypoint && !entrypoint.kind_of?(String)
|
|
59
|
+
raise ArgumentError, "command must be an Array" if command && !command.kind_of?(Array)
|
|
50
60
|
|
|
51
61
|
image = resource.sub("docker://", "")
|
|
52
62
|
name = container_name(image)
|
|
@@ -54,8 +64,17 @@ module Floe
|
|
|
54
64
|
execution_id = context.execution["Id"]
|
|
55
65
|
runner_context = {"container_ref" => name, "container_state" => {"phase" => "Pending"}, "secrets_ref" => secret}
|
|
56
66
|
|
|
67
|
+
persistent_volumes, host_volumes = volumes.partition { |v| v[:volume_name] }
|
|
68
|
+
staged_volumes = stage_host_path_volumes(host_volumes, execution_id, context.logger) if host_volumes.any?
|
|
69
|
+
runner_context["staged_volumes"] = staged_volumes if staged_volumes
|
|
70
|
+
|
|
57
71
|
begin
|
|
58
|
-
|
|
72
|
+
spec = pod_spec(name, image, env, execution_id, secret, staged_volumes || [], persistent_volumes || [], entrypoint, command)
|
|
73
|
+
context.logger.debug("Running pod #{name} with image #{image}")
|
|
74
|
+
kubeclient.create_pod(spec)
|
|
75
|
+
# Always record the primary container name so get_pod_log targets the
|
|
76
|
+
# right container even when init containers are present.
|
|
77
|
+
runner_context["primary_container"] = spec.dig(:spec, :containers, 0, :name)
|
|
59
78
|
runner_context
|
|
60
79
|
rescue Kubeclient::HttpError => err
|
|
61
80
|
cleanup(runner_context)
|
|
@@ -93,7 +112,9 @@ module Floe
|
|
|
93
112
|
failed_state = failed_container_states(runner_context).first
|
|
94
113
|
{"Error" => failed_state["reason"], "Cause" => failed_state["message"]}
|
|
95
114
|
else
|
|
96
|
-
|
|
115
|
+
log_options = {}
|
|
116
|
+
log_options[:container] = runner_context["primary_container"] if runner_context["primary_container"]
|
|
117
|
+
runner_context["output"] = kubeclient.get_pod_log(runner_context["container_ref"], namespace, **log_options).body
|
|
97
118
|
end
|
|
98
119
|
end
|
|
99
120
|
|
|
@@ -102,6 +123,8 @@ module Floe
|
|
|
102
123
|
|
|
103
124
|
delete_pod(pod) if pod
|
|
104
125
|
delete_secret(secret) if secret
|
|
126
|
+
|
|
127
|
+
cleanup_staged_volumes(runner_context["staged_volumes"])
|
|
105
128
|
end
|
|
106
129
|
|
|
107
130
|
def wait(timeout: nil, events: %i[create update delete])
|
|
@@ -152,31 +175,25 @@ module Floe
|
|
|
152
175
|
end
|
|
153
176
|
end
|
|
154
177
|
|
|
178
|
+
def inspect
|
|
179
|
+
vars = instance_variables_to_inspect.map { |ivar| "#{ivar}=#{instance_variable_get(ivar).inspect}" }.join(", ")
|
|
180
|
+
prefix = Kernel.instance_method(:inspect).bind_call(self).split(' ', 2).first
|
|
181
|
+
"#{prefix} #{vars}>"
|
|
182
|
+
end
|
|
183
|
+
|
|
155
184
|
private
|
|
156
185
|
|
|
157
186
|
attr_reader :ca_file, :kubeconfig_file, :kubeconfig_context, :namespace, :server, :token, :verify_ssl
|
|
158
187
|
|
|
159
|
-
def
|
|
160
|
-
kubeclient
|
|
161
|
-
rescue Kubeclient::HttpError => err
|
|
162
|
-
raise Floe::ExecutionError, "Failed to get status for pod #{namespace}/#{pod_name}: #{err}"
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def pod_running?(context)
|
|
166
|
-
RUNNING_PHASES.include?(context.dig("container_state", "phase"))
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def failed_container_states(context)
|
|
170
|
-
container_statuses = context.dig("container_state", "containerStatuses") || []
|
|
171
|
-
container_statuses.filter_map { |status| status["state"]&.values&.first }
|
|
172
|
-
.select { |state| FAILURE_REASONS.include?(state["reason"]) }
|
|
188
|
+
def instance_variables_to_inspect
|
|
189
|
+
instance_variables - %i[@kubeclient @token] - host_path_volume_instance_variables_to_hide
|
|
173
190
|
end
|
|
174
191
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
# Pod spec construction
|
|
194
|
+
# ------------------------------------------------------------------
|
|
178
195
|
|
|
179
|
-
def pod_spec(name, image, env, execution_id, secret
|
|
196
|
+
def pod_spec(name, image, env, execution_id, secret, staged_volumes, persistent_volumes, entrypoint, command)
|
|
180
197
|
spec = {
|
|
181
198
|
:kind => "Pod",
|
|
182
199
|
:apiVersion => "v1",
|
|
@@ -186,13 +203,7 @@ module Floe
|
|
|
186
203
|
:labels => {"execution_id" => execution_id}
|
|
187
204
|
},
|
|
188
205
|
:spec => {
|
|
189
|
-
:containers => [
|
|
190
|
-
{
|
|
191
|
-
:name => name[0...-9], # remove the random suffix and its leading hyphen
|
|
192
|
-
:image => image,
|
|
193
|
-
:env => env.map { |k, v| {:name => k, :value => v.to_s} }
|
|
194
|
-
}
|
|
195
|
-
],
|
|
206
|
+
:containers => [build_container_spec(name, image, env, entrypoint, command)],
|
|
196
207
|
:restartPolicy => "Never"
|
|
197
208
|
}
|
|
198
209
|
}
|
|
@@ -222,11 +233,45 @@ module Floe
|
|
|
222
233
|
]
|
|
223
234
|
end
|
|
224
235
|
|
|
236
|
+
add_host_path_volumes_to_spec!(spec, staged_volumes) if staged_volumes.any?
|
|
237
|
+
add_persistent_volumes_to_spec!(spec, persistent_volumes) if persistent_volumes.any?
|
|
238
|
+
|
|
225
239
|
spec
|
|
226
240
|
end
|
|
227
241
|
|
|
228
|
-
def
|
|
229
|
-
|
|
242
|
+
def build_container_spec(name, image, env, entrypoint, command)
|
|
243
|
+
container = {
|
|
244
|
+
:name => name[0...-9], # remove the random suffix and its leading hyphen
|
|
245
|
+
:image => image,
|
|
246
|
+
:env => env.map { |k, v| {:name => k, :value => v.to_s} }
|
|
247
|
+
}
|
|
248
|
+
container[:command] = Array(entrypoint) if entrypoint
|
|
249
|
+
container[:args] = command if command
|
|
250
|
+
container
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# ------------------------------------------------------------------
|
|
254
|
+
# Pod / secret lifecycle
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def pod_info(pod_name)
|
|
258
|
+
kubeclient.get_pod(pod_name, namespace)
|
|
259
|
+
rescue Kubeclient::HttpError => err
|
|
260
|
+
raise Floe::ExecutionError, "Failed to get status for pod #{namespace}/#{pod_name}: #{err}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def pod_running?(context)
|
|
264
|
+
RUNNING_PHASES.include?(context.dig("container_state", "phase"))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def failed_container_states(context)
|
|
268
|
+
container_statuses = context.dig("container_state", "containerStatuses") || []
|
|
269
|
+
container_statuses.filter_map { |status| status["state"]&.values&.first }
|
|
270
|
+
.select { |state| FAILURE_REASONS.include?(state["reason"]) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def container_failed?(context)
|
|
274
|
+
failed_container_states(context).any?
|
|
230
275
|
end
|
|
231
276
|
|
|
232
277
|
def delete_pod!(name)
|
|
@@ -30,7 +30,7 @@ module Floe
|
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
-
def run_container_params(image, env, execution_id, secret)
|
|
33
|
+
def run_container_params(image, env, execution_id, secret, volumes, entrypoint, command)
|
|
34
34
|
params = ["run"]
|
|
35
35
|
params << :detach
|
|
36
36
|
params += env.map { |k, v| [:e, "#{k}=#{v}"] }
|
|
@@ -39,8 +39,12 @@ module Floe
|
|
|
39
39
|
params << [:net, "host"] if @network == "host"
|
|
40
40
|
params << [:label, "execution_id=#{execution_id}"]
|
|
41
41
|
params << [:secret, secret] if secret
|
|
42
|
+
params += volumes.map { |v| [:v, volume_to_flag(v)] }
|
|
43
|
+
params << [:entrypoint, entrypoint] if entrypoint
|
|
42
44
|
params << [:name, container_name(image)]
|
|
43
45
|
params << image
|
|
46
|
+
params.concat(command) if command
|
|
47
|
+
params
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def create_secret(secrets)
|
data/lib/floe/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: floe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ManageIQ Developers
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '5.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: aws-sdk-s3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: awesome_spawn
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -258,6 +272,8 @@ files:
|
|
|
258
272
|
- lib/floe/container_runner/docker.rb
|
|
259
273
|
- lib/floe/container_runner/docker_mixin.rb
|
|
260
274
|
- lib/floe/container_runner/kubernetes.rb
|
|
275
|
+
- lib/floe/container_runner/kubernetes/host_path_volume_handler.rb
|
|
276
|
+
- lib/floe/container_runner/kubernetes/persistent_volume_handler.rb
|
|
261
277
|
- lib/floe/container_runner/podman.rb
|
|
262
278
|
- lib/floe/logging.rb
|
|
263
279
|
- lib/floe/null_logger.rb
|