Skip to main content

Developer edition

The actions.rb in the project Git repository is the core of an OpsChain change. After following this guide you should understand:

  • how to add actions to the actions.rb file for use in a change
  • how to add resource types and resources to the actions.rb file for use in a change
  • how to create a simple controller to support a resource type

Prerequisites

If you have not already done so, we suggest completing the main getting started guide before this guide.

This guide assumes that:

Create a Git repository

OpsChain projects can use remote Git repositories to centrally manage configuration.

Create a new Git repository for this guide:

mkdir opschain-git-repo
cd opschain-git-repo
git init

This guide uses an existing repository that already contains some sample content. Fork the sample repo on GitHub and use your own fork to allow you to push your changes and use them from your OpsChain project.

git remote add origin https://{github username}:{github personal access token}@github.com/{github username}/opschain-getting-started.git
git fetch
git checkout developer-guide

Repository setup

All OpsChain project Git repositories must contain a Gemfile and an actions.rb.

$ tree
├── Gemfile
└── actions.rb

By using the existing sample repository these files have already been created - but with normal repositories they will need to be created manually.

Running OpsChain actions

OpsChain changes run actions from a project's Git repository.

OpsChain actions can be developed interactively in the OpsChain development environment, accessed via the opschain dev CLI subcommand.

tip

Throughout the documentation, the following prefixes for bash commands will be used to denote where the command should be run.

  • [host] $ execute the command on your local host
  • [dev] $ execute the command inside the OpsChain development environment

Developing actions locally

Start the OpsChain development environment:

# From your project repository folder
[host] $ opschain dev

The OpsChain development environment opens a Bash prompt within an OpsChain runner container. You can now use the opschain-action utility to list the actions available within the current project Git repository:

[dev] $ opschain-action -AT # this will list all actions - use `opschain-action -T` to show only actions with a description

The sample branch we checked out earlier has an actions.rb file in the repository that contains a single action, hello_world.

You can run this action in the development environment by using the opschain-action command as follows:

[dev] $ opschain-action hello_world
Hello world
tip

Once an action is ready, the opschain change create command should be used to execute it via the OpsChain server to gain the collaboration and auditing benefits that OpsChain provides. This also allows the change to run with secure network access that can be granted to the OpsChain server, without giving that network access directly to developers.

Adding a new action

Open the actions.rb file with your favourite editor so that you can add a new action to the Git repository.

Add the following to the bottom of the file (after the hello_world action):

desc 'Say goodbye world' # if this line were omitted then this action would not be shown in the tasks displayed by `opschain-action -T`
action :goodbye_world do
puts 'Goodbye world' # you could write any Ruby in here, but OpsChain provides a friendlier API in addition to this
end

You can now manually run the new goodbye_world task in addition to the existing hello_world task:

[dev] $ opschain-action hello_world goodbye_world
Hello world
Goodbye world

