floe 0.0.1 → 0.1.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 +7 -0
 - data/.rspec +1 -0
 - data/CHANGELOG.md +11 -0
 - data/Gemfile +8 -1
 - data/README.md +30 -13
 - data/Rakefile +3 -0
 - data/examples/workflow.asl +89 -0
 - data/exe/floe +21 -0
 - data/floe.gemspec +34 -18
 - data/lib/floe/logging.rb +13 -0
 - data/lib/floe/null_logger.rb +13 -0
 - data/lib/floe/version.rb +3 -1
 - data/lib/floe/workflow/catcher.rb +17 -0
 - data/lib/floe/workflow/choice_rule/boolean.rb +19 -0
 - data/lib/floe/workflow/choice_rule/data.rb +94 -0
 - data/lib/floe/workflow/choice_rule.rb +41 -0
 - data/lib/floe/workflow/path.rb +34 -0
 - data/lib/floe/workflow/payload_template.rb +37 -0
 - data/lib/floe/workflow/reference_path.rb +44 -0
 - data/lib/floe/workflow/retrier.rb +22 -0
 - data/lib/floe/workflow/runner/docker.rb +45 -0
 - data/lib/floe/workflow/runner/kubernetes.rb +118 -0
 - data/lib/floe/workflow/runner/podman.rb +42 -0
 - data/lib/floe/workflow/runner.rb +33 -0
 - data/lib/floe/workflow/state.rb +76 -0
 - data/lib/floe/workflow/states/choice.rb +53 -0
 - data/lib/floe/workflow/states/fail.rb +37 -0
 - data/lib/floe/workflow/states/map.rb +13 -0
 - data/lib/floe/workflow/states/parallel.rb +13 -0
 - data/lib/floe/workflow/states/pass.rb +31 -0
 - data/lib/floe/workflow/states/succeed.rb +26 -0
 - data/lib/floe/workflow/states/task.rb +82 -0
 - data/lib/floe/workflow/states/wait.rb +25 -0
 - data/lib/floe/workflow.rb +82 -0
 - data/lib/floe.rb +41 -2
 - data/sig/floe.rbs/floe.rbs +4 -0
 - metadata +116 -37
 - data/.gitignore +0 -17
 - data/LICENSE.txt +0 -22
 
| 
         @@ -0,0 +1,118 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Runner
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Kubernetes < Floe::Workflow::Runner
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :namespace
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(*)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      require "awesome_spawn"
         
     | 
| 
      
 11 
     | 
    
         
            +
                      require "securerandom"
         
     | 
| 
      
 12 
     | 
    
         
            +
                      require "base64"
         
     | 
| 
      
 13 
     | 
    
         
            +
                      require "yaml"
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      @namespace = ENV.fetch("DOCKER_RUNNER_NAMESPACE", "default")
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                      super
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    def run!(resource, env = {}, secrets = {})
         
     | 
| 
      
 21 
     | 
    
         
            +
                      raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 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)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                      result = kubectl_run!(image, name, overrides)
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                      # Kubectl prints that the pod was deleted, strip this from the output
         
     | 
| 
      
 31 
     | 
    
         
            +
                      output = result.output.gsub(/pod \"#{name}\" deleted/, "")
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                      [result.exit_status, output]
         
     | 
| 
      
 34 
     | 
    
         
            +
                    ensure
         
     | 
| 
      
 35 
     | 
    
         
            +
                      delete_secret!(secret) if secret
         
     | 
| 
      
 36 
     | 
    
         
            +
                    end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    private
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                    def container_name(image)
         
     | 
| 
      
 41 
     | 
    
         
            +
                      image.match(%r{^(?<repository>.+\/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    def pod_name(image)
         
     | 
| 
      
 45 
     | 
    
         
            +
                      container_short_name = container_name(image)
         
     | 
| 
      
 46 
     | 
    
         
            +
                      raise ArgumentError, "Invalid docker image [#{image}]" if container_short_name.nil?
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                      "#{container_short_name}-#{SecureRandom.uuid}"
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 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} }
         
     | 
| 
      
 56 
     | 
    
         
            +
                      }
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                      spec = {"spec" => {"containers" => [container_spec]}}
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      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"] = [
         
     | 
| 
      
 64 
     | 
    
         
            +
                          {
         
     | 
| 
      
 65 
     | 
    
         
            +
                            "name"      => "secret-volume",
         
     | 
| 
      
 66 
     | 
    
         
            +
                            "mountPath" => "/run/secrets/#{secret}",
         
     | 
| 
      
 67 
     | 
    
         
            +
                            "readOnly"  => true
         
     | 
| 
      
 68 
     | 
    
         
            +
                          }
         
     | 
| 
      
 69 
     | 
    
         
            +
                        ]
         
     | 
