Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a State to set a value in Credentials for subsequent states #145

Merged
merged 4 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added
- Implement `ReferencePath#get` ([#144](https://github.com/ManageIQ/floe/pull/144))
- Allow a State to set a value in Credentials for subsequent states ([#145](https://github.com/ManageIQ/floe/pull/145))

## [0.6.1] - 2023-11-21
### Fixed
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ You can provide that at runtime via the `--credentials` parameter:
bundle exec ruby exe/floe --workflow my-workflow.asl --credentials='{"roleArn": "arn:aws:iam::111122223333:role/LambdaRole"}'
```

If you need to set a credential at runtime you can do that by using the `"ResultPath": "$.Credentials"` directive, for example to user a username/password to login and get a Bearer token:

```
bundle exec ruby exe/floe --workflow my-workflow.asl --credentials='{"username": "user", "password": "pass"}'
```

```json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use asl here

Suggested change
```json
asl

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get it, but I actually think the json markup actually renders nicer

{
  "StartAt": "Login",
  "States": {
    "Login": {
      "Type": "Task",
      "Resource": "docker://login:latest",
      "Credentials": {
        "username.$": "$.username",
        "password.$": "$.password"
      },
      "ResultPath": "$.Credentials",
      "Next": "DoSomething"
    },
    "DoSomething": {
      "Type": "Task",
      "Resource": "docker://do-something:latest",
      "Credentials": {
        "token.$": "$.bearer_token"
      },
      "End": true
    }
  }
}
{
  "StartAt": "Login",
  "States": {
    "Login": {
      "Type": "Task",
      "Resource": "docker://login:latest",
      "Credentials": {
        "username.$": "$.username",
        "password.$": "$.password"
      },
      "ResultPath": "$.Credentials",
      "Next": "DoSomething"
    },
    "DoSomething": {
      "Type": "Task",
      "Resource": "docker://do-something:latest",
      "Credentials": {
        "token.$": "$.bearer_token"
      },
      "End": true
    }
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asl doesn't differentiate between keys and values, and doesn't even markup the boolean

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fryguy do you still want to do the asl tag even though I think it is objectively worse?

{
"StartAt": "Login",
"States": {
"Login": {
"Type": "Task",
"Resource": "docker://login:latest",
"Credentials": {
"username.$": "$.username",
"password.$": "$.password"
},
"ResultPath": "$.Credentials",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to state where in the credentials hash this will go?

Copy link
Member Author

@agrare agrare Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary since the ResultSelector has "bearer_token.$": "$.echo" which means that bearer_token will be set to Credentials.

If I did "ResultPath": "$Credentials.bearer_token" this would end up looking like "Credentials": {"bearer_token": {"bearer_token": "abcd"}}

(and if I didn't have the ResultSelector it would look like "Credentials": {"bearer_token": {"echo": "abcd"}})

"Next": "DoSomething"
},
"DoSomething": {
"Type": "Task",
"Resource": "docker://do-something:latest",
"Credentials": {
"token.$": "$.bearer_token"
},
"End": true
}
}
}
```

### Ruby Library

```ruby
Expand Down
26 changes: 26 additions & 0 deletions examples/set-credential.asl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"Comment": "An example showing how to set a credential.",
"StartAt": "Login",
"States": {
"Login": {
"Type": "Task",
"Resource": "docker://docker.io/agrare/echo:latest",
"Parameters": {
"ECHO": "TOKEN"
},
"ResultPath": "$.Credentials",
"ResultSelector": {
"bearer_token.$": "$.echo"
},
"Next": "DoSomething"
},
"DoSomething": {
"Type": "Task",
"Resource": "docker://docker.io/agrare/hello-world:latest",
"Credentials": {
"bearer_token.$": "$.bearer_token"
},
"End": true
}
}
}
1 change: 1 addition & 0 deletions lib/floe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
require_relative "floe/workflow/state"
require_relative "floe/workflow/states/choice"
require_relative "floe/workflow/states/fail"
require_relative "floe/workflow/states/input_output_mixin"
require_relative "floe/workflow/states/map"
require_relative "floe/workflow/states/non_terminal_mixin"
require_relative "floe/workflow/states/parallel"
Expand Down
6 changes: 2 additions & 4 deletions lib/floe/workflow/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ def value(payload, context, input = {})
end
end

attr_reader :payload

def initialize(payload)
@payload = payload

Expand All @@ -28,10 +30,6 @@ def value(context, input = {})

results.count < 2 ? results.first : results
end

private

attr_reader :payload
end
end
end
31 changes: 31 additions & 0 deletions lib/floe/workflow/states/input_output_mixin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Floe
class Workflow
module States
module InputOutputMixin
def process_input(input)
input = input_path.value(context, input)
input = parameters.value(context, input) if parameters
input
end

def process_output(input, results)
return input if results.nil?
return if output_path.nil?

results = result_selector.value(context, results) if @result_selector
if result_path.payload.start_with?("$.Credentials")
credentials = result_path.set(workflow.credentials, results)["Credentials"]
workflow.credentials.merge!(credentials)
output = input
else
output = result_path.set(input, results)
end

output_path.value(context, output)
end
end
end
end
end
8 changes: 4 additions & 4 deletions lib/floe/workflow/states/pass.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Floe
class Workflow
module States
class Pass < Floe::Workflow::State
include InputOutputMixin
include NonTerminalMixin

attr_reader :end, :next, :result, :parameters, :input_path, :output_path, :result_path
Expand All @@ -25,12 +26,11 @@ def initialize(workflow, name, payload)

def start(input)
super
output = input_path.value(context, input)
output = result_path.set(output, result) if result && result_path
output = output_path.value(context, output)

input = process_input(input)

context.output = process_output(input, result)
context.next_state = end? ? nil : @next
context.output = output
end

def running?
Expand Down
19 changes: 2 additions & 17 deletions lib/floe/workflow/states/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Floe
class Workflow
module States
class Task < Floe::Workflow::State
include InputOutputMixin
include NonTerminalMixin

attr_reader :credentials, :end, :heartbeat_seconds, :next, :parameters,
Expand Down Expand Up @@ -49,7 +50,7 @@ def finish

if success?
output = parse_output(output)
context.state["Output"] = process_output!(output)
context.state["Output"] = process_output(context.input.dup, output)
context.next_state = next_state
else
error = parse_error(output)
Expand Down Expand Up @@ -126,12 +127,6 @@ def fail_workflow!(error)
context.state["Error"] = context.output["Error"]
end

def process_input(input)
input = input_path.value(context, input)
input = parameters.value(context, input) if parameters
input
end

def parse_error(output)
return if output.nil?
return output if output.kind_of?(Hash)
Expand All @@ -150,16 +145,6 @@ def parse_output(output)
nil
end

def process_output!(results)
output = context.input.dup
return output if results.nil?
return if output_path.nil?

results = result_selector.value(context, results) if result_selector
output = result_path.set(output, results)
output_path.value(context, output)
end

def next_state
end? ? nil : @next
end
Expand Down
50 changes: 36 additions & 14 deletions spec/workflow/states/pass_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@
let(:input) { {} }
let(:ctx) { Floe::Workflow::Context.new(:input => input) }
let(:state) { workflow.current_state }
let(:workflow) do
make_workflow(
ctx, {
"PassState" => {
"Type" => "Pass",
"Result" => {
"foo" => "bar",
"bar" => "baz"
},
"ResultPath" => "$.result",
"Next" => "SuccessState"
let(:workflow) { make_workflow(ctx, payload) }
let(:payload) do
{
"PassState" => {
"Type" => "Pass",
"Result" => {
"foo" => "bar",
"bar" => "baz"
},
"SuccessState" => {"Type" => "Succeed"}
}
)
"ResultPath" => "$.result",
"Next" => "SuccessState"
},
"SuccessState" => {"Type" => "Succeed"}
}
end

describe "#end?" do
Expand All @@ -31,5 +30,28 @@
expect(ctx.output["result"]).to include({"foo" => "bar", "bar" => "baz"})
expect(ctx.next_state).to eq("SuccessState")
end

context "with a ResultPath setting a Credential" do
let(:payload) do
{
"PassState" => {
"Type" => "Pass",
"Result" => {
"user" => "luggage",
"password" => "1234"
},
"ResultPath" => "$.Credentials",
"Next" => "SuccessState"
},
"SuccessState" => {"Type" => "Succeed"}
}
end

it "sets the result in Credentials" do
state.run_nonblock!
expect(workflow.credentials).to include({"user" => "luggage", "password" => "1234"})
expect(ctx.next_state).to eq("SuccessState")
end
end
end
end
16 changes: 16 additions & 0 deletions spec/workflow/states/task_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@
"ip_addrs" => ["192.168.1.2"]
)
end

context "setting a Credential" do
let(:workflow) { make_workflow(ctx, {"State" => {"Type" => "Task", "Resource" => resource, "ResultPath" => "$.Credentials", "End" => true}}) }

it "inserts the response into the workflow credentials" do
expect_run_async(input, :output => "{\"token\": \"shhh!\"}")

workflow.current_state.run_nonblock!

expect(workflow.credentials).to include("token" => "shhh!")
expect(ctx.output).to eq(
"foo" => {"bar" => "baz"},
"bar" => {"baz" => "foo"}
)
end
end
end

context "OutputPath" do
Expand Down