floe 0.3.0 → 0.4.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.
@@ -46,33 +46,65 @@ module Floe
46
46
  secret = create_secret!(secrets) if secrets && !secrets.empty?
47
47
 
48
48
  begin
49
+ runner_context = {"container_ref" => name}
50
+
49
51
  create_pod!(name, image, env, secret)
50
- while running?(name)
51
- sleep(1)
52
+ loop do
53
+ case pod_info(name).dig("status", "phase")
54
+ when "Pending", "Running"
55
+ sleep(1)
56
+ else # also "Succeeded"
57
+ runner_context["exit_code"] = 0
58
+ output(runner_context)
59
+ break
60
+ end
52
61
  end
53
62
 
54
- exit_status = success?(name) ? 0 : 1
55
- results = output(name)
56
-
57
- [exit_status, results]
63
+ runner_context
58
64
  ensure
59
- cleanup(name, secret)
65
+ cleanup({"container_ref" => name, "secrets_ref" => secret})
66
+ end
67
+ end
68
+
69
+ def run_async!(resource, env = {}, secrets = {})
70
+ raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
71
+
72
+ image = resource.sub("docker://", "")
73
+ name = pod_name(image)
74
+ secret = create_secret!(secrets) if secrets && !secrets.empty?
75
+
76
+ runner_context = {"container_ref" => name, "secrets_ref" => secret}
77
+
78
+ begin
79
+ create_pod!(name, image, env, secret)
80
+ rescue
81
+ cleanup(runner_context)
82
+ raise
60
83
  end
84
+
85
+ runner_context
86
+ end
87
+
88
+ def status!(runner_context)
89
+ runner_context["container_state"] = pod_info(runner_context["container_ref"])["status"]
61
90
  end
62
91
 
63
- def running?(pod_name)
64
- %w[Pending Running].include?(pod_info(pod_name).dig("status", "phase"))
92
+ def running?(runner_context)
93
+ %w[Pending Running].include?(runner_context.dig("container_state", "phase"))
65
94
  end
66
95
 
67
- def success?(pod_name)
68
- pod_info(pod_name).dig("status", "phase") == "Succeeded"
96
+ def success?(runner_context)
97
+ runner_context.dig("container_state", "phase") == "Succeeded"
69
98
  end
70
99
 
71
- def output(pod)
72
- kubeclient.get_pod_log(pod, namespace).body
100
+ def output(runner_context)
101
+ output = kubeclient.get_pod_log(runner_context["container_ref"], namespace).body
102
+ runner_context["output"] = output
73
103
  end
74
104
 
75
- def cleanup(pod, secret)
105
+ def cleanup(runner_context)
106
+ pod, secret = runner_context.values_at("container_ref", "secrets_ref")
107
+
76
108
  delete_pod(pod) if pod
77
109
  delete_secret(secret) if secret
78
110
  end
@@ -125,7 +157,7 @@ module Floe
125
157
  ]
126
158
 
127
159
  spec[:spec][:containers][0][:env] << {
128
- :name => "SECRETS",
160
+ :name => "_CREDENTIALS",
129
161
  :value => "/run/secrets/#{secret}/secret"
130
162
  }
131
163
 
@@ -10,7 +10,20 @@ module Floe
10
10
 
11
11
  super
12
12
 
13
- @network = options.fetch("network", "bridge")
13
+ @identity = options["identity"]
14
+ @log_level = options["log-level"]
15
+ @network = options["network"]
16
+ @noout = options["noout"].to_s == "true" if options.key?("noout")
17
+ @root = options["root"]
18
+ @runroot = options["runroot"]
19
+ @runtime = options["runtime"]
20
+ @runtime_flag = options["runtime-flag"]
21
+ @storage_driver = options["storage-driver"]
22
+ @storage_opt = options["storage-opt"]
23
+ @syslog = options["syslog"].to_s == "true" if options.key?("syslog")
24
+ @tmpdir = options["tmpdir"]
25
+ @transient_store = !!options["transient-store"] if options.key?("transient-store")
26
+ @volumepath = options["volumepath"]
14
27
  end