| 
      
 70 
     | 
    
         
            +
                      end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      spec
         
     | 
| 
      
 73 
     | 
    
         
            +
                    end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                    def create_secret!(secrets)
         
     | 
| 
      
 76 
     | 
    
         
            +
                      secret_name = SecureRandom.uuid
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                      secret_yaml = {
         
     | 
| 
      
 79 
     | 
    
         
            +
                        "kind"       => "Secret",
         
     | 
| 
      
 80 
     | 
    
         
            +
                        "apiVersion" => "v1",
         
     | 
| 
      
 81 
     | 
    
         
            +
                        "metadata"   => {
         
     | 
| 
      
 82 
     | 
    
         
            +
                          "name" => secret_name,
         
     | 
| 
      
 83 
     | 
    
         
            +
                          "namespace" => namespace
         
     | 
| 
      
 84 
     | 
    
         
            +
                        },
         
     | 
| 
      
 85 
     | 
    
         
            +
                        "data"       => {
         
     | 
| 
      
 86 
     | 
    
         
            +
                          "secret" => Base64.urlsafe_encode64(secrets.to_json)
         
     | 
| 
      
 87 
     | 
    
         
            +
                        },
         
     | 
| 
      
 88 
     | 
    
         
            +
                        "type"       => "Opaque"
         
     | 
| 
      
 89 
     | 
    
         
            +
                      }.to_yaml
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                      kubectl!("create", "-f", "-", :in_data => secret_yaml)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                      secret_name
         
     | 
| 
      
 94 
     | 
    
         
            +
                    end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                    def delete_secret!(secret_name)
         
     | 
| 
      
 97 
     | 
    
         
            +
                      kubectl!("delete", "secret", secret_name, [:namespace, namespace])
         
     | 
| 
      
 98 
     | 
    
         
            +
                    end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                    def kubectl!(*params, **kwargs)
         
     | 
| 
      
 101 
     | 
    
         
            +
                      AwesomeSpawn.run!("kubectl", :params => params, **kwargs)
         
     | 
| 
      
 102 
     | 
    
         
            +
                    end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
                    def kubectl_run!(image, name, overrides = nil)
         
     | 
| 
      
 105 
     | 
    
         
            +
                      params = [
         
     | 
| 
      
 106 
     | 
    
         
            +
                        "run", :rm, :attach, [:image, image], [:restart, "Never"], [:namespace, namespace], name
         
     | 
| 
      
 107 
     | 
    
         
            +
                      ]
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                      params << "--overrides=#{overrides.to_json}" if overrides
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                      logger.debug("Running kubectl: #{AwesomeSpawn.build_command_line("kubectl", params)}")
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                      kubectl!(*params)
         
     | 
| 
      
 114 
     | 
    
         
            +
                    end
         
     | 
| 
      
 115 
     | 
    
         
            +
                  end
         
     | 
| 
      
 116 
     | 
    
         
            +
                end
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,42 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Runner
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Podman < Floe::Workflow::Runner
         
     | 
| 
      
 7 
     | 
    
         
            +
                    def initialize(*)
         
     | 
| 
      
 8 
     | 
    
         
            +
                      require "awesome_spawn"
         
     | 
| 
      
 9 
     | 
    
         
            +
                      require "securerandom"
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                      super
         
     | 
| 
      
 12 
     | 
    
         
            +
                    end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    def run!(resource, env = {}, secrets = {})
         
     | 
| 
      
 15 
     | 
    
         
            +
                      raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                      image = resource.sub("docker://", "")
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                      params = ["run", :rm]
         
     | 
| 
      
 20 
     | 
    
         
            +
                      params += env.map { |k, v| [:e, "#{k}=#{v}"] } if env
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                      if secrets && !secrets.empty?
         
     | 
| 
      
 23 
     | 
    
         
            +
                        secret_guid = SecureRandom.uuid
         
     | 
| 
      
 24 
     | 
    
         
            +
                        AwesomeSpawn.run!("podman", :params => ["secret", "create", secret_guid, "-"], :in_data => secrets.to_json)
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                        params << [:e, "SECRETS=/run/secrets/#{secret_guid}"]
         
     | 