Add the following to the actions.rb file to configure the project to run both of these actions as the default action (i.e. when you don't specify which action to run):

action default: [:hello_world, :goodbye_world]

You can now run the default action:

[dev] $ opschain-action
Hello world
Goodbye world

Leveraging OpsChain steps

Splitting OpsChain actions into steps allows OpsChain to:

  • isolate the step execution - to avoid concurrency conflicts and improve security
  • report on the progress of a change

To help your changes complete faster, steps can also run in parallel - we'll cover this later.

Steps are run in isolated runner containers when run as part of an OpsChain change.

Edit the actions.rb file to make the default action run it's dependent actions as steps:

action :default, steps: [:hello_world, :goodbye_world]

Child steps are always run automatically when running a change, however to automatically run these child steps when running them in the OpsChain development environment the OPSCHAIN_ACTION_RUN_CHILDREN environment variable must be set to true:

[dev] $ opschain-action
2021-01-01 12:05:00.000+1000 WARNING: Child steps (hello_world, goodbye_world) will not be executed - set OPSCHAIN_ACTION_RUN_CHILDREN to run locally.
[dev] $ export OPSCHAIN_ACTION_RUN_CHILDREN=true
[dev] $ opschain-action
Hello world
Goodbye world
tip

Add export OPSCHAIN_ACTION_RUN_CHILDREN=true to your host's shell configuration (e.g. ~/.zshrc) to avoid needing to set it each time you start the development environment.

OpsChain lint pre-commit hook

OpsChain provides a linting command for detecting issues in project Git repositories.

This command is automatically setup as a pre-commit hook for project Git repositories created by OpsChain.

If you would like to commit code that fails linting (e.g. incomplete code) the Git --no-verify argument can be used when committing, e.g. git commit --no-verify.

See the OpsChain lint documentation to learn more.

Commit your action

Commit the changes to the actions.rb file to allow them to be used via the OpsChain server:

[host] $ git add actions.rb
[host] $ git commit -m 'Add a goodbye action and run hello_world and goodbye_world by default.'

Running the action as a change (optional)

Now that you've developed and tested your actions, use the OpsChain server to run them as part of a change. This facilitates collaboration and record keeping, and can also be done to improve security by only executing changes in a secure environment.

This step assumes you have completed the running sample changes steps from the getting started guide - alternatively you could create a new project and environment to run the change in.

Push your commit to the remote

Push your new Git commit to the Git repository on GitHub for use by your project Git repository:

[host] $ git push origin HEAD:hello-goodbye

Add the project Git remote

Associate your Git repository with the web project created during the getting started guide.

# Note: to avoid potentially storing the repository credentials in the shell history the `-U` (user) and `-P` (password) arguments can be omitted and filled in when prompted
# Option 1: Using password authentication:
$ opschain project add-git-remote -p <project code> -n origin -U '{username}' -P '{password / personal access token}' -u 'https://github.com/{username}/opschain-getting-started.git'
# Option 2: Using SSH authentication:
$ opschain project add-git-remote -p <project code> -n origin -s ./path/to/private/key -u 'git@github.com:{username}/opschain-getting-started.git'

Run the change

Use the OpsChain CLI to run the change using the OpsChain server. This will run the new steps in isolated containers and will report on the status of each step as it progresses.

opschain change create -p web -e test -G origin -g hello-goodbye -a '' -y # -a '' is a synonym for -a 'default'

Use the opschain change show-logs command in another terminal to see the latest log output whilst the change is still executing, or wait until the change completes:

opschain change show-logs -c xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # the change ID from the change create output

Developing resources

OpsChain resources and resource types are features of the actions.rb file that make your configuration easier to follow and more reusable.

Using resources is very simple, here is an example temp_file resource using a non-existent file resource type to demonstrate:

# this won't work, yet
file :temp_file do
path '/tmp/testing'
content 'Hello :-)'
end

This temp_file resource configures two properties - path and content. These would need to be supported by the file resource type.

Resources can define actions, for example you could define a create action as part of this resource:

# this won't work, yet
file :temp_file do
path '/tmp/testing'
content 'Hello :-)'

action :create do
OpsChain.logger.info 'Lets create a file.'
end
end

With a working resource type (which we haven't created yet), you could run this action using opschain-action temp_file:create - note how this uses the resource name and the action name.

Resources are instances of resource types. The resource type is the backing definition of the resource.

A basic file resource type for the temp_file resource above could be:

resource_type :file do
property :path
property :content
end

If we assume all file resources can be created the same way, the create action can be moved from the temp_file resource to the file resource type - this allows it to be reused. Replace the contents of the sample actions.rb with the following to demonstrate this:

Bundler.require

resource_type :file do
property :path
property :content

action :create do
OpsChain.logger.info 'Lets create a file.'
end
end

file :temp_file do
path '/tmp/testing'
content 'Hello :-)'
end

Now run the temp_file:create command:

[dev] $ opschain-action temp_file:create
2021-01-01 12:05:00.000+1000 Lets create a file.

As you can see, this has run the Ruby code in the create action. Inside the resource type, modify the action definition to create the file:

action :create do
OpsChain.logger.info "Lets create a file: #{path}"
File.write(path, content)
end

Run the temp_file:create command again:

[dev] $ opschain-action temp_file:create
2021-01-01 12:05:00.000+1000 Lets create a file: /tmp/testing
[dev] $ cat /tmp/testing
Hello :-)

Creating files in the short-lived OpsChain development container isn't the most useful - lets make the host where the resource type will create the file configurable. OpsChain provides some tools to make this more convenient.

Add the opschain-resource-types Gem to your Gemfile (see the included resource types guide to learn more about this Gem):

# The following Gems are pre-installed on the OpsChain runner image
gem 'opschain-core', require: 'opschain'
# the require below automatically requires `opschain-infrastructure` (rather than doing it manually in the actions.rb)
# `require:` uses an array because `opschain-resource-types` includes many paths that can be required, and in the future more could be required here
gem 'opschain-resource-types', require: ['opschain-infrastructure']

To load the new Gem:

[dev] $ rm -f Gemfile.lock; bundle install

Update your actions.rb with the following:

Bundler.require

resource_type :file do
property :path
property :content
property :host

host MintPress::Infrastructure::Localhost.new

action :create do
OpsChain.logger.info "Lets create a file: #{path}"
host.transport.File.write(path, content)
end
end

file :temp_file do
path '/tmp/testing'
content 'Hello :-)'
end

Run the temp_file action again:

[dev] $ rm -f /tmp/testing
[dev] $ export MINTPRESS_LOG_LEVEL=error # hide the detailed logging - it's not necessary for this guide
[dev] $ opschain-action temp_file:create
2021-01-01 12:05:00.000+1000 Lets create a file: /tmp/testing
[dev] $ cat /tmp/testing
Hello :-)