15
28
 
16
29
  def run!(resource, env = {}, secrets = {})
@@ -18,31 +31,123 @@ module Floe
18
31
 
19
32
  image = resource.sub("docker://", "")
20
33
 
21
- params = ["run", :rm]
22
- params += [[:net, "host"]] if network == "host"
23
- params += env.map { |k, v| [:e, "#{k}=#{v}"] } if env
34
+ if secrets && !secrets.empty?
35
+ secret = create_secret(secrets)
36
+ env["_CREDENTIALS"] = "/run/secrets/#{secret}"
37
+ end
38
+
39
+ output = run_container(image, env, secret)
40
+
41
+ {"exit_code" => 0, :output => output}
42
+ ensure
43
+ delete_secret(secret) if secret
44
+ end
45
+
46
+ def run_async!(resource, env = {}, secrets = {})
47
+ raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
48
+
49
+ image = resource.sub("docker://", "")
24
50
 
25
51
  if secrets && !secrets.empty?
26
- secret_guid = SecureRandom.uuid
27
- AwesomeSpawn.run!("podman", :params => ["secret", "create", secret_guid, "-"], :in_data => secrets.to_json)
52
+ secret_guid = create_secret(secrets)
53
+ env["_CREDENTIALS"] = "/run/secrets/#{secret_guid}"
54
+ end
28
55
 
29
- params << [:e, "SECRETS=/run/secrets/#{secret_guid}"]
30
- params << [:secret, secret_guid]
56
+ begin
57
+ container_id = run_container(image, env, secret_guid, :detached => true)
58
+ rescue
59
+ cleanup({"container_ref" => container_id, "secrets_ref" => secret_guid})
60
+ raise
31
61
  end
32
62
 
63
+ {"container_ref" => container_id, "secrets_ref" => secret_guid}
64
+ end
65
+
66
+ def cleanup(runner_context)
67
+ container_id, secret_guid = runner_context.values_at("container_ref", "secrets_ref")
68
+
69
+ delete_container(container_id) if container_id
70
+ delete_secret(secret_guid) if secret_guid
71
+ end
72
+
73
+ def status!(runner_context)
74
+ runner_context["container_state"] = inspect_container(runner_context["container_ref"]).first&.dig("State")
75
+ end
76
+
77
+ def running?(runner_context)
78
+ runner_context.dig("container_state", "Running")
79
+ end
80
+
81
+ def success?(runner_context)
82
+ runner_context.dig("container_state", "ExitCode") == 0
83
+ end
84
+
85
+ def output(runner_context)
86
+ output = podman!("logs", runner_context["container_ref"]).output
87
+ runner_context["output"] = output
88
+ end
89
+
90
+ private
91
+
92
+ def run_container(image, env, secret, detached: false)
93
+ params = ["run"]
94
+ params << (detached ? :detach : :rm)
95
+ params += env.map { |k, v| [:e, "#{k}=#{v}"] }
96
+ params << [:net, "host"] if @network == "host"
97
+ params << [:secret, secret] if secret
33
98
  params << image
34
99
 
35
100
  logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
36
- result = AwesomeSpawn.run!("podman", :params => params)
37
101
 
38
- [result.exit_status, result.output]
39
- ensure
40
- AwesomeSpawn.run("podman", :params => ["secret", "rm", secret_guid]) if secret_guid
102
+ result = podman!(*params)
103
+ result.output
41
104
  end
42
105
 
43
- private
106
+ def inspect_container(container_id)
107
+ JSON.parse(podman!("inspect", container_id).output)
108
+ end
44
109
 
