Skip to content

Brew#

The Brew behavior is a basic set of interfaces for writing idempotent actions that bring a system or subcomponent to a desired state.

It primary use case is persistent process supervsion, although it can be used for short-lived actions as well.

It exposes a CLI that can be compiled to run commands that concurrently orchestrate actions.

Each task represents a single resource that belongs to a project.

An interface is provided to write classes that represent a idempotent action against a resource.
These actions can be provided to a task via Barista::Behaviors::Brew::TaskClassMethods#actions(*)

Project#

Like other Barista Behaviors, a Project is used for orchestrating a collection of tasks.

A Barista::Behaviors::Brew::Project is bootstrapped with a CLI designed for orchestrating a single action against a graph per invocation.

It exposes methods for specifying

  • which commands use an inverted dependency graph (used for reversing a change to a system)
  • recipes that can perform a set of actions in a single command
  • configuration of where process pids and process logs should live

Task#

A Brew task is for representing a singular resource which can alternate between different desired states.

Tasks can have dependencies on other tasks, and commands will attempt to cascade across all tasks. They also manage process supervision handling should an action use it.

Info

If a task depends on another task, executing an action will attempt to execute the same action on it's upstreams first

This is desireable to ensure the proper state for the target task to run.

This behavior can also be inverted

Actions#

An Action represents an idempotent operation in the context of a task.

To create one, we can implement Barista::Behaviors::Brew::Action

The interface specifies that we need 2 methods

  • action#skip? returns a boolean indicating whether or not this operation should be skipped.
  • action#ready? returns a boolean indiciation if this action is fully complete

Note

action#ready? will be run inside of a wait condition that rescues errors until the method returns true or the wait condition times out.

and since this is a Barista::Task it also requires an execute method. #execute is invoked to perform the action.

Reversible Actions#

Since tasks can depend on each other, upstreams dependencies will be executed first. This is great to get to a desired state, but what about reversing the desired state?

The #invert method on a project specifies which commands should use an inverted dependency graph.

  • When installing a collection of software, we want to install any upstreams first.
  • When uninstalling a collection of software, it usually makes sense to uninstall the downstreams firat.

Install/Uninstall Example#

For an example of a Brew project, we can consider installing and uninstalling asdf. In this example, asdf depends on homebrew.

  • When running the install command, postgres will be installed first, and then asdf
  • When running the uninstall command, asdf will be removed first and then postgres
require "./src/barista"

class SoftwareProject < Barista::Project
  include_behavior(Brew)

  invert("uninstall")

  recipe("reinstall") do
    action("uninstall")
    action("install")
  end

  def initialize
    process_dir("./me/process")
    log_dir("./me/log")
  end
end

module Homebrew 
  @[Barista::BelongsTo(SoftwareProject)]
  class Task < Barista::Task
    include_behavior(Brew)

    @@name = "homebrew"

    actions(Install, Uninstall)
  end

  class Install < Barista::Behaviors::Brew::Action
    @@name = "install"

    def skip? : Bool
      success?("which brew")
    end

    def ready? : Bool
      skip?
    end

    def execute
      command("/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"", env: { "NONINTERACTIVE" => "1" })
      patch_bash_profile_if_needed
    end

    def patch_bash_profile_if_needed
      user = run("whoami").output
      profile = run("cat /home/#{user}/.bash_profile").output

      return if profile.includes?("Homebrew")

      run("echo '# Set PATH, MANPATH, etc., for Homebrew.' >> /home/#{user}/.bash_profile")
      run("echo 'eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"' >> /home/#{user}/.bash_profile")
      run("eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"")
    end
  end

  class Uninstall < Barista::Behaviors::Brew::Action
    @@name = "uninstall"

    def skip? : Bool
      !success?("which brew")
    end

    def ready? : Bool
      skip?
    end

    def execute
      command("/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)\"", env: { "NONINTERACTIVE" => "1" })
    end
  end
end

module Asdf
  @[Barista::BelongsTo(SoftwareProject)]
  class Task < Barista::Task
    include_behavior(Brew)

    dependency Homebrew::Task

    @@name = "asdf"

    actions(Install, Uninstall)
  end

  class Install < Barista::Behaviors::Brew::Action
    @@name = "install"

    def skip? : Bool
      success?("brew list asdf")
    end

    def ready? : Bool
      skip?
    end

    def execute
      command("brew install asdf")
    end
  end

  class Uninstall < Barista::Behaviors::Brew::Action
    @@name = "uninstall"

    def skip? : Bool
      !success?("brew list asdf")
    end

    def ready? : Bool
      skip?
    end

    def execute
      command("brew uninstall --force asdf")
      command("brew autoremove")
    end
  end
end

SoftwareProject
  .new
  .default_output
  .console_application
  .run