| 
      
 27 
     | 
    
         
            +
                        params << [:secret, secret_guid]
         
     | 
| 
      
 28 
     | 
    
         
            +
                      end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                      params << image
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                      logger.debug("Running podman: #{AwesomeSpawn.build_command_line("podman", params)}")
         
     | 
| 
      
 33 
     | 
    
         
            +
                      result = AwesomeSpawn.run!("podman", :params => params)
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                      [result.exit_status, result.output]
         
     | 
| 
      
 36 
     | 
    
         
            +
                    ensure
         
     | 
| 
      
 37 
     | 
    
         
            +
                      AwesomeSpawn.run("podman", :params => ["secret", "rm", secret_guid]) if secret_guid
         
     | 
| 
      
 38 
     | 
    
         
            +
                    end
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,33 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Runner
         
     | 
| 
      
 6 
     | 
    
         
            +
                  include Logging
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 9 
     | 
    
         
            +
                    attr_writer :docker_runner_klass
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def docker_runner_klass
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @docker_runner_klass ||= const_get(ENV.fetch("DOCKER_RUNNER", "docker").capitalize)
         
     | 
| 
      
 13 
     | 
    
         
            +
                    end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    def for_resource(resource)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      raise ArgumentError, "resource cannot be nil" if resource.nil?
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                      scheme = resource.split("://").first
         
     | 
| 
      
 19 
     | 
    
         
            +
                      case scheme
         
     | 
| 
      
 20 
     | 
    
         
            +
                      when "docker"
         
     | 
| 
      
 21 
     | 
    
         
            +
                        docker_runner_klass.new
         
     | 
| 
      
 22 
     | 
    
         
            +
                      else
         
     | 
| 
      
 23 
     | 
    
         
            +
                        raise "Invalid resource scheme [#{scheme}]"
         
     | 
| 
      
 24 
     | 
    
         
            +
                      end
         
     | 
| 
      
 25 
     | 
    
         
            +
                    end
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  def run!(image, env = {}, secrets = {})
         
     | 
| 
      
 29 
     | 
    
         
            +
                    raise NotImplementedError, "Must be implemented in a subclass"
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,76 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                class State
         
     | 