45
- attr_reader :network
110
+ def delete_container(container_id)
111
+ podman!("rm", container_id)
112
+ rescue
113
+ nil
114
+ end
115
+
116
+ def create_secret(secrets)
117
+ secret_guid = SecureRandom.uuid
118
+ podman!("secret", "create", secret_guid, "-", :in_data => secrets.to_json)
119
+ secret_guid
120
+ end
121
+
122
+ def delete_secret(secret_guid)
123
+ podman!("secret", "rm", secret_guid)
124
+ rescue
125
+ nil
126
+ end
127
+
128
+ def podman!(*args, **kwargs)
129
+ params = podman_global_options + args
130
+
131
+ AwesomeSpawn.run!("podman", :params => params, **kwargs)
132
+ end
133
+
134
+ def podman_global_options
135
+ options = []
136
+ options << [:identity, @identity] if @identity
137
+ options << [:"log-level", @log_level] if @log_level
138
+ options << :noout if @noout
139
+ options << [:root, @root] if @root
140
+ options << [:runroot, @runroot] if @runroot
141
+ options << [:runtime, @runtime] if @runtime
142
+ options << [:"runtime-flag", @runtime_flag] if @runtime_flag
143
+ options << [:"storage-driver", @storage_driver] if @storage_driver
144
+ options << [:"storage-opt", @storage_opt] if @storage_opt
145
+ options << :syslog if @syslog
146
+ options << [:tmpdir, @tmpdir] if @tmpdir
147
+ options << [:"transient-store", @transient_store] if @transient_store
148
+ options << [:volumepath, @volumepath] if @volumepath
149
+ options
150
+ end
46
151
  end
47
152
  end
48
153
  end
@@ -30,7 +30,27 @@ module Floe
30
30
  end
31
31
  end
32
32
 
33
- def run!(image, env = {}, secrets = {})
33
+ def run!(resource, env = {}, secrets = {})
34
+ raise NotImplementedError, "Must be implemented in a subclass"
35
+ end
36
+
37
+ def run_async!(_image, _env = {}, _secrets = {})
38
+ raise NotImplementedError, "Must be implemented in a subclass"
39
+ end
40
+
41
+ def running?(_ref)
42
+ raise NotImplementedError, "Must be implemented in a subclass"
43
+ end
44
+
45
+ def success?(_ref)
46
+ raise NotImplementedError, "Must be implemented in a subclass"
47
+ end
48
+
49
+ def output(_ref)
50
+ raise NotImplementedError, "Must be implemented in a subclass"
51
+ end
52
+
53
+ def cleanup(_ref, _secret)
34
54
  raise NotImplementedError, "Must be implemented in a subclass"
35
55
  end
36
56
  end
@@ -29,9 +29,69 @@ module Floe
29
29
  @comment = payload["Comment"]
30
30
  end
31
31
 
32
+ def run!(_input = nil)
33
+ run_wait until run_nonblock! == 0
34
+ end
35
+
36
+ def run_wait(timeout: 5)
37
+ start = Time.now.utc
38
+
39
+ loop do
40
+ return 0 if ready?
41
+ return Errno::EAGAIN if timeout.zero? || Time.now.utc - start > timeout
42
+
43
+ sleep(1)
44
+ end
45
+ end
46
+
47
+ def run_nonblock!
48
+ start(context.input) unless started?
49
+ return Errno::EAGAIN unless ready?
50
+
51
+ finish
52
+ end
53
+
54
+ def start(_input)
55
+ start_time = Time.now.utc.iso8601
56
+
57
+ context.execution["StartTime"] ||= start_time
58
+ context.state["Guid"] = SecureRandom.uuid
59
+ context.state["EnteredTime"] = start_time
60
+
61
+ logger.info("Running state: [#{context.state_name}] with input [#{context.input}]...")
62
+ end
63
+
64
+ def finish
65
+ finished_time = Time.now.utc
66
+ finished_time_iso = finished_time.iso8601
67
+ entered_time = Time.parse(context.state["EnteredTime"])
68
+
69
+ context.state["FinishedTime"] ||= finished_time_iso
70
+ context.state["Duration"] = finished_time - entered_time
71
+ context.execution["EndTime"] = finished_time_iso if context.next_state.nil?
72
+
73
+ logger.info("Running state: [#{context.state_name}] with input [#{context.input}]...Complete - next state: [#{context.next_state}] output: [#{context.output}]")
74
+
75
+ context.state_history << context.state
76
+
77
+ 0
78
+ end
79
+
32
80
  def context
33
81
  workflow.context
34
82
  end