So far, nothing has changed - the file has still been created locally - but there is now a host property on the resource type (which is defaulted to localhost).

Add a local infrastructure_host resource to leverage that property:

infrastructure_host :test_host do
protocol 'local'
end

file :temp_file do
host test_host
path '/tmp/testing'
content 'Hello :-)'
end

Again, using the temp_file:create action will do this locally.

To show this code working with a remote host, the repository includes a docker-compose.yml file to create an opschain-getting-started Docker network, with a target host that we can treat like a remote host:

[host] $ docker-compose up -d

Once docker-compose completes, we need to add the OpsChain development environment container to the opschain-getting-started network so it can communicate with it.

[host] $ docker network connect opschain-getting-started "$(docker ps --filter 'label=opschain-dev' -q)"

Return to the terminal running the OpsChain development environment to perform the following steps.

Update the test_host resource to use the sample container:

infrastructure_host :test_host do
hostname 'target'
connect_user 'opschain'
password 'password'
end

Run the action again:

[dev] $ rm -f /tmp/testing
[dev] $ opschain-action temp_file:create
2021-01-01 12:05:00.000+1000 Lets create a file: /tmp/testing
[dev] $ cat /tmp/testing
cat: /tmp/testing: No such file or directory

Now the file does not exist locally as it has been created on the remote host.

In a new terminal verify that the new file exists:

$ docker exec target cat /tmp/testing
Hello :-)

This example leverages the MintPress InfrastructureHost and Transport classes which can transparently execute code on remote or local hosts. View the MintPress documentation for these classes to learn more about the powerful functions they provide - if you need credentials to access this documentation contact OpsChain support.

To complete the new file resource example, update the contents of your actions.rb as follows:

Bundler.require

infrastructure_host :test_host do
hostname 'target'
connect_user 'opschain'
# tip: use OpsChain database properties for credentials like passwords as they are stored securely
password 'password'
end

resource_type :file do
property :path
property :content
property :host

host test_host # NOTE we've made test_host the default, but we can override it in a resource if we desire

action :create do
OpsChain.logger.info "Lets create a file: #{path}"
host.transport.File.write(path, content)
end
end

file :temp_file do
path '/tmp/testing'
content 'Hello :-)'
end

file :another_temp_file do
path '/tmp/testing2'
content 'Goodbye :-)'
end

desc 'Create sample files'
action :default, steps: ['temp_file:create', 'another_temp_file:create']

Running [dev] $ OPSCHAIN_ACTION_RUN_CHILDREN=true opschain-action now will create two files on the target host.

Moving the complexity to a reusable controller

In addition to creating files, the file resource type could also delete files.

As we are adding this extra complexity to the resource type, moving the Ruby code to a controller will simplify the resource and resource type. This simplifies the actions.rb and keeps it focussed on configuration rather than implementation.

Moving the code to a Ruby controller class also allows developers to add unit tests to ensure the code is tested and reliable.

Create a new file, lib/controllers/file_controller.rb, with the following contents:

class FileController
def initialize(opts)
@host, @path, @content = opts.values_at(:host, :path, :content)
end

def create
OpsChain.logger.info "Lets create a file: #{@path}"
@host.transport.File.write(@path, @content)
end
end

Modify the top of the actions.rb as follows:

Bundler.require
require_relative 'lib/controllers/file_controller'

Then, simplify the resource type:

resource_type :file do
controller FileController, action_methods: [:create]
property :path
property :content
property :host
host test_host # NOTE we've made test_host the default, but we can override it in a resource if we desire
end

Running [dev] $ opschain-action will now use the new controller to create two files.

Notice that the action_methods argument has specified that this controller's create method should be exposed as an action on the file resource type.

Update the contents of the controller to add a new delete method. Rather than updating the action_methods argument in the resource type, the class can implement a self.resource_type_actions method:

class FileController
def self.resource_type_actions
[:create, :delete]
end