| 
      
 6 
     | 
    
         
            +
                  include Logging
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 9 
     | 
    
         
            +
                    def build!(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      state_type = payload["Type"]
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 13 
     | 
    
         
            +
                        klass = Floe::Workflow::States.const_get(state_type)
         
     | 
| 
      
 14 
     | 
    
         
            +
                      rescue NameError
         
     | 
| 
      
 15 
     | 
    
         
            +
                        raise Floe::InvalidWorkflowError, "Invalid state type: [#{state_type}]"
         
     | 
| 
      
 16 
     | 
    
         
            +
                      end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                      klass.new(workflow, name, payload)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  attr_reader :workflow, :comment, :name, :type, :payload
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def initialize(workflow, name, payload)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    @workflow = workflow
         
     | 
| 
      
 26 
     | 
    
         
            +
                    @name     = name
         
     | 
| 
      
 27 
     | 
    
         
            +
                    @payload  = payload
         
     | 
| 
      
 28 
     | 
    
         
            +
                    @end      = !!payload["End"]
         
     | 
| 
      
 29 
     | 
    
         
            +
                    @type     = payload["Type"]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @comment  = payload["Comment"]
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def end?
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @end
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def context
         
     | 
| 
      
 38 
     | 
    
         
            +
                    workflow.context
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def run!(input)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    logger.info("Running state: [#{name}] with input [#{input}]")
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    input = input_path.value(context, input)
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                    output, next_state = block_given? ? yield(input) : input
         
     | 
| 
      
 47 
     | 
    
         
            +
                    next_state ||= workflow.states_by_name[payload["Next"]] unless end?
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    output ||= input
         
     | 
| 
      
 50 
     | 
    
         
            +
                    output   = output_path&.value(context, output)
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                    logger.info("Running state: [#{name}] with input [#{input}]...Complete - next state: [#{next_state&.name}] output: [#{output}]")
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    [next_state, output]
         
     | 
| 
      
 55 
     | 
    
         
            +
                  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 
     | 
    
         
            +
                end
         
     | 
| 
      
 75 
     | 
    
         
            +
              end
         
     | 
| 
      
 76 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Choice < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :choices, :default, :input_path, :output_path
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      super
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      @choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @default = payload["Default"]
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      @input_path  = Path.new(payload.fetch("InputPath", "$"))
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @output_path = Path.new(payload.fetch("OutputPath", "$"))
         
     | 
| 
      
 17 
     | 
    
         
            +
                    end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    def run!(*)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      super do |input|
         
     | 
| 
      
 21 
     | 
    
         
            +
                        next_state_name = choices.detect { |choice| choice.true?(context, input) }&.next || default
         
     | 
| 
      
 22 
     | 
    
         
            +
                        next_state      = workflow.states_by_name[next_state_name]
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                        output = input
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                        [output, next_state]
         
     | 
| 
      
 27 
     | 
    
         
            +
                      end
         
     | 
| 
      
 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 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,37 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Fail < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :cause, :error
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      super
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      @cause = payload["Cause"]
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @error = payload["Error"]
         
     | 
| 
      
 14 
     | 
    
         
            +
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    def run!(input)
         
     | 
| 
      
 17 
     | 
    
         
            +
                      logger.info("Running state: [#{name}] with input [#{input}]")
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                      next_state = nil
         
     | 
| 
      
 20 
     | 
    
         
            +
                      output     = input
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                      logger.info("Running state: [#{name}] with input [#{input}]...Complete - next state: [#{next_state&.name}]")
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                      [next_state, output]
         
     | 
| 
      
 25 
     | 
    
         
            +
                    end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                    def end?
         
     | 
| 
      
 28 
     | 
    
         
            +
                      true
         
     | 
| 
      
 29 
     | 
    
         
            +
                    end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                    private def to_dot_attributes
         
     | 
| 
      
 32 
     | 
    
         
            +
                      super.merge(:color => "red")
         
     | 
| 
      
 33 
     | 
    
         
            +
                    end
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
                end
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
      
 37 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,31 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Pass < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :end, :next, :result, :parameters, :input_path, :output_path, :result_path
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      super
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      @next        = payload["Next"]
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @result      = payload["Result"]
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      @parameters  = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"]
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @input_path  = Path.new(payload.fetch("InputPath", "$"))
         
     | 
| 
      
 17 
     | 
    
         
            +
                      @output_path = Path.new(payload.fetch("OutputPath", "$"))
         
     | 
| 
      
 18 
     | 
    
         
            +
                      @result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    def run!(*)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      super do |input|
         
     | 
| 
      
 23 
     | 
    
         
            +
                        output = input
         
     | 
| 
      
 24 
     | 
    
         
            +
                        output = result_path.set(output, result) if result && result_path
         
     | 
| 
      
 25 
     | 
    
         
            +
                        output
         
     | 
| 
      
 26 
     | 
    
         
            +
                      end
         
     | 
| 
      
 27 
     | 
    
         
            +
                    end
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Succeed < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :input_path, :output_path
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      super
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      @input_path  = Path.new(payload.fetch("InputPath", "$"))
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @output_path = Path.new(payload.fetch("OutputPath", "$"))
         
     | 
| 
      
 14 
     | 
    
         
            +
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    def end?
         
     | 
| 
      
 17 
     | 
    
         
            +
                      true # TODO: Handle if this is ending a parallel or map state
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    private def to_dot_attributes
         
     | 
| 
      
 21 
     | 
    
         
            +
                      super.merge(:color => "green")
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Task < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :credentials, :end, :heartbeat_seconds, :next, :parameters,
         
     | 
| 
      
 8 
     | 
    
         
            +
                                :result_selector, :resource, :timeout_seconds, :retry, :catch,
         
     | 
| 
      
 9 
     | 
    
         
            +
                                :input_path, :output_path, :result_path
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      super
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                      @heartbeat_seconds = payload["HeartbeatSeconds"]
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @next              = payload["Next"]
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @resource          = payload["Resource"]
         
     | 
| 
      
 17 
     | 
    
         
            +
                      @timeout_seconds   = payload["TimeoutSeconds"]
         
     | 
| 
      
 18 
     | 
    
         
            +
                      @retry             = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @catch             = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
         
     | 
| 
      
 20 
     | 
    
         
            +
                      @input_path        = Path.new(payload.fetch("InputPath", "$"))
         
     | 
| 
      
 21 
     | 
    
         
            +
                      @output_path       = Path.new(payload.fetch("OutputPath", "$"))
         
     | 
| 
      
 22 
     | 
    
         
            +
                      @result_path       = ReferencePath.new(payload.fetch("ResultPath", "$"))
         
     | 
| 
      
 23 
     | 
    
         
            +
                      @parameters        = PayloadTemplate.new(payload["Parameters"])     if payload["Parameters"]
         
     | 
| 
      
 24 
     | 
    
         
            +
                      @result_selector   = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
         
     | 
| 
      
 25 
     | 
    
         
            +
                      @credentials       = PayloadTemplate.new(payload["Credentials"])    if payload["Credentials"]
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    def run!(*)
         
     | 
| 
      
 29 
     | 
    
         
            +
                      super do |input|
         
     | 
| 
      
 30 
     | 
    
         
            +
                        input = parameters.value(context, input) if parameters
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                        runner = Floe::Workflow::Runner.for_resource(resource)
         
     | 
| 
      
 33 
     | 
    
         
            +
                        _exit_status, results = runner.run!(resource, input, credentials&.value({}, workflow.credentials))
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                        output = input
         
     | 
| 
      
 36 
     | 
    
         
            +
                        process_output!(output, results)
         
     | 
| 
      
 37 
     | 
    
         
            +
                      rescue => err
         
     | 
| 
      
 38 
     | 
    
         
            +
                        retrier = self.retry.detect { |r| (r.error_equals & [err.to_s, "States.ALL"]).any? }
         
     | 
| 
      
 39 
     | 
    
         
            +
                        retry if retry!(retrier)
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                        catcher = self.catch.detect { |c| (c.error_equals & [err.to_s, "States.ALL"]).any? }
         
     | 
| 
      
 42 
     | 
    
         
            +
                        raise if catcher.nil?
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                        [output, workflow.states_by_name[catcher.next]]
         
     | 
| 
      
 45 
     | 
    
         
            +
                      end
         
     | 
| 
      
 46 
     | 
    
         
            +
                    end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    private
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    def retry!(retrier)
         
     | 
| 
      
 51 
     | 
    
         
            +
                      return if retrier.nil?
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 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}
         
     | 