83
+
84
+ def started?
85
+ context.state.key?("EnteredTime")
86
+ end
87
+
88
+ def ready?
89
+ !started? || !running?
90
+ end
91
+
92
+ def finished?
93
+ context.state.key?("FinishedTime")
94
+ end
35
95
  end
36
96
  end
37
97
  end
@@ -16,16 +16,18 @@ module Floe
16
16
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
17
17
  end
18
18
 
19
- def run!(input)
19
+ def start(input)
20
+ super
20
21
  input = input_path.value(context, input)
21
22
  next_state = choices.detect { |choice| choice.true?(context, input) }&.next || default
22
23
  output = output_path.value(context, input)
23
24
 
24
- [next_state, output]
25
+ context.next_state = next_state
26
+ context.output = output
25
27
  end
26
28
 
27
- def status
28
- "running"
29
+ def running?
30
+ false
29
31
  end
30
32
 
31
33
  def end?
@@ -13,12 +13,16 @@ module Floe
13
13
  @error = payload["Error"]
14
14
  end
15
15
 
16
- def run!(input)
17
- [nil, input]
16
+ def start(input)
17
+ super
18
+ context.state["Error"] = error
19
+ context.state["Cause"] = cause
20
+ context.next_state = nil
21
+ context.output = input
18
22
  end
19
23
 
20
- def status
21
- "errored"
24
+ def running?
25
+ false
22
26
  end
23
27
 
24
28
  def end?
@@ -5,6 +5,7 @@ module Floe
5
5
  module States
6
6
  class Map < Floe::Workflow::State
7
7
  def initialize(*)
8
+ super
8
9
  raise NotImplementedError
9
10
  end
10
11
  end
@@ -5,6 +5,7 @@ module Floe
5
5
  module States
6
6
  class Parallel < Floe::Workflow::State
7
7
  def initialize(*)
8
+ super
8
9
  raise NotImplementedError
9
10
  end
10
11
  end
@@ -19,16 +19,18 @@ module Floe
19
19
  @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
20
20
  end
21
21
 
22
- def run!(input)
22
+ def start(input)
23
+ super
23
24
  output = input_path.value(context, input)
24
25
  output = result_path.set(output, result) if result && result_path
25
26
  output = output_path.value(context, output)
26
27
 
27
- [@end ? nil : @next, output]
28
+ context.next_state = end? ? nil : @next
29
+ context.output = output
28
30
  end
29
31
 
30
- def status
31
- @end ? "success" : "running"
32
+ def running?
33
+ false
32
34
  end
33
35
 
34
36
  def end?
@@ -10,12 +10,14 @@ module Floe
10
10
  super
11
11
  end
12
12
 
13
- def run!(input)
14
- [nil, input]
13
+ def start(input)
14
+ super
15
+ context.next_state = nil
16
+ context.output = input
15
17
  end
16
18
 
17
- def status
18
- "success"
19
+ def running?
20
+ false
19
21
  end
20
22
 
21
23
  def end?
@@ -15,6 +15,7 @@ module Floe
15
15
  @next = payload["Next"]
16
16
  @end = !!payload["End"]
17
17
  @resource = payload["Resource"]
18
+ @runner = Floe::Workflow::Runner.for_resource(@resource)
18
19
  @timeout_seconds = payload["TimeoutSeconds"]
19
20
  @retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
20
21
  @catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
@@ -26,27 +27,37 @@ module Floe
26
27
  @credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
27
28
  end
28
29
 
29
- def run!(input)
30
+ def start(input)
31
+ super
30
32
  input = input_path.value(context, input)
31
33
  input = parameters.value(context, input) if parameters
32
34
 
33
- runner = Floe::Workflow::Runner.for_resource(resource)
34
- _exit_status, results = runner.run!(resource, input, credentials&.value({}, workflow.credentials))
35
+ runner_context = runner.run_async!(resource, input, credentials&.value({}, workflow.credentials))
36
+ context.state["RunnerContext"] = runner_context
37
+ end
35
38
 
