floe 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![CI](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml/badge.svg)](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/ManageIQ/floe.svg)](https://codeclimate.com/github/ManageIQ/floe)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/ManageIQ/floe/badges/coverage.svg)](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
|