floe 0.1.1 → 0.2.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: 12212f802a614d03428c29b36a882efc7b12a2ae0e61ec1ac118aea28c34d95b
4
- data.tar.gz: 5d62f266c83af21dffce7f8c2a1a8a3686ad24f85ad5421bced3cd483903ccb8
3
+ metadata.gz: 3a8984568027905b79c95ab066b45f0e87872bbb290022a018f3574d47e3054e
4
+ data.tar.gz: 73390d044e75811f2b1c91acfa1aea1814b38f1d3a60ad4b9bd0eed88d131af0
5
5
  SHA512:
6
- metadata.gz: 16f88b2b6f1cbb7baa4038f5c6f55da0a1f74b2a1a54b49e74ac24480dddb0b64d59fda3c6b26512a116d8060be3a171a68f6f52f4a9e4ff9a971737312c327c
7
- data.tar.gz: b5a318594423446d5de524950a894bbcac334e5875cc2d52cdfcfaef08f068eb946a1a9120c1626392917a8a325944652b4a8d8fb21aa270393df25a3460fac7
6
+ metadata.gz: b5d6cec81ef1adc60cc5912cc34275c032f250d84361982ac556284ada3c807f3189eef5357c193ae5b76db795f328b761fd53fd5dbfcd3468e5fc439d54c9fe
7
+ data.tar.gz: d4a9ce5beea167b41ec66b1b049c6466a41c91b64076792e87b34e92787e19120965580c638a755c5a1e406398f4b1842851105dad2d388c21b7db76944c7df7
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_gem:
2
+ manageiq-style: ".rubocop_base.yml"
3
+ inherit_from:
4
+ - ".rubocop_local.yml"
data/.rubocop_cc.yml ADDED
@@ -0,0 +1,4 @@
1
+ inherit_from:
2
+ - ".rubocop_base.yml"
3
+ - ".rubocop_cc_base.yml"
4
+ - ".rubocop_local.yml"
@@ -0,0 +1,2 @@
1
+ Rails:
2
+ Enabled: false
data/.yamllint ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ extends: relaxed
3
+
4
+ rules:
5
+ indentation:
6
+ indent-sequences: false
7
+ line-length:
8
+ max: 120
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] - 2023-07-05
8
+ ### Added
9
+ - Add ability to pass options to `Floe::Workflow::Runner` (#48)
10
+ - Add kubeconfig file support to `Floe::Workflow::Runner::Kubernetes` (#53)
11
+
12
+ ### Removed
13
+ - Remove to_dot/to_svg code (#54)
14
+
15
+ ### Fixed
16
+ - Fixed default rake task to spec (#55)
17
+
7
18
  ## [0.1.1] - 2023-06-05
8
19
  ### Fixed
9
20
  - Fix States::Wait Path initializer arguments (#47)
data/README.md CHANGED
@@ -28,6 +28,14 @@ Floe can be run as a command-line utility or as a ruby class.
28
28
  bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}'
29
29
  ```
30
30
 
31
+ By default Floe will use `docker` to run `docker://` type resources, but `podman` and `kubernetes` are also supported runners.
32
+ A different runner can be specified with the `--docker-runner` option:
33
+
34
+ ```
35
+ bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}' --docker-runner podman
36
+ bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}' --docker-runner kubernetes --docker-runner-options namespace=default server=https://k8s.example.com:6443 token=my-token
37
+ ```
38
+
31
39
  ### Ruby Library
32
40
 
33
41
  ```ruby
@@ -37,6 +45,32 @@ workflow = Floe::Workflow.load(File.read("workflow.asl"))
37
45
  workflow.run!
38
46
  ```
39
47
 
48
+ You can also specify a specific docker runner and runner options:
49
+ ```ruby
50
+ require 'floe'
51
+
52
+ Floe::Workflow::Runner.docker_runner = Floe::Workflow::Runner::Podman.new
53
+ # Or
54
+ Floe::Workflow::Runner.docker_runner = Floe::Workflow::Runner::Kubernetes.new("namespace" => "default", "server" => "https://k8s.example.com:6443", "token" => "my-token")
55
+
56
+ workflow = Floe::Workflow.load(File.read("workflow.asl"))
57
+ workflow.run!
58
+ ```
59
+
60
+ ### Docker Runner Options
61
+
62
+ #### Kubernetes
63
+
64
+ Options supported by the kubernetes docker runner are:
65
+
66
+ * `kubeconfig` - Path to a kubeconfig file, defaults to `KUBECONFIG` environment variable or `~/.kube/config`
67
+ * `kubeconfig_context` - Context to use in the kubeconfig file, defaults to `"default"`
68
+ * `namespace` - Namespace to use when creating kubernetes resources, defaults to `"default"`
69
+ * `server` - A kubernetes API Server URL, overrides anything in your kubeconfig file. If set `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` will be used
70
+ * `token` - A bearer_token to use to authenticate to the kubernetes API, overrides anything in your kubeconfig file. If present, `/run/secrets/kubernetes.io/serviceaccount/token` will be used
71
+ * `ca_file` - Path to a certificate-authority file for the kubernetes API, only valid if server and token are passed. If present `/run/secrets/kubernetes.io/serviceaccount/ca.crt` will be used
72
+ * `verify_ssl` - Controls if the kubernetes API certificate-authority should be verified, defaults to "true", only vaild if server and token are passed
73
+
40
74
  ## Development
41
75
 
42
76
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new("spec")
7
+ task :default => :spec
data/exe/floe CHANGED
@@ -5,17 +5,35 @@ require "floe"
5
5
  require "optimist"
6
6
 
7
7
  opts = Optimist.options do
8
- version "v#{Floe::VERSION}\n"
9
- opt :workflow, "Path to your workflow json", :type => :string, :required => true
10
- opt :inputs, "JSON payload to input to the workflow", :type => :string, :default => '{}'
11
- opt :credentials, "JSON payload with credentials", :type => :string, :default => '{}'
8
+ version("v#{Floe::VERSION}\n")
9
+ opt :workflow, "Path to your workflow json", :type => :string, :required => true
10
+ opt :input, "JSON payload to input to the workflow", :default => '{}'
11
+ opt :credentials, "JSON payload with credentials", :default => "{}"
12
+ opt :docker_runner, "Type of runner for docker images", :default => "docker"
13
+ opt :docker_runner_options, "Options to pass to the runner", :type => :strings
12
14
  end
13
15
 
16
+ Optimist.die(:docker_runner, "must be one of #{Floe::Workflow::Runner::TYPES.join(", ")}") unless Floe::Workflow::Runner::TYPES.include?(opts[:docker_runner])
17
+
14
18
  require "logger"
15
- Floe.logger = Logger.new(STDOUT)
19
+ Floe.logger = Logger.new($stdout)
20
+
21
+ context = Floe::Workflow::Context.new(input: opts[:input])
22
+ workflow = Floe::Workflow.load(opts[:workflow], context, opts[:credentials])
23
+
24
+ runner_klass = case opts[:docker_runner]
25
+ when "docker"
26
+ Floe::Workflow::Runner::Docker
27
+ when "podman"
28
+ Floe::Workflow::Runner::Podman
29
+ when "kubernetes"
30
+ Floe::Workflow::Runner::Kubernetes
31
+ end
32
+
33
+ runner_options = opts[:docker_runner_options].to_h { |opt| opt.split("=", 2) }
16
34
 
17
- workflow = Floe::Workflow.load(opts[:workflow], opts[:inputs], opts[:credentials])
35
+ Floe::Workflow::Runner.docker_runner = runner_klass.new(runner_options)
18
36
 
19
- output = workflow.run!
37
+ workflow.run!
20
38
 
21
- puts output.inspect
39
+ puts workflow.context.state["Output"].inspect
data/floe.gemspec CHANGED
@@ -31,9 +31,11 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_dependency "awesome_spawn", "~>1.0"
33
33
  spec.add_dependency "jsonpath", "~>1.1"
34
- spec.add_dependency "optimist", "~>3.0"
34
+ spec.add_dependency "kubeclient", "~>4.7"
35
35
  spec.add_dependency "more_core_extensions"
36
+ spec.add_dependency "optimist", "~>3.0"
36
37
 
37
- spec.add_development_dependency "rubocop"
38
+ spec.add_development_dependency "manageiq-style"
38
39
  spec.add_development_dependency "rspec"
40
+ spec.add_development_dependency "rubocop"
39
41
  end
@@ -4,7 +4,7 @@ require 'logger'
4
4
 
5
5
  module Floe
6
6
  class NullLogger < Logger
7
- def initialize(*_args)
7
+ def initialize(*)
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.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,19 +5,18 @@ module Floe
5
5
  class ChoiceRule
6
6
  class Data < Floe::Workflow::ChoiceRule
7
7
  def true?(context, input)
8
-
9
8
  lhs = variable_value(context, input)
10
9
  rhs = compare_value(context, input)
11
10
 
12
11
  validate!(lhs)
13
12
 
14
13
  case compare_key
15
- when "IsNull"; is_null?(lhs)
16
- when "IsPresent"; is_present?(lhs)
17
- when "IsNumeric"; is_numeric?(lhs)
18
- when "IsString"; is_string?(lhs)
19
- when "IsBoolean"; is_boolean?(lhs)
20
- when "IsTimestamp"; is_timestamp?(lhs)
14
+ when "IsNull" then is_null?(lhs)
15
+ when "IsPresent" then is_present?(lhs)
16
+ when "IsNumeric" then is_numeric?(lhs)
17
+ when "IsString" then is_string?(lhs)
18
+ when "IsBoolean" then is_boolean?(lhs)
19
+ when "IsTimestamp" then is_timestamp?(lhs)
21
20
  when "StringEquals", "StringEqualsPath",
22
21
  "NumericEquals", "NumericEqualsPath",
23
22
  "BooleanEquals", "BooleanEqualsPath",
@@ -40,7 +39,7 @@ module Floe
40
39
  "TimestampGreaterThanEquals", "TimestampGreaterThanEqualsPath"
41
40
  lhs >= rhs
42
41
  when "StringMatches"
43
- lhs.match?(Regexp.escape(rhs).gsub('\*','.*?'))
42
+ lhs.match?(Regexp.escape(rhs).gsub('\*', '.*?'))
44
43
  else
45
44
  raise Floe::InvalidWorkflowError, "Invalid choice [#{compare_key}]"
46
45
  end
@@ -49,7 +48,7 @@ module Floe
49
48
  private
50
49
 
51
50
  def validate!(value)
52
- raise RuntimeError, "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
51
+ raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
53
52
  end
54
53
 
55
54
  def is_null?(value)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Floe
4
+ class Workflow
5
+ class Context
6
+ def initialize(context = nil, input: {})
7
+ context = JSON.parse(context) if context.kind_of?(String)
8
+
9
+ @context = context || {
10
+ "Execution" => {
11
+ "Input" => input
12
+ },
13
+ "State" => {},
14
+ "States" => [],
15
+ "StateMachine" => {},
16
+ "Task" => {}
17
+ }
18
+ end
19
+
20
+ def execution
21
+ @context["Execution"]
22
+ end
23
+
24
+ def state
25
+ @context["State"]
26
+ end
27
+
28
+ def states
29
+ @context["States"]
30
+ end
31
+
32
+ def state_machine
33
+ @context["StateMachine"]
34
+ end
35
+
36
+ def task
37
+ @context["Task"]
38
+ end
39
+
40
+ def [](key)
41
+ @context[key]
42
+ end
43
+
44
+ def []=(key, val)
45
+ @context[key] = val
46
+ end
47
+
48
+ def dig(*args)
49
+ @context.dig(*args)
50
+ end
51
+
52
+ def to_h
53
+ @context
54
+ end
55
+ end
56
+ end
57
+ end
@@ -4,7 +4,7 @@ module Floe
4
4
  class Workflow
5
5
  class ReferencePath < Path
6
6
  class << self
7
- def set (payload, context, value)
7
+ def set(payload, context, value)
8
8
  new(payload).set(context, value)
9
9
  end
10
10
  end
@@ -24,7 +24,7 @@ module Floe
24
24
  path = JsonPath.new(payload)
25
25
  .path[1..]
26
26
  .map { |v| v.match(/\[(?<name>.+)\]/)["name"] }
27
- .map { |v| v[0] == "'" ? v.gsub("'", "") : v.to_i }
27
+ .map { |v| v[0] == "'" ? v.delete("'") : v.to_i }
28
28
  .compact
29
29
 
30
30
  # If the payload is '$' then merge the value into the context
@@ -4,15 +4,36 @@ module Floe
4
4
  class Workflow
5
5
  class Runner
6
6
  class Kubernetes < Floe::Workflow::Runner
7
- attr_reader :namespace
7
+ TOKEN_FILE = "/run/secrets/kubernetes.io/serviceaccount/token"
8
+ CA_CERT_FILE = "/run/secrets/kubernetes.io/serviceaccount/ca.crt"
8
9
 
9
- def initialize(*)
10
+ def initialize(options = {})
10
11
  require "awesome_spawn"
11
12
  require "securerandom"
12
13
  require "base64"
14
+ require "kubeclient"
13
15
  require "yaml"
14
16
 
15
- @namespace = ENV.fetch("DOCKER_RUNNER_NAMESPACE", "default")
17
+ @kubeconfig_file = ENV.fetch("KUBECONFIG", nil) || options.fetch("kubeconfig", File.join(Dir.home, ".kube", "config"))
18
+ @kubeconfig_context = options["kubeconfig_context"]
19
+
20
+ @token = options["token"]
21
+ @token ||= File.read(options["token_file"]) if options.key?("token_file")
22
+ @token ||= File.read(TOKEN_FILE) if File.exist?(TOKEN_FILE)
23
+
24
+ @server = options["server"]
25
+ @server ||= URI::HTTPS.build(:host => ENV.fetch("KUBERNETES_SERVICE_HOST"), :port => ENV.fetch("KUBERNETES_SERVICE_PORT", 6443)) if ENV.key?("KUBERNETES_SERVICE_HOST")
26
+
27
+ @ca_file = options["ca_file"]
28
+ @ca_file ||= CA_CERT_FILE if File.exist?(CA_CERT_FILE)
29
+
30
+ @verify_ssl = options["verify_ssl"] == "false" ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
31
+
32
+ if server.nil? && token.nil? && !File.exist?(kubeconfig_file)
33
+ raise ArgumentError, "Missing connections options, provide a kubeconfig file or pass server and token via --docker-runner-options"
34
+ end
35
+
36
+ @namespace = options.fetch("namespace", "default")
16
37
 
17
38
  super
18
39
  end
@@ -20,25 +41,52 @@ module Floe
20
41
  def run!(resource, env = {}, secrets = {})
21
42
  raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
22
43
 
23
- image = resource.sub("docker://", "")
24
- name = pod_name(image)
25
- secret = create_secret!(secrets) unless secrets&.empty?
26
- overrides = pod_spec(image, env, secret)
44
+ image = resource.sub("docker://", "")
45
+ name = pod_name(image)
46
+ secret = create_secret!(secrets) if secrets && !secrets.empty?
47
+
48
+ begin
49
+ create_pod!(name, image, env, secret)
50
+ while running?(name)
51
+ sleep(1)
52
+ end
53
+
54
+ exit_status = success?(name) ? 0 : 1
55
+ results = output(name)
27
56
 
28
- result = kubectl_run!(image, name, overrides)
57
+ [exit_status, results]
58
+ ensure
59
+ cleanup(name, secret)
60
+ end
61
+ end
29
62
 
30
- # Kubectl prints that the pod was deleted, strip this from the output
31
- output = result.output.gsub(/pod \"#{name}\" deleted/, "")
63
+ def running?(pod_name)
64
+ %w[Pending Running].include?(pod_info(pod_name).dig("status", "phase"))
65
+ end
66
+
67
+ def success?(pod_name)
68
+ pod_info(pod_name).dig("status", "phase") == "Succeeded"
69
+ end
32
70
 
33
- [result.exit_status, output]
34
- ensure
35
- delete_secret!(secret) if secret
71
+ def output(pod)
72
+ kubeclient.get_pod_log(pod, namespace).body
73
+ end
74
+
75
+ def cleanup(pod, secret)
76
+ delete_pod(pod) if pod
77
+ delete_secret(secret) if secret
36
78
  end
37
79
 
38
80
  private
39
81
 
82
+ attr_reader :ca_file, :kubeconfig_file, :kubeconfig_context, :namespace, :server, :token, :verify_ssl
83
+
84
+ def pod_info(pod_name)
85
+ kubeclient.get_pod(pod_name, namespace)
86
+ end
87
+
40
88
  def container_name(image)
41
- image.match(%r{^(?<repository>.+\/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
89
+ image.match(%r{^(?<repository>.+/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
42
90
  end
43
91
 
44
92
  def pod_name(image)
@@ -48,23 +96,44 @@ module Floe
48
96
  "#{container_short_name}-#{SecureRandom.uuid}"
49
97
  end
50
98
 
51
- def pod_spec(image, env, secret = nil)
52
- container_spec = {
53
- "name" => container_name(image),
54
- "image" => image,
55
- "env" => env.to_h.map { |k, v| {"name" => k, "value" => v} }
99
+ def pod_spec(name, image, env, secret = nil)
100
+ spec = {
101
+ :kind => "Pod",
102
+ :apiVersion => "v1",
103
+ :metadata => {
104
+ :name => name,
105
+ :namespace => namespace
106
+ },
107
+ :spec => {
108
+ :containers => [
109
+ {
110
+ :name => container_name(image),
111
+ :image => image,
112
+ :env => env.map { |k, v| {:name => k, :value => v.to_s} }
113
+ }
114
+ ],
115
+ :restartPolicy => "Never"
116
+ }
56
117
  }
57
118
 
58
- spec = {"spec" => {"containers" => [container_spec]}}
59
-
60
119
  if secret
61
- spec["spec"]["volumes"] = [{"name" => "secret-volume", "secret" => {"secretName" => secret}}]
62
- container_spec["env"] << {"name" => "SECRETS", "value" => "/run/secrets/#{secret}/secret"}
63
- container_spec["volumeMounts"] = [
120
+ spec[:spec][:volumes] = [
64
121
  {
65
- "name" => "secret-volume",
66
- "mountPath" => "/run/secrets/#{secret}",
67
- "readOnly" => true
122
+ :name => "secret-volume",
123
+ :secret => {:secretName => secret}
124
+ }
125
+ ]
126
+
127
+ spec[:spec][:containers][0][:env] << {
128
+ :name => "SECRETS",
129
+ :value => "/run/secrets/#{secret}/secret"
130
+ }
131
+
132
+ spec[:spec][:containers][0][:volumeMounts] = [
133
+ {
134
+ :name => "secret-volume",
135
+ :mountPath => "/run/secrets/#{secret}",
136
+ :readOnly => true
68
137
  }
69
138
  ]
70
139
  end
@@ -72,45 +141,75 @@ module Floe
72
141
  spec
73
142
  end
74
143
 
144
+ def create_pod!(name, image, env, secret = nil)
145
+ kubeclient.create_pod(pod_spec(name, image, env, secret))
146
+ end
147
+
148
+ def delete_pod!(name)
149
+ kubeclient.delete_pod(name, namespace)
150
+ end
151
+
152
+ def delete_pod(name)
153
+ delete_pod!(name)
154
+ rescue
155
+ nil
156
+ end
157
+
75
158
  def create_secret!(secrets)
76
159
  secret_name = SecureRandom.uuid
77
160
 
78
- secret_yaml = {
79
- "kind" => "Secret",
80
- "apiVersion" => "v1",
81
- "metadata" => {
82
- "name" => secret_name,
83
- "namespace" => namespace
161
+ secret_config = {
162
+ :kind => "Secret",
163
+ :apiVersion => "v1",
164
+ :metadata => {
165
+ :name => secret_name,
166
+ :namespace => namespace
84
167
  },
85
- "data" => {
86
- "secret" => Base64.urlsafe_encode64(secrets.to_json)
168
+ :data => {
169
+ :secret => Base64.urlsafe_encode64(secrets.to_json)
87
170
  },
88
- "type" => "Opaque"
89
- }.to_yaml
171
+ :type => "Opaque"
172
+ }
90
173
 
91
- kubectl!("create", "-f", "-", :in_data => secret_yaml)
174
+ kubeclient.create_secret(secret_config)
92
175
 
93
176
  secret_name
94
177
  end
95
178
 
96
179
  def delete_secret!(secret_name)
97
- kubectl!("delete", "secret", secret_name, [:namespace, namespace])
180
+ kubeclient.delete_secret(secret_name, namespace)
98
181
  end
99
182
 
100
- def kubectl!(*params, **kwargs)
101
- AwesomeSpawn.run!("kubectl", :params => params, **kwargs)
183
+ def delete_secret(name)
184
+ delete_secret!(name)
185
+ rescue
186
+ nil
102
187
  end
103
188
 
104
- def kubectl_run!(image, name, overrides = nil)
105
- params = [
106
- "run", :rm, :attach, [:image, image], [:restart, "Never"], [:namespace, namespace], name
107
- ]
189
+ def kubeclient
190
+ return @kubeclient unless @kubeclient.nil?
191
+
192
+ if server && token
193
+ api_endpoint = server
194
+ auth_options = {:bearer_token => token}
195
+ ssl_options = {:verify_ssl => verify_ssl}
196
+ ssl_options[:ca_file] = ca_file if ca_file
197
+ else
198
+ context = kubeconfig&.context(kubeconfig_context)
199
+ raise ArgumentError, "Missing connections options, provide a kubeconfig file or pass server and token via --docker-runner-options" if context.nil?
200
+
201
+ api_endpoint = context.api_endpoint
202
+ auth_options = context.auth_options
203
+ ssl_options = context.ssl_options
204
+ end
108
205
 
109
- params << "--overrides=#{overrides.to_json}" if overrides
206
+ @kubeclient = Kubeclient::Client.new(api_endpoint, "v1", :ssl_options => ssl_options, :auth_options => auth_options).tap(&:discover)
207
+ end
110
208
 
111
- logger.debug("Running kubectl: #{AwesomeSpawn.build_command_line("kubectl", params)}")
209
+ def kubeconfig
210
+ return if kubeconfig_file.nil? || !File.exist?(kubeconfig_file)
112
211
 
113
- kubectl!(*params)
212
+ Kubeclient::Config.read(kubeconfig_file)
114
213
  end
115
214
  end
116
215
  end
@@ -5,11 +5,16 @@ module Floe
5
5
  class Runner
6
6
  include Logging
7
7
 
8
+ TYPES = %w[docker podman kubernetes].freeze
9
+
10
+ def initialize(_options = {})
11
+ end
12
+
8
13
  class << self
9
- attr_writer :docker_runner_klass
14
+ attr_writer :docker_runner
10
15
 
11
- def docker_runner_klass
12
- @docker_runner_klass ||= const_get(ENV.fetch("DOCKER_RUNNER", "docker").capitalize)
16
+ def docker_runner
17
+ @docker_runner ||= Floe::Workflow::Runner::Docker.new
13
18
  end
14
19
 
15
20
  def for_resource(resource)
@@ -18,7 +23,7 @@ module Floe
18
23
  scheme = resource.split("://").first
19
24
  case scheme
20
25
  when "docker"
21
- docker_runner_klass.new
26
+ docker_runner
22
27
  else
23
28
  raise "Invalid resource scheme [#{scheme}]"
24
29
  end
@@ -38,6 +38,10 @@ module Floe
38
38
  workflow.context
39
39
  end
40
40
 
41
+ def status
42
+ end? ? "success" : "running"
43
+ end
44
+
41
45
  def run!(input)
42
46
  logger.info("Running state: [#{name}] with input [#{input}]")
43
47
 
@@ -53,24 +57,6 @@ module Floe
53
57
 
54
58
  [next_state, output]
55
59
  end
56
-
57
- def to_dot
58
- String.new.tap do |s|
59
- s << " #{name}"
60
-
61
- attributes = to_dot_attributes
62
- s << " [ #{attributes.to_a.map { |kv| kv.join("=") }.join(" ")} ]" unless attributes.empty?
63
- end
64
- end
65
-
66
- private def to_dot_attributes
67
- end? ? {:style => "bold"} : {}
68
- end
69
-
70
- def to_dot_transitions
71
- next_state_name = payload["Next"] unless end?
72
- Array(next_state_name && " #{name} -> #{next_state_name}")
73
- end
74
60
  end
75
61
  end
76
62
  end
@@ -26,27 +26,6 @@ module Floe
26
26
  [output, next_state]
27
27
  end
28
28
  end
29
-
30
- private def to_dot_attributes
31
- super.merge(:shape => "diamond")
32
- end
33
-
34
- def to_dot_transitions
35
- [].tap do |a|
36
- choices.each do |choice|
37
- choice_label =
38
- if choice.payload["NumericEquals"]
39
- "#{choice.variable} == #{choice.payload["NumericEquals"]}"
40
- else
41
- "Unknown" # TODO
42
- end
43
-
44
- a << " #{name} -> #{choice.next} [ label=#{choice_label.inspect} ]"
45
- end
46
-
47
- a << " #{name} -> #{default} [ label=\"Default\" ]" if default
48
- end
49
- end
50
29
  end
51
30
  end
52
31
  end
@@ -28,8 +28,8 @@ module Floe
28
28
  true
29
29
  end
30
30
 
31
- private def to_dot_attributes
32
- super.merge(:color => "red")
31
+ def status
32
+ "errored"
33
33
  end
34
34
  end
35
35
  end
@@ -16,10 +16,6 @@ module Floe
16
16
  def end?
17
17
  true # TODO: Handle if this is ending a parallel or map state
18
18
  end
19
-
20
- private def to_dot_attributes
21
- super.merge(:color => "green")
22
- end
23
19
  end
24
20
  end
25
21
  end
@@ -51,15 +51,16 @@ module Floe
51
51
  return if retrier.nil?
52
52
 
53
53
  # If a different retrier is hit reset the context
54
- if !context.key?("retrier") || context["retrier"]["error_equals"] != retrier.error_equals
55
- context["retrier"] = {"error_equals" => retrier.error_equals, "retry_count" => 0}
54
+ if !context["State"].key?("RetryCount") || context["State"]["Retrier"] != retrier.error_equals
55
+ context["State"]["RetryCount"] = 0
56
+ context["State"]["Retrier"] = retrier.error_equals
56
57
  end
57
58
 
58
- context["retrier"]["retry_count"] += 1
59
+ context["State"]["RetryCount"] += 1
59
60
 
60
- return if context["retrier"]["retry_count"] > retrier.max_attempts
61
+ return if context["State"]["RetryCount"] > retrier.max_attempts
61
62
 
62
- Kernel.sleep(retrier.sleep_duration(context["retrier"]["retry_count"]))
63
+ Kernel.sleep(retrier.sleep_duration(context["State"]["RetryCount"]))
63
64
  true
64
65
  end
65
66
 
@@ -17,7 +17,10 @@ module Floe
17
17
  end
18
18
 
19
19
  def run!(*)
20
- super { sleep(seconds); nil }
20
+ super do
21
+ sleep(seconds)
22
+ nil
23
+ end
21
24
  end
22
25
  end
23
26
  end
data/lib/floe/workflow.rb CHANGED
@@ -5,78 +5,74 @@ require "json"
5
5
  module Floe
6
6
  class Workflow
7
7
  class << self
8
- def load(path_or_io, context = {}, credentials = {})
8
+ def load(path_or_io, context = nil, credentials = {})
9
9
  payload = path_or_io.respond_to?(:read) ? path_or_io.read : File.read(path_or_io)
10
10
  new(payload, context, credentials)
11
11
  end
12
12
  end
13
13
 
14
- attr_reader :context, :credentials, :first_state, :payload, :states, :states_by_name, :start_at
14
+ attr_reader :context, :credentials, :payload, :states, :states_by_name, :current_state, :status
15
15
 
16
- def initialize(payload, context = {}, credentials = {})
16
+ def initialize(payload, context = nil, credentials = {})
17
17
  payload = JSON.parse(payload) if payload.kind_of?(String)
18
18
  context = JSON.parse(context) if context.kind_of?(String)
19
19
  credentials = JSON.parse(credentials) if credentials.kind_of?(String)
20
+ context = Context.new(context) unless context.kind_of?(Context)
21
+
22
+ @payload = payload
23
+ @context = context || {"global" => {}}
24
+ @credentials = credentials
20
25
 
21
- @payload = payload
22
- @context = context
23
- @credentials = credentials
24
26
  @states = payload["States"].to_a.map { |name, state| State.build!(self, name, state) }
25
- @states_by_name = states.to_h { |state| [state.name, state] }
26
- @start_at = @payload["StartAt"]
27
- @first_state = @states_by_name[@start_at]
27
+ @states_by_name = @states.each_with_object({}) { |state, result| result[state.name] = state }
28
+ start_at = @payload["StartAt"]
29
+
30
+ current_state_name = @context["State"]["Name"] || start_at
31
+ @current_state = @states_by_name[current_state_name]
32
+
33
+ @status = current_state_name == start_at ? "pending" : current_state.status
28
34
  rescue JSON::ParserError => err
29
35
  raise Floe::InvalidWorkflowError, err.message
30
36
  end
31
37
 
32
- def run!
33
- state = first_state
34
- input = context.dup
38
+ def step
39
+ @status = "running" if @status == "pending"
40
+ @context["Execution"]["StartTime"] ||= Time.now.utc
35
41
 
36
- until state.nil?
37
- state, output = state.run!(input)
38
- input = output
39
- end
42
+ input = @context["State"]["Output"] || @context["Execution"]["Input"].dup
40
43
 
41
- output
42
- end
44
+ tick = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
+ next_state, output = current_state.run!(input)
46
+ tock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
47
 
44
- def to_dot
45
- String.new.tap do |s|
46
- s << "digraph {\n"
47
- states.each do |state|
48
- s << state.to_dot << "\n"
49
- end
50
- s << "\n"
51
- states.each do |state|
52
- Array(state.to_dot_transitions).each do |transition|
53
- s << transition << "\n"
54
- end
55
- end
56
- s << "}\n"
57
- end
58
- end
48
+ @context["State"] = {
49
+ "EnteredTime" => tick,
50
+ "FinishedTime" => tock,
51
+ "Duration" => tock - tick,
52
+ "Output" => output,
53
+ "Name" => next_state&.name,
54
+ "Input" => output
55
+ }
59
56
 
60
- def to_svg(path: nil)
61
- require "open3"
62
- out, err, _status = Open3.capture3("dot -Tsvg", :stdin_data => to_dot)
57
+ @context["States"] << @context["State"]
63
58
 
64
- raise "Error from graphviz:\n#{err}" if err && !err.empty?
59
+ @status = current_state.status
65
60
 
66
- File.write(path, out) if path
61
+ next_state_name = next_state&.name
62
+ @current_state = next_state_name && @states_by_name[next_state_name]
67
63
 
68
- out
64
+ self
69
65
  end
70
66
 
71
- def to_ascii(path: nil)
72
- require "open3"
73
- out, err, _status = Open3.capture3("graph-easy", :stdin_data => to_dot)
74
-
75
- raise "Error from graph-easy:\n#{err}" if err && !err.empty?
76
-
77
- File.write(path, out) if path
67
+ def run!
68
+ until end?
69
+ step
70
+ end
71
+ self
72
+ end
78
73
 
79
- out
74
+ def end?
75
+ current_state.nil?
80
76
  end
81
77
  end
82
78
  end
data/lib/floe.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "floe/workflow/catcher"
10
10
  require_relative "floe/workflow/choice_rule"
11
11
  require_relative "floe/workflow/choice_rule/boolean"
12
12
  require_relative "floe/workflow/choice_rule/data"
13
+ require_relative "floe/workflow/context"
13
14
  require_relative "floe/workflow/path"
14
15
  require_relative "floe/workflow/payload_template"
15
16
  require_relative "floe/workflow/reference_path"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-05 00:00:00.000000000 Z
11
+ date: 2023-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.1'
41
41
  - !ruby/object:Gem::Dependency
42
- name: optimist
42
+ name: kubeclient
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '4.7'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '4.7'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: more_core_extensions
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +67,21 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rubocop
70
+ name: optimist
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: manageiq-style
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - ">="
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: Simple Workflow Runner.
98
126
  email:
99
127
  executables:
@@ -102,6 +130,10 @@ extensions: []
102
130
  extra_rdoc_files: []
103
131
  files:
104
132
  - ".rspec"
133
+ - ".rubocop.yml"
134
+ - ".rubocop_cc.yml"
135
+ - ".rubocop_local.yml"
136
+ - ".yamllint"
105
137
  - CHANGELOG.md
106
138
  - Gemfile
107
139
  - README.md
@@ -118,6 +150,7 @@ files:
118
150
  - lib/floe/workflow/choice_rule.rb
119
151
  - lib/floe/workflow/choice_rule/boolean.rb
120
152
  - lib/floe/workflow/choice_rule/data.rb
153
+ - lib/floe/workflow/context.rb
121
154
  - lib/floe/workflow/path.rb
122
155
  - lib/floe/workflow/payload_template.rb
123
156
  - lib/floe/workflow/reference_path.rb