floe 0.3.0 → 0.4.0

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