| 
      
 56 
     | 
    
         
            +
                      end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                      context["retrier"]["retry_count"] += 1
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                      return if context["retrier"]["retry_count"] > retrier.max_attempts
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                      Kernel.sleep(retrier.sleep_duration(context["retrier"]["retry_count"]))
         
     | 
| 
      
 63 
     | 
    
         
            +
                      true
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    def process_output!(output, results)
         
     | 
| 
      
 67 
     | 
    
         
            +
                      return output if results.nil?
         
     | 
| 
      
 68 
     | 
    
         
            +
                      return if output_path.nil?
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 71 
     | 
    
         
            +
                        results = JSON.parse(results)
         
     | 
| 
      
 72 
     | 
    
         
            +
                      rescue JSON::ParserError
         
     | 
| 
      
 73 
     | 
    
         
            +
                        results = {"results" => results}
         
     | 
| 
      
 74 
     | 
    
         
            +
                      end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                      results = result_selector.value(context, results) if result_selector
         
     | 
| 
      
 77 
     | 
    
         
            +
                      result_path.set(output, results)
         
     | 
| 
      
 78 
     | 
    
         
            +
                    end
         
     | 
| 
      
 79 
     | 
    
         
            +
                  end
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 5 
     | 
    
         
            +
                module States
         
     | 
| 
      
 6 
     | 
    
         
            +
                  class Wait < Floe::Workflow::State
         
     | 
| 
      
 7 
     | 
    
         
            +
                    attr_reader :end, :next, :seconds, :input_path, :output_path
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    def initialize(workflow, name, payload)
         
     | 
| 
      
 10 
     | 
    
         
            +
                      super
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                      @next    = payload["Next"]
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @seconds = payload["Seconds"].to_i
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                      @input_path  = Path.new(payload.fetch("InputPath", "$"), context)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      @output_path = Path.new(payload.fetch("OutputPath", "$"), context)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                    def run!(*)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      super { sleep(seconds); nil }
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "json"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Floe
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Workflow
         
     | 
| 
      
 7 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                  def load(path_or_io, context = {}, credentials = {})
         
     | 
| 
      
 9 
     | 
    
         
            +
                    payload = path_or_io.respond_to?(:read) ? path_or_io.read : File.read(path_or_io)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    new(payload, context, credentials)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                attr_reader :context, :credentials, :first_state, :payload, :states, :states_by_name, :start_at
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def initialize(payload, context = {}, credentials = {})
         
     | 
