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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2791bb2351fad37f210264d51e059b780937e42a5cb1ab2c9c157ac01e18eea
4
- data.tar.gz: 37436eb4095fc92c4ec4d07e84a09fd27a5b3e794faea09d5ebd37383f886830
3
+ metadata.gz: e7a1c4e51ef3ec3052e8c3c150070464ed1a8b463a3b1237774b8f1b2ff416dc
4
+ data.tar.gz: 8cd6fd6e8c7bed14635d906699f13dde1f5e666018f0c62709972dbddc1999cc
5
5
  SHA512:
6
- metadata.gz: ef1fd3317c12e6af2cdfaaf07dc7cf9c281579d21b623b8ff1a7bcfe906794e19d4b964e3e33560a8d1a3e66abc1c7b1fe85e236c05497aaafea5dc027e8acfd
7
- data.tar.gz: da683adbe9e0513613524f1e6c94380159e0cb07a34b6464c90b76b1ea9227534c1a7af6c1c884873ee9a811cbf90bc40275c4c728c20d2a8732172eab6554d6
6
+ metadata.gz: 73bf61a41d240922786746b0ace87c616311c4bd3f63ac59883096e53b0fce766ef791db3270a29cf257cd5315f5764d69c91bcfd4817efcadfaae72be4c226b
7
+ data.tar.gz: 3cb8f1ad58c7b1b7895363f46871b38b300a66eb40ebb69d2fc8ffd1b5ec303c982c0f49f89f9575b209b5e0671c2570e54670078da4e4916a3718050c9e0b9d
data/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.0] - 2023-09-26
8
+ ### Added
9
+ - Add ability to run workflows asynchronously ([#52](https://github.com/ManageIQ/floe/pull/92))
10
+ - Add Workflow.wait, Workflow#step_nonblock, Workflow#step_nonblock_wait ([#92](https://github.com/ManageIQ/floe/pull/92))
11
+
12
+ ## [0.3.1] - 2023-08-29
13
+ ### Added
14
+ - Add more global podman runner options ([#90])(https://github.com/ManageIQ/floe/pull/90)
15
+
7
16
  ## [0.3.0] - 2023-08-07
8
17
  ### Added
9
18
  - Add --network=host option to Docker/Podman runners ([#81])(https://github.com/ManageIQ/floe/pull/81)
@@ -54,7 +63,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
54
63
  ### Added
55
64
  - Initial release
56
65
 
57
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.3.0...HEAD
66
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.4.0...HEAD
67
+ [0.4.0]: https://github.com/ManageIQ/floe/compare/v0.3.1...v0.4.0
68
+ [0.3.1]: https://github.com/ManageIQ/floe/compare/v0.3.0...v0.3.1
58
69
  [0.3.0]: https://github.com/ManageIQ/floe/compare/v0.2.3...v0.3.0
59
70
  [0.2.3]: https://github.com/ManageIQ/floe/compare/v0.2.2...v0.2.3
60
71
  [0.2.2]: https://github.com/ManageIQ/floe/compare/v0.2.1...v0.2.2
data/Gemfile CHANGED
@@ -8,4 +8,7 @@ require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundle
8
8
  # Specify your gem's dependencies in floe.gemspec
9
9
  gemspec
10
10
 
11
+ gem "manageiq-style"
11
12
  gem "rake", "~> 13.0"
13
+ gem "rspec"
14
+ gem "rubocop"
data/README.md CHANGED
@@ -56,7 +56,7 @@ bundle exec ruby exe/floe --workflow my-workflow.asl --credentials='{"roleArn":
56
56
  ```ruby
57
57
  require 'floe'
58
58
 
59
- workflow = Floe::Workflow.load(File.read("workflow.asl"))
59
+ workflow = Floe::Workflow.load("workflow.asl")
60
60
  workflow.run!
61
61
  ```
62
62
 
@@ -68,10 +68,51 @@ Floe::Workflow::Runner.docker_runner = Floe::Workflow::Runner::Podman.new
68
68
  # Or
69
69
  Floe::Workflow::Runner.docker_runner = Floe::Workflow::Runner::Kubernetes.new("namespace" => "default", "server" => "https://k8s.example.com:6443", "token" => "my-token")
70
70
 
71
- workflow = Floe::Workflow.load(File.read("workflow.asl"))
71
+ workflow = Floe::Workflow.load("workflow.asl")
72
72
  workflow.run!
73
73
  ```
74
74
 
75
+ ### Non-Blocking Workflow Execution
76
+
77
+ It is also possible to step through a workflow without blocking, and any state which
78
+ would block will return `Errno::EAGAIN`.
79
+
80
+ ```ruby
81
+ require 'floe'
82
+
83
+ workflow = Floe::Workflow.load("workflow.asl")
84
+
85
+ # Step through the workflow while it would not block
86
+ workflow.run_nonblock
87
+
88
+ # Go off and do some other task
89
+
90
+ # Continue stepping until the workflow is finished
91
+ workflow.run_nonblock
92
+ ```
93
+
94
+ You can also use the `Floe::Workflow.wait` class method to wait on multiple workflows
95
+ and return all that are ready to be stepped through.
96
+
97
+ ```ruby
98
+ require 'floe'
99
+
100
+ workflow1 = Floe::Workflow.load("workflow1.asl")
101
+ workflow2 = Floe::Workflow.load("workflow2.asl")
102
+
103
+ running_workflows = [workflow1, workflow2]
104
+ until running_workflows.empty?
105
+ # Wait for any of the running workflows to be ready (up to the timeout)
106
+ ready_workflows = Floe::Workflow.wait(running_workflows)
107
+ # Step through the ready workflows until they would block
108
+ ready_workflows.each do |workflow|
109
+ loop while workflow.step_nonblock == 0
110
+ end
111
+ # Remove any finished workflows from the list of running_workflows
112
+ running_workflows.reject!(&:end?)
113
+ end
114
+ ```
115
+
75
116
  ### Docker Runner Options
76
117
 
77
118
  #### Docker
@@ -84,7 +125,20 @@ Options supported by the Docker docker runner are:
84
125
 
85
126
  Options supported by the podman docker runner are:
86
127
 
87
- * `network` - What docker to connect the container to, defaults to `"bridge"`. If you need access to host resources for development you can pass `network=host`.
128
+ * `identity=string` - path to SSH identity file, (CONTAINER_SSHKEY)
129
+ * `log-level=string` - Log messages above specified level (trace, debug, info, warn, warning, error, fatal, panic)
130
+ * `network=string` - What docker to connect the container to, defaults to `"bridge"`. If you need access to host resources for development you can pass `network=host`.
131
+ * `noout=boolean` - do not output to stdout
132
+ * `root=string` - Path to the root directory in which data, including images, is stored
133
+ * `runroot=string` - Path to the 'run directory' where all state information is stored
134
+ * `runtime=string` - Path to the OCI-compatible binary used to run containers
135
+ * `runtime-flag=stringArray` - add global flags for the container runtime
136
+ * `storage-driver=string` - Select which storage driver is used to manage storage of images and containers
137
+ * `storage-opt=stringArray` - Used to pass an option to the storage driver
138
+ * `syslog=boolean` - Output logging information to syslog as well as the console
139
+ * `tmpdir=string` - Path to the tmp directory for libpod state content
140
+ * `transient-store=boolean` - Enable transient container storage
141
+ * `volumepath=string` - Path to the volume directory in which volume data is stored
88
142
 
89
143
  #### Kubernetes
90
144
 
data/exe/floe CHANGED
@@ -18,9 +18,6 @@ Optimist.die(:docker_runner, "must be one of #{Floe::Workflow::Runner::TYPES.joi
18
18
  require "logger"
19
19
  Floe.logger = Logger.new($stdout)
20
20
 
21
- context = Floe::Workflow::Context.new(input: opts[:input])
22
- workflow = Floe::Workflow.load(opts[:workflow], context, opts[:credentials])
23
-
24
21
  runner_klass = case opts[:docker_runner]
25
22
  when "docker"
26
23
  Floe::Workflow::Runner::Docker
@@ -34,6 +31,9 @@ runner_options = opts[:docker_runner_options].to_h { |opt| opt.split("=", 2) }
34
31
 
35
32
  Floe::Workflow::Runner.docker_runner = runner_klass.new(runner_options)
36
33
 
34
+ context = Floe::Workflow::Context.new(:input => opts[:input])
35
+ workflow = Floe::Workflow.load(opts[:workflow], context, opts[:credentials])
36
+
37
37
  workflow.run!
38
38
 
39
39
  puts workflow.output.inspect
data/floe.gemspec CHANGED
@@ -34,8 +34,4 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "kubeclient", "~>4.7"
35
35
  spec.add_dependency "more_core_extensions"
36
36
  spec.add_dependency "optimist", "~>3.0"
37
-
38
- spec.add_development_dependency "manageiq-style"
39
- spec.add_development_dependency "rspec"
40
- spec.add_development_dependency "rubocop"
41
37
  end
@@ -4,7 +4,7 @@ require 'logger'
4
4
 
5
5
  module Floe
6
6
  class NullLogger < Logger
7
- def initialize(*)
7
+ def initialize(*) # rubocop:disable Lint/MissingSuper
8
8
  end
9
9
 
10
10
  def add(*_args, &_block)
data/lib/floe/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Floe
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class And < Floe::Workflow::ChoiceRule
7
+ def true?(context, input)
8
+ children.all? { |choice| choice.true?(context, input) }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -51,27 +51,27 @@ module Floe
51
51
  raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
52
52
  end
53
53
 
54
- def is_null?(value)
54
+ def is_null?(value) # rubocop:disable Naming/PredicateName
55
55
  value.nil?
56
56
  end
57
57
 
58
- def is_present?(value)
58
+ def is_present?(value) # rubocop:disable Naming/PredicateName
59
59
  !value.nil?
60
60
  end
61
61
 
62
- def is_numeric?(value)
62
+ def is_numeric?(value) # rubocop:disable Naming/PredicateName
63
63
  value.kind_of?(Integer) || value.kind_of?(Float)
64
64
  end
65
65
 
66
- def is_string?(value)
66
+ def is_string?(value) # rubocop:disable Naming/PredicateName
67
67
  value.kind_of?(String)
68
68
  end
69
69
 
70
- def is_boolean?(value)
70
+ def is_boolean?(value) # rubocop:disable Naming/PredicateName
71
71
  [true, false].include?(value)
72
72
  end
73
73
 
74
- def is_timestamp?(value)
74
+ def is_timestamp?(value) # rubocop:disable Naming/PredicateName
75
75
  require "date"
76
76
 
77
77
  DateTime.rfc3339(value)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class Not < Floe::Workflow::ChoiceRule
7
+ def true?(context, input)
8
+ choice = children.first
9
+ !choice.true?(context, input)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class ChoiceRule
6
+ class Or < Floe::Workflow::ChoiceRule
7
+ def true?(context, input)
8
+ children.any? { |choice| choice.true?(context, input) }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -4,24 +4,28 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class << self
7
- def true?(payload, context, input)
8
- build(payload).true?(context, input)
9
- end
10
-
11
7
  def build(payload)
12
- data_expression = (payload.keys & %w[And Not Or]).empty?
13
- if data_expression
14
- Floe::Workflow::ChoiceRule::Data.new(payload)
8
+ if (sub_payloads = payload["Not"])
9
+ Floe::Workflow::ChoiceRule::Not.new(payload, build_children([sub_payloads]))
10
+ elsif (sub_payloads = payload["And"])
11
+ Floe::Workflow::ChoiceRule::And.new(payload, build_children(sub_payloads))
12
+ elsif (sub_payloads = payload["Or"])
13
+ Floe::Workflow::ChoiceRule::Or.new(payload, build_children(sub_payloads))
15
14
  else
16
- Floe::Workflow::ChoiceRule::Boolean.new(payload)
15
+ Floe::Workflow::ChoiceRule::Data.new(payload)
17
16
  end
18
17
  end
18
+
19
+ def build_children(sub_payloads)
20
+ sub_payloads.map { |payload| build(payload) }
21
+ end
19
22
  end
20
23
 
21
- attr_reader :next, :payload, :variable
24
+ attr_reader :next, :payload, :variable, :children
22
25
 
23
- def initialize(payload)
24
- @payload = payload
26
+ def initialize(payload, children = nil)
27
+ @payload = payload
28
+ @children = children
25
29
 
26
30
  @next = payload["Next"]
27
31
  @variable = payload["Variable"]
@@ -34,7 +38,7 @@ module Floe
34
38
  private
35
39
 
36
40
  def variable_value(context, input)
37
- @variable_value ||= Path.value(variable, context, input)
41
+ Path.value(variable, context, input)
38
42
  end
39
43
  end
40
44
  end
@@ -11,7 +11,7 @@ module Floe
11
11
  "Input" => input
12
12
  },
13
13
  "State" => {},
14
- "States" => [],
14
+ "StateHistory" => [],
15
15
  "StateMachine" => {},
16
16
  "Task" => {}
17
17
  }
@@ -21,16 +21,64 @@ module Floe
21
21
  @context["Execution"]
22
22
  end
23
23
 
24
+ def started?
25
+ execution.key?("StartTime")
26
+ end
27
+
28
+ def running?
29
+ started? && !ended?
30
+ end
31
+
32
+ def ended?
33
+ execution.key?("EndTime")
34
+ end
35
+
24
36
  def state
25
37
  @context["State"]
26
38
  end
27
39
 
40
+ def input
41
+ state["Input"]
42
+ end
43
+
44
+ def output
45
+ state["Output"]
46
+ end
47
+
48
+ def output=(val)
49
+ state["Output"] = val
50
+ end
51
+
52
+ def state_name
53
+ state["Name"]
54
+ end
55
+
56
+ def next_state
57
+ state["NextState"]
58
+ end
59
+
60
+ def next_state=(val)
61
+ state["NextState"] = val
62
+ end
63
+
64
+ def status
65
+ if !started?
66
+ "pending"
67
+ elsif running?
68
+ "running"
69
+ elsif state["Error"]
70
+ "failure"
71
+ else
72
+ "success"
73
+ end
74
+ end
75
+
28
76
  def state=(val)
29
77
  @context["State"] = val
30
78
  end
31
79
 
32
- def states
33
- @context["States"]
80
+ def state_history
81
+ @context["StateHistory"]
34
82
  end
35
83
 
36
84
  def state_machine
@@ -4,35 +4,76 @@ module Floe
4
4
  class Workflow
5
5
  class PayloadTemplate
6
6
  def initialize(payload)
7
- @payload = payload
7
+ @payload_template = parse_payload(payload)
8
8
  end
9
9
 
10
10
  def value(context, inputs = {})
11
- interpolate_value_nested(payload, context, inputs)
11
+ interpolate_value(payload_template, context, inputs)
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- attr_reader :payload
16
+ attr_reader :payload_template
17
17
 
18
- def interpolate_value_nested(value, context, inputs)
18
+ def parse_payload(value)
19
19
  case value
20
- when Array
21
- value.map { |val| interpolate_value_nested(val, context, inputs) }
22
- when Hash
23
- value.to_h do |key, val|
24
- if key.end_with?(".$")
25
- [key.chomp(".$"), interpolate_value_nested(val, context, inputs)]
26
- else
27
- [key, val]
28
- end
20
+ when Array then parse_payload_array(value)
21
+ when Hash then parse_payload_hash(value)
22
+ when String then parse_payload_string(value)
23
+ else
24
+ value
25
+ end
26
+ end
27
+
28
+ def parse_payload_array(value)
29
+ value.map { |val| parse_payload(val) }
30
+ end
31
+
32
+ def parse_payload_hash(value)
33
+ value.to_h do |key, val|
34
+ if key.end_with?(".$")
35
+ check_key_conflicts(key, value)
36
+
37
+ [key, parse_payload(val)]
38
+ else
39
+ [key, val]
29
40
  end
30
- when String
31
- value.start_with?("$") ? Path.value(value, context, inputs) : value
41
+ end
42
+ end
43
+
44
+ def parse_payload_string(value)
45
+ value.start_with?("$") ? Path.new(value) : value
46
+ end
47
+
48
+ def interpolate_value(value, context, inputs)
49
+ case value
50
+ when Array then interpolate_value_array(value, context, inputs)
51
+ when Hash then interpolate_value_hash(value, context, inputs)
52
+ when Path then value.value(context, inputs)
32
53
  else
33
54
  value
34
55
  end
35
56
  end
57
+
58
+ def interpolate_value_array(value, context, inputs)
59
+ value.map { |val| interpolate_value(val, context, inputs) }
60
+ end
61
+
62
+ def interpolate_value_hash(value, context, inputs)
63
+ value.to_h do |key, val|
64
+ if key.end_with?(".$")
65
+ [key.chomp(".$"), interpolate_value(val, context, inputs)]
66
+ else
67
+ [key, val]
68
+ end
69
+ end
70
+ end
71
+
72
+ def check_key_conflicts(key, value)
73
+ if value.key?(key.chomp(".$"))
74
+ raise Floe::InvalidWorkflowError, "both #{key} and #{key.chomp(".$")} present"
75
+ end
76
+ end
36
77
  end
37
78
  end
38
79
  end
@@ -18,34 +18,103 @@ module Floe
18
18
 
19
19
  image = resource.sub("docker://", "")
20
20
 
21
- params = ["run", :rm]
22
- params += [[:net, "host"]] if network == "host"
23
- params += env.map { |k, v| [:e, "#{k}=#{v}"] } if env
24
-
25
21
  secrets_file = nil
22
+ if secrets && !secrets.empty?
23
+ secrets_file = create_secret(secrets)
24
+ env["_CREDENTIALS"] = "/run/secrets"
25
+ end
26
+
27
+ output = run_container(image, env, secrets_file)
28
+
29
+ {"exit_code" => 0, "output" => output}
30
+ ensure
31
+ cleanup({"secrets_ref" => secrets_file})
32
+ end
33
+
34
+ def run_async!(resource, env = {}, secrets = {})
35
+ raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
36
+
37
+ image = resource.sub("docker://", "")
38
+
39
+ runner_context = {}
26
40
 
27
41
  if secrets && !secrets.empty?
28
- secrets_file = Tempfile.new
29
- secrets_file.write(secrets.to_json)
30
- secrets_file.flush
42
+ runner_context["secrets_ref"] = create_secret(secrets)
43
+ env["_CREDENTIALS"] = "/run/secrets"
44
+ end
31
45
 
32
- params << [:e, "SECRETS=/run/secrets"]
33
- params << [:v, "#{secrets_file.path}:/run/secrets:z"]
46
+ begin
47
+ runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"], :detached => true)
48
+ rescue
49
+ cleanup(runner_context)
50
+ raise
34
51
  end
35
52
 
36
- params << image
53
+ runner_context
54
+ end
37
55
 
38
- logger.debug("Running docker: #{AwesomeSpawn.build_command_line("docker", params)}")
39
- result = AwesomeSpawn.run!("docker", :params => params)
56
+ def cleanup(runner_context)
57
+ container_id, secrets_file = runner_context.values_at("container_ref", "secrets_ref")
40
58
 
41
- [result.exit_status, result.output]
42
- ensure
43
- secrets_file&.close!
59
+ delete_container(container_id) if container_id
60
+ File.unlink(secrets_file) if secrets_file && File.exist?(secrets_file)
61
+ end
62
+
63
+ def status!(runner_context)
64
+ runner_context["container_state"] = inspect_container(runner_context["container_ref"]).first&.dig("State")
65
+ end
66
+
67
+ def running?(runner_context)
68
+ runner_context.dig("container_state", "Running")
69
+ end
70
+
71
+ def success?(runner_context)
72
+ runner_context.dig("container_state", "ExitCode") == 0
73
+ end
74
+
75
+ def output(runner_context)
76
+ output = docker!("logs", runner_context["container_ref"]).output
77
+ runner_context["output"] = output
44
78
  end
45
79
 
46
80
  private
47
81
 
48
82
  attr_reader :network
83
+
84
+ def run_container(image, env, secrets_file, detached: false)
85
+ params = ["run"]
86
+ params << (detached ? :detach : :rm)
87
+ params += env.map { |k, v| [:e, "#{k}=#{v}"] }
88
+ params << [:net, "host"] if @network == "host"
89
+ params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
90
+ params << image
91
+
92
+ logger.debug("Running docker: #{AwesomeSpawn.build_command_line("docker", params)}")
93
+
94
+ result = docker!(*params)
95
+ result.output
96
+ end
97
+
98
+ def inspect_container(container_id)
99
+ JSON.parse(docker!("inspect", container_id).output)
100
+ end
101
+
102
+ def delete_container(container_id)
103
+ docker!("rm", container_id)
104
+ rescue
105
+ nil
106
+ end
107
+
108
+ def create_secret(secrets)
109
+ secrets_file = Tempfile.new
110
+ secrets_file.write(secrets.to_json)
111
+ secrets_file.close
112
+ secrets_file.path
113
+ end
114
+
115
+ def docker!(*params, **kwargs)
116
+ AwesomeSpawn.run!("docker", :params => params, **kwargs)
117
+ end
49
118
  end
50
119
  end
51
120
  end