floe 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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?