| 
      
 17 
     | 
    
         
            +
                  payload     = JSON.parse(payload)     if payload.kind_of?(String)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  context     = JSON.parse(context)     if context.kind_of?(String)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  credentials = JSON.parse(credentials) if credentials.kind_of?(String)
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  @payload        = payload
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @context        = context
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @credentials    = credentials
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @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]
         
     | 
| 
      
 28 
     | 
    
         
            +
                rescue JSON::ParserError => err
         
     | 
| 
      
 29 
     | 
    
         
            +
                  raise Floe::InvalidWorkflowError, err.message
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def run!
         
     | 
| 
      
 33 
     | 
    
         
            +
                  state = first_state
         
     | 
| 
      
 34 
     | 
    
         
            +
                  input = context.dup
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  until state.nil?
         
     | 
| 
      
 37 
     | 
    
         
            +
                    state, output = state.run!(input)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    input = output
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  output
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 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
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                def to_svg(path: nil)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  require "open3"
         
     | 
| 
      
 62 
     | 
    
         
            +
                  out, err, _status = Open3.capture3("dot -Tsvg", :stdin_data => to_dot)
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  raise "Error from graphviz:\n#{err}" if err && !err.empty?
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                  File.write(path, out) if path
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  out
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 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
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                  out
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/floe.rb
    CHANGED
    
    | 
         @@ -1,5 +1,44 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "floe/version"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative "floe/null_logger"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "floe/logging"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            require_relative "floe/workflow"
         
     | 
| 
      
 9 
     | 
    
         
            +
            require_relative "floe/workflow/catcher"
         
     | 
| 
      
 10 
     | 
    
         
            +
            require_relative "floe/workflow/choice_rule"
         
     | 
| 
      
 11 
     | 
    
         
            +
            require_relative "floe/workflow/choice_rule/boolean"
         
     | 
| 
      
 12 
     | 
    
         
            +
            require_relative "floe/workflow/choice_rule/data"
         
     | 
| 
      
 13 
     | 
    
         
            +
            require_relative "floe/workflow/path"
         
     | 
| 
      
 14 
     | 
    
         
            +
            require_relative "floe/workflow/payload_template"
         
     | 
| 
      
 15 
     | 
    
         
            +
            require_relative "floe/workflow/reference_path"
         
     | 
| 
      
 16 
     | 
    
         
            +
            require_relative "floe/workflow/retrier"
         
     | 
| 
      
 17 
     | 
    
         
            +
            require_relative "floe/workflow/runner"
         
     | 
| 
      
 18 
     | 
    
         
            +
            require_relative "floe/workflow/runner/docker"
         
     | 
| 
      
 19 
     | 
    
         
            +
            require_relative "floe/workflow/runner/kubernetes"
         
     | 
| 
      
 20 
     | 
    
         
            +
            require_relative "floe/workflow/runner/podman"
         
     | 
| 
      
 21 
     | 
    
         
            +
            require_relative "floe/workflow/state"
         
     | 
| 
      
 22 
     | 
    
         
            +
            require_relative "floe/workflow/states/choice"
         
     | 
| 
      
 23 
     | 
    
         
            +
            require_relative "floe/workflow/states/fail"
         
     | 
| 
      
 24 
     | 
    
         
            +
            require_relative "floe/workflow/states/map"
         
     | 
| 
      
 25 
     | 
    
         
            +
            require_relative "floe/workflow/states/parallel"
         
     | 
| 
      
 26 
     | 
    
         
            +
            require_relative "floe/workflow/states/pass"
         
     | 
| 
      
 27 
     | 
    
         
            +
            require_relative "floe/workflow/states/succeed"
         
     | 
| 
      
 28 
     | 
    
         
            +
            require_relative "floe/workflow/states/task"
         
     | 
| 
      
 29 
     | 
    
         
            +
            require_relative "floe/workflow/states/wait"
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            require "jsonpath"
         
     | 
| 
       2 
32 
     | 
    
         | 
| 
       3 
33 
     | 
    
         
             
            module Floe
         
     | 
| 
       4 
     | 
    
         
            -
               
     | 
| 
      
 34 
     | 
    
         
            +
              class Error < StandardError; end
         
     | 
| 
      
 35 
     | 
    
         
            +
              class InvalidWorkflowError < Error; end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              def self.logger
         
     | 
| 
      
 38 
     | 
    
         
            +
                @logger ||= NullLogger.new
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
              def self.logger=(logger)
         
     | 
| 
      
 42 
     | 
    
         
            +
                @logger = logger
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
       5 
44 
     | 
    
         
             
            end
         
     |