floe 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +4 -0
- data/.rubocop_local.yml +2 -0
- data/.yamllint +8 -0
- data/CHANGELOG.md +16 -1
- data/README.md +37 -1
- data/Rakefile +4 -1
- data/examples/workflow.asl +6 -0
- data/exe/floe +26 -8
- data/floe.gemspec +4 -2
- data/lib/floe/null_logger.rb +1 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/choice_rule/data.rb +8 -9
- data/lib/floe/workflow/context.rb +57 -0
- data/lib/floe/workflow/reference_path.rb +2 -2
- data/lib/floe/workflow/runner/kubernetes.rb +147 -48
- data/lib/floe/workflow/runner.rb +9 -4
- data/lib/floe/workflow/state.rb +4 -18
- data/lib/floe/workflow/states/choice.rb +0 -21
- data/lib/floe/workflow/states/fail.rb +2 -2
- data/lib/floe/workflow/states/succeed.rb +0 -4
- data/lib/floe/workflow/states/task.rb +6 -5
- data/lib/floe/workflow/states/wait.rb +6 -3
- data/lib/floe/workflow.rb +43 -47
- data/lib/floe.rb +1 -0
- metadata +39 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a8984568027905b79c95ab066b45f0e87872bbb290022a018f3574d47e3054e
|
4
|
+
data.tar.gz: 73390d044e75811f2b1c91acfa1aea1814b38f1d3a60ad4b9bd0eed88d131af0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5d6cec81ef1adc60cc5912cc34275c032f250d84361982ac556284ada3c807f3189eef5357c193ae5b76db795f328b761fd53fd5dbfcd3468e5fc439d54c9fe
|
7
|
+
data.tar.gz: d4a9ce5beea167b41ec66b1b049c6466a41c91b64076792e87b34e92787e19120965580c638a755c5a1e406398f4b1842851105dad2d388c21b7db76944c7df7
|
data/.rubocop.yml
ADDED
data/.rubocop_cc.yml
ADDED
data/.rubocop_local.yml
ADDED
data/.yamllint
ADDED
data/CHANGELOG.md
CHANGED
@@ -4,8 +4,23 @@ 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
|
+
|
18
|
+
## [0.1.1] - 2023-06-05
|
19
|
+
### Fixed
|
20
|
+
- Fix States::Wait Path initializer arguments (#47)
|
21
|
+
|
7
22
|
## [0.1.0] - 2023-03-13
|
8
23
|
### Added
|
9
24
|
- Initial release
|
10
25
|
|
11
|
-
[Unreleased]: https://github.com/ManageIQ/
|
26
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.1.0...HEAD
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Floe
|
2
2
|
|
3
3
|
[](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml)
|
4
|
+
[](https://codeclimate.com/github/ManageIQ/floe)
|
5
|
+
[](https://codeclimate.com/github/ManageIQ/floe/coverage)
|
4
6
|
|
5
7
|
## Overview
|
6
8
|
|
@@ -26,15 +28,49 @@ Floe can be run as a command-line utility or as a ruby class.
|
|
26
28
|
bundle exec ruby exe/floe --workflow examples/workflow.asl --inputs='{"foo": 1}'
|
27
29
|
```
|
28
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
|
+
|
29
39
|
### Ruby Library
|
30
40
|
|
31
41
|
```ruby
|
32
42
|
require 'floe'
|
33
43
|
|
34
|
-
workflow = Floe::Workflow.load(File.read("workflow.
|
44
|
+
workflow = Floe::Workflow.load(File.read("workflow.asl"))
|
35
45
|
workflow.run!
|
36
46
|
```
|
37
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
|
+
|
38
74
|
## Development
|
39
75
|
|
40
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
data/examples/workflow.asl
CHANGED
data/exe/floe
CHANGED
@@ -5,17 +5,35 @@ require "floe"
|
|
5
5
|
require "optimist"
|
6
6
|
|
7
7
|
opts = Optimist.options do
|
8
|
-
version
|
9
|
-
opt :workflow, "Path to your workflow json",
|
10
|
-
opt :
|
11
|
-
opt :credentials, "JSON payload with credentials",
|
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(
|
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
|
-
|
35
|
+
Floe::Workflow::Runner.docker_runner = runner_klass.new(runner_options)
|
18
36
|
|
19
|
-
|
37
|
+
workflow.run!
|
20
38
|
|
21
|
-
puts
|
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 "
|
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 "
|
38
|
+
spec.add_development_dependency "manageiq-style"
|
38
39
|
spec.add_development_dependency "rspec"
|
40
|
+
spec.add_development_dependency "rubocop"
|
39
41
|
end
|
data/lib/floe/null_logger.rb
CHANGED
data/lib/floe/version.rb
CHANGED
@@ -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"
|
16
|
-
when "IsPresent"
|
17
|
-
when "IsNumeric"
|
18
|
-
when "IsString"
|
19
|
-
when "IsBoolean"
|
20
|
-
when "IsTimestamp"
|
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
|
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
|
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.
|
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
|
-
|
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
|
-
@
|
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
|
24
|
-
name
|
25
|
-
secret
|
26
|
-
|
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
|
-
|
57
|
+
[exit_status, results]
|
58
|
+
ensure
|
59
|
+
cleanup(name, secret)
|
60
|
+
end
|
61
|
+
end
|
29
62
|
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
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
|
-
|
53
|
-
"
|
54
|
-
|
55
|
-
|
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[
|
62
|
-
container_spec["env"] << {"name" => "SECRETS", "value" => "/run/secrets/#{secret}/secret"}
|
63
|
-
container_spec["volumeMounts"] = [
|
120
|
+
spec[:spec][:volumes] = [
|
64
121
|
{
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
161
|
+
secret_config = {
|
162
|
+
:kind => "Secret",
|
163
|
+
:apiVersion => "v1",
|
164
|
+
:metadata => {
|
165
|
+
:name => secret_name,
|
166
|
+
:namespace => namespace
|
84
167
|
},
|
85
|
-
|
86
|
-
|
168
|
+
:data => {
|
169
|
+
:secret => Base64.urlsafe_encode64(secrets.to_json)
|
87
170
|
},
|
88
|
-
|
89
|
-
}
|
171
|
+
:type => "Opaque"
|
172
|
+
}
|
90
173
|
|
91
|
-
|
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
|
-
|
180
|
+
kubeclient.delete_secret(secret_name, namespace)
|
98
181
|
end
|
99
182
|
|
100
|
-
def
|
101
|
-
|
183
|
+
def delete_secret(name)
|
184
|
+
delete_secret!(name)
|
185
|
+
rescue
|
186
|
+
nil
|
102
187
|
end
|
103
188
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
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
|
-
|
206
|
+
@kubeclient = Kubeclient::Client.new(api_endpoint, "v1", :ssl_options => ssl_options, :auth_options => auth_options).tap(&:discover)
|
207
|
+
end
|
110
208
|
|
111
|
-
|
209
|
+
def kubeconfig
|
210
|
+
return if kubeconfig_file.nil? || !File.exist?(kubeconfig_file)
|
112
211
|
|
113
|
-
|
212
|
+
Kubeclient::Config.read(kubeconfig_file)
|
114
213
|
end
|
115
214
|
end
|
116
215
|
end
|
data/lib/floe/workflow/runner.rb
CHANGED
@@ -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 :
|
14
|
+
attr_writer :docker_runner
|
10
15
|
|
11
|
-
def
|
12
|
-
@
|
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
|
-
|
26
|
+
docker_runner
|
22
27
|
else
|
23
28
|
raise "Invalid resource scheme [#{scheme}]"
|
24
29
|
end
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -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
|
@@ -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?("
|
55
|
-
context["
|
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["
|
59
|
+
context["State"]["RetryCount"] += 1
|
59
60
|
|
60
|
-
return if context["
|
61
|
+
return if context["State"]["RetryCount"] > retrier.max_attempts
|
61
62
|
|
62
|
-
Kernel.sleep(retrier.sleep_duration(context["
|
63
|
+
Kernel.sleep(retrier.sleep_duration(context["State"]["RetryCount"]))
|
63
64
|
true
|
64
65
|
end
|
65
66
|
|
@@ -12,12 +12,15 @@ module Floe
|
|
12
12
|
@next = payload["Next"]
|
13
13
|
@seconds = payload["Seconds"].to_i
|
14
14
|
|
15
|
-
@input_path = Path.new(payload.fetch("InputPath", "$")
|
16
|
-
@output_path = Path.new(payload.fetch("OutputPath", "$")
|
15
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
16
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
17
17
|
end
|
18
18
|
|
19
19
|
def run!(*)
|
20
|
-
super
|
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 =
|
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, :
|
14
|
+
attr_reader :context, :credentials, :payload, :states, :states_by_name, :current_state, :status
|
15
15
|
|
16
|
-
def initialize(payload, context =
|
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.
|
26
|
-
|
27
|
-
|
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
|
33
|
-
|
34
|
-
|
38
|
+
def step
|
39
|
+
@status = "running" if @status == "pending"
|
40
|
+
@context["Execution"]["StartTime"] ||= Time.now.utc
|
35
41
|
|
36
|
-
|
37
|
-
state, output = state.run!(input)
|
38
|
-
input = output
|
39
|
-
end
|
42
|
+
input = @context["State"]["Output"] || @context["Execution"]["Input"].dup
|
40
43
|
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
61
|
-
require "open3"
|
62
|
-
out, err, _status = Open3.capture3("dot -Tsvg", :stdin_data => to_dot)
|
57
|
+
@context["States"] << @context["State"]
|
63
58
|
|
64
|
-
|
59
|
+
@status = current_state.status
|
65
60
|
|
66
|
-
|
61
|
+
next_state_name = next_state&.name
|
62
|
+
@current_state = next_state_name && @states_by_name[next_state_name]
|
67
63
|
|
68
|
-
|
64
|
+
self
|
69
65
|
end
|
70
66
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
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.
|
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-
|
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:
|
42
|
+
name: kubeclient
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
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: '
|
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:
|
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
|