braintree / runbook

A framework for gradual system automation
MIT License
729 stars 43 forks source link

Conditional steps and menu choices? #11

Open fabn opened 4 years ago

fabn commented 4 years ago

I really like the approach of this gem but I'm wondering if there's a way to enable conditional steps or there's a way to present a menu to the user to choice from.

I.e. something like (pseudo code)

optional_step 'Do you want to do this?' do
  # if user choose yes this step is executed
end

choices 'Choose your fruit', from: %w(banana apple) do |f|
  case 'banana'
    ask ...
    command ...
  case 'apple'
    # something else
end

Since you're already using tty-prompt gem you have building blocks for that, so it should be a nice addition to this gem.

fabn commented 4 years ago

Currently I'm trying to configure a k8s cluster with this gem and I want to make a component optional. What I did until now is the following (just a proof of concept) but it doesn't follow the runbook approach and it doesn't work with auto mode obviously

  section "Additional tools" do
    step 'Install External DNS Chart' do
      ruby_command do
        prompt = TTY::Prompt.new
        if prompt.yes?('Install External DNS Chart?')
          options = case prompt.select("Choose your DNS Provider?", %w(DigitalOcean Cloudflare))
                    when 'DigitalOcean'
                      prompt.collect do
                        key('digitalocean.apiToken').ask('DigitalOcean Api Key:', required: true)
                      end

                    when 'Cloudflare'
                      prompt.collect do
                        key(:cloudflare) do
                          key(:email).ask('Cloudflare Email:', validate: :email, required: true)
                          key(:apiKey).ask('Cloudflare Api Key:', required: true)
                        end
                      end
                    else
                      raise 'Unnkown provider given'
                    end
          puts "DNS options: #{options.inspect}"
          command "helm install stable/external-dns #{dns_options}" # To be completed
        end
      end
    end
  end

It would be awesome to fully expose TTY::Prompt api in runbook, I don't know if it's possible.

pblesi commented 4 years ago

Regarding your initial comment, this can be accomplished by extending the runbook DSL to include your own statements

Implementation for optional_step is essentially a mashup of the confirm and ruby_command statements. It would go something like this:

$ runbook generate statement optional_step --root lib/runbook/extensions

# lib/runbook/extensions/optional_step.rb
module Runbook::Statements
  class OptionalStep < Runbook::Statement
    attr_reader :prompt, :block

    def initialize(prompt, &block)
      @prompt = prompt
      @block = block
    end
  end
end

module RunbookOptionalStep
  module RunbookExtensions
    module OptionalStepMarkdown
      def runbook__statements__optional_step(object, output, metadata)
        # Format how your statement will be displayed when rendered with markdown
        output << "   if: #{object.prompt}\n"
        output << "   then run:\n"
        output << "   ```ruby\n"
        begin
          output << "#{deindent(object.block.source, padding: 3)}\n"
        rescue ::MethodSource::SourceNotFoundError => e
          output << "   Unable to retrieve source code\n"
        end
        output << "   ```\n\n"
      end
    end
    Runbook::Views::Markdown.singleton_class.prepend(OptionalStepMarkdown)

    module OptionalStepRun
      def runbook__statements__optional_step(object, metadata)
        auto = metadata[:auto]

        if metadata[:noop]
          metadata[:toolbox].output("[NOOP] Prompt: #{object.prompt}") unless auto
          metadata[:toolbox].output("[NOOP] #{auto ? "Run" : "If yes, run"} the following Ruby block:\n")
          begin
            source = deindent(object.block.source)
            metadata[:toolbox].output("```ruby\n#{source}\n```\n")
          rescue ::MethodSource::SourceNotFoundError => e
            metadata[:toolbox].output("Unable to retrieve source code")
          end
          return
        end

        if auto
          metadata[:toolbox].output("Skipping confirmation (auto): #{object.prompt}")
          # Not sure if the default should be execute or don't execute under auto
          result = true
        else
          result = metadata[:toolbox].yes?(object.prompt)
        end

        return unless result
        next_index = metadata[:index] + 1
        parent_items = object.parent.items
        remaining_items = parent_items.slice!(next_index..-1)
        object.parent.dsl.instance_exec(object, metadata, &object.block)
        parent_items[next_index..-1].each { |item| item.dynamic! }
        parent_items.push(*remaining_items)
      end
    end
    Runbook::Runs::SSHKit.singleton_class.prepend(OptionalStepRun)
  end
end

Remember to include this in your Runbookfile or equivalent config file.

This will allow you to execute a runbook like the following:

runbook = Runbook.book "Test Optional Step" do
  section "Test Optional Step" do
    step "Test me" do
      optional_step "Exec the following?" do
        puts "I've been executed"
      end
    end
  end
end

One of the main use cases for the ruby_command is a one-off way to define your own statements. The block has access to the ruby_command object and the metadata. This allows you to do pretty much anything with it that you can when defining your own statements. For example, you can do the following:

ruby_command do |rb_cmd, metadata|
  prompt = metadata[:toolbox].prompt
  if metadata[:auto] || prompt.yes?('Install External DNS Chart?')
  # ...
end