def initialize(opts)
@host, @path, @content = opts.values_at(:host, :path, :content)
end

def create
OpsChain.logger.info "Lets create a file: #{@path}"
@host.transport.File.write(@path, @content)
end

def delete
OpsChain.logger.info "Deleting file: #{@path}"
@host.transport.File.delete(@path)
end
end

Update the resource type to use the self.resource_type_actions method by removing the action_methods configuration (action_methods overrides the controller resource_type_actions):

resource_type :file do
controller FileController
property :path
property :content
property :host
host test_host # NOTE we've made test_host the default, but we can override it in a resource if we desire
end

Listing the actions available using the opschain-action command now includes the new delete command:

[dev] $ opschain-action -AT temp_file: # adding the `temp_file:` here filters the output to only show actions that match this pattern
opschain-action another_temp_file:create #
opschain-action another_temp_file:delete #
opschain-action temp_file:create #
opschain-action temp_file:delete #

Try running the delete command to remove one of the files:

[dev] $ opschain-action temp_file:delete
2021-01-01 12:05:00.000+1000 Deleting file: /tmp/testing

To further simplify the resource type, implement a self.resource_type_properties method in the controller class:

class FileController
# add these at the top to make it easier for others to quickly see the properties and actions that are supported
def self.resource_type_properties
[:host, :path, :content]
end

...
end

Then simplify the resource type in the actions.rb:

info

Note that the order is important, if we swapped the following lines this would fail with NoMethodError: undefined method host' for #<OpsChain::Dsl::ResourceConfiguration:0x0000000003b15208>`

resource_type :file do
controller FileController
host test_host
end

To facilitate changes that remove the temporary files, add a clean action to the actions.rb file to remove these two files, working in parallel to improve the performance:

desc 'Remove sample files'
action :clean, steps: ['temp_file:delete', 'another_temp_file:delete'], run_as: :parallel

By adding descriptions to the core actions in the actions.rb they will be listed when running opschain-action -T - which only lists actions with a description:

[dev] $ opschain-action -T
opschain-action clean # Remove sample files
opschain-action default # Create sample files

Doing this is considered a best practice - especially in a team environment where it tells other team members about the key actions in a project Git repository.

Making the target host configurable

By defining the target host as an OpsChain property we allow the host to be overridden independently of the Git repository.

infrastructure_host :test_host do
properties OpsChain.properties.target_host
end

Lets create an in-repository set of default properties (these will be used if the project or environment doesn't provide overrides) - this is not mandatory in all project repositories, but is for this example:

[dev] $ mkdir -p .opschain
[dev] $ cat <<EOH > .opschain/properties.json
{
"target_host": {
"protocol": "local"
}
}
EOH

Here we've used JSON, but OpsChain also supports TOML and YAML as properties.toml and properties.yaml respectively.

Setting a remote target (optional)

To use the changes against a remote host, the project or environment properties would need to be updated to specify a remote host to target - by default it would just act locally due to the default properties.json in the repository.

An example properties file could be:

{
"target_host": {
"hostname": "my-server.example.com",
"connect_user": "opschain",
"password": "password"
}
}
note

The values would need to be updated to match a server that the OpsChain API could access.

Commit your updates

Commit the changes to the actions.rb and lib/controllers/file_controller.rb files to allow them to be used via the OpsChain server:

[dev] $ git add Gemfile actions.rb lib/controllers/file_controller.rb .opschain/properties.json
[dev] $ git commit -m 'Example creating and removing files.'

Once committed, the code can be pushed to the Git remote and can then be used as part of a change by following the same process as earlier.

A completed example

The sample repository includes the developer-guide-complete branch which is a completed example of this tutorial.

It includes the actions.rb with resources and actions, the file resource type, and the FileController with tests (in the spec directory).

Remove target container and opschain-getting-started network

note

Before running the command below, ensure you have exited the OpsChain development environment.

[host] $ docker-compose down

What to do next

Learn more about OpsChain actions

Read our more comprehensive actions reference guide to learn more about creating actions, resources, resource types and controllers.

Create OpsChain actions that need manual intervention

Read more about OpsChain wait steps to learn how to create changes that can pause and wait for human intervention before continuing.

Learn more about the OpsChain step runner

Read our more comprehensive step runner guide to learn more about how OpsChain steps are executed - and how to install custom commands and dependencies.

Learn more about OpsChain properties

Follow the properties guide to try editing some project or environment properties.

Try more advanced examples

The OpsChain examples include a variety of tutorials and Git repository samples for you to explore.