36
- output = process_output!(input, results)
37
- [@end ? nil : @next, output]
38
- rescue => err
39
- retrier = self.retry.detect { |r| (r.error_equals & [err.to_s, "States.ALL"]).any? }
40
- retry if retry!(retrier)
39
+ def status
40
+ @end ? "success" : "running"
41
+ end
42
+
43
+ def finish
44
+ results = runner.output(context.state["RunnerContext"])
41
45
 
42
- catcher = self.catch.detect { |c| (c.error_equals & [err.to_s, "States.ALL"]).any? }
43
- raise if catcher.nil?
46
+ if success?
47
+ context.state["Output"] = process_output!(results)
48
+ context.next_state = next_state
49
+ else
50
+ retry_state!(results) || catch_error!(results)
51
+ end
44
52
 
45
- [catcher.next, output]
53
+ super
54
+ ensure
55
+ runner.cleanup(context.state["RunnerContext"])
46
56
  end
47
57
 
48
- def status
49
- @end ? "success" : "running"
58
+ def running?
59
+ runner.status!(context.state["RunnerContext"])
60
+ runner.running?(context.state["RunnerContext"])
50
61
  end
51
62
 
52
63
  def end?
@@ -55,7 +66,22 @@ module Floe
55
66
 
56
67
  private
57
68
 
58
- def retry!(retrier)
69
+ attr_reader :runner
70
+
71
+ def success?
72
+ runner.success?(context.state["RunnerContext"])
73
+ end
74
+
75
+ def find_retrier(error)
76
+ self.retry.detect { |r| (r.error_equals & [error, "States.ALL"]).any? }
77
+ end
78
+
79
+ def find_catcher(error)
80
+ self.catch.detect { |c| (c.error_equals & [error, "States.ALL"]).any? }
81
+ end
82
+
83
+ def retry_state!(error)
84
+ retrier = find_retrier(error)
59
85
  return if retrier.nil?
60
86
 
61
87
  # If a different retrier is hit reset the context
@@ -68,11 +94,26 @@ module Floe
68
94
 
69
95
  return if context["State"]["RetryCount"] > retrier.max_attempts
70
96
 
71
- Kernel.sleep(retrier.sleep_duration(context["State"]["RetryCount"]))
97
+ # TODO: Kernel.sleep(retrier.sleep_duration(context["State"]["RetryCount"]))
98
+ context.next_state = context.state_name
72
99
  true
73
100
  end
74
101
 
75
- def process_output!(output, results)
102
+ def catch_error!(error)
103
+ catcher = find_catcher(error)
104
+ raise error if catcher.nil?
105
+
106
+ context.next_state = catcher.next
107
+ end
108
+
109
+ def process_input(input)
110
+ input = input_path.value(context, input)
111
+ input = parameters.value(context, input) if parameters
112
+ input
113
+ end
114
+
115
+ def process_output!(results)
116
+ output = process_input(context.state["Input"])
76
117
  return output if results.nil?
77
118
  return if output_path.nil?
78
119
 
@@ -86,6 +127,10 @@ module Floe
86
127
  output = result_path.set(output, results)
87
128
  output_path.value(context, output)
88
129
  end
130
+
131
+ def next_state
132
+ end? ? nil : @next
133
+ end
89
134
  end
90
135
  end
91
136
  end
@@ -17,15 +17,22 @@ module Floe
17
17
  @output_path = Path.new(payload.fetch("OutputPath", "$"))
18
18
  end
19
19
 
20
- def run!(input)
20
+ def start(input)
21
+ super
21
22
  input = input_path.value(context, input)
22
- sleep(seconds)
23
- output = output_path.value(context, input)
24
- [@end ? nil : @next, output]
23
+
24
+ context.output = output_path.value(context, input)
25
+ context.next_state = end? ? nil : @next
25
26
  end
26
27
 
27
- def status
28
- @end ? "success" : "running"
28
+ def running?
29
+ now = Time.now.utc
30
+ if now > (Time.parse(context.state["EnteredTime"]) + @seconds)
31
+ context.state["FinishedTime"] = now.iso8601
32
+ false
33
+ else
34
+ true
35
+ end
29
36
  end
30
37
 
31
38
  def end?