Skip to content

danslimmon/donothing

Repository files navigation

donothing

donothing is a Go framework for do-nothing scripting. Do-nothing scripting is an approach to writing procedures. It allows you to start with a documented manual process and gradually make it better by automating a step at a time. Do-nothing scripting aims to minimize the activation energy for automating steps of a manual procedure.

A donothing script walks through a procedure, which comprises a sequence of steps. As an example, here's a do-nothing procedure for restoring a database backup:

package main

import (
  "github.com/danslimmon/donothing"
)

func main() {
  pcd := donothing.NewProcedure()
  pcd.Short(`Restore a database backup`)
  pcd.Long(`
    This is our procedure for restoring a database backup. To familiarize yourself with our database
    setup, see [the database docs](https://example.com/docs/database.html).
  `)

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("retrieveBackupFile")
    step.Short("Retrieve the backup file")
    step.Long(`
      Log in to the [storage control panel](https://example.com/storage/) and locate the latest file
      of the form "backup_YYYYMMDD.sql". Download that file to your workstation.
    `)
  })

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("loadBackupData")
    step.Short("Load the backup data into the database")
    step.Long(`
      Run this command to load the backup data into the database:

          psql < backup_YYYYMMDD.sql
    `)
  })

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("testRestoredData")
    step.Short("Check the restored data for consistency")
    step.Long(`
      Log in to the database and make sure there are recent records in the events table.
    `)
  })

  pcd.Execute()
}

When this code is run, the user will be prompted to follow the instructions step by step:

# Restore a database backup

This is our procedure for restoring a database backup. To familiarize yourself with our database
setup, see [the database docs](https://example.com/docs/database.html).

[Enter] to begin:

## Retrieve the backup file

Log in to the [storage control panel](https://example.com/storage/) and locate the latest file
of the form "backup_YYYYMMDD.sql". Download that file to your workstation.

[Enter] when done:

## Load the backup data into the database

Run this command to load the backup data into the database:

    psql < backup_YYYYMMDD.sql

[Enter] when done:

## Check the restored data for consistency

Log in to the database and make sure there are recent records in the events table.

[Enter] when done:

Done!

The main idea behind donothing is that, when you're ready to automate a step instead of performing it manually, you can just add a Run() function to the step. Continuing with the example above, suppose we write a retrieveBackupFile function that downloads the latest database backup to our working directory. We can then automate the first step of our procedure:

// retrieveBackupFile downloads the latest backup file from the S3 bucket.
//
// It returns the path to the local file containing the backup data.
func downloadBackupFile() (string, error) {
  // ... use the AWS API to download the latest backup file ...
  return filename, nil
}

func main() {
  // ...
  pcd.AddStep(func(step *donothing.Step) {
    step.Name("retrieveBackupFile")
    step.Short("Retrieve the backup file")
    step.HasStringOutput("backupFilePath")

    step.Run(func(inputs *donothing.Inputs) (*donothing.Outputs, error) {
      filename, err := downloadBackupFile()
      if err != nil {
        return nil, err
      }
      return &donothing.Outputs{"backupFilePath": filename}, nil
    }
  })
  // ...
}

Note that the retrieveBackupFile step's Long() call has been removed, since it's obsoleted by the automation provided by Run(). A step can have both Long() and Run(), but in this case it doesn't need to.

Now when we run our script, it will automatically download the backup file from S3, allowing us to move on to the second step immediately:

# Restore a database backup

This is our procedure for restoring a database backup. To familiarize yourself with our database
setup, see [the database docs](https://example.com/docs/database.html).

[Enter] to begin:

## Retrieve the backup file

Executing step `retrieveBackupFile` automatically.

**Outputs**:
  - `backupFilePath`: ./backup_20200226.sql

## Load the backup data into the database

Run this command to load the backup data into the database:

    psql < backup_YYYYMMDD.sql

[Enter] when done:

This paradigm makes it easy to automate the restore procedure piece by piece.

Inputs and outputs

Sometimes we need to pass information from one step to another. We can do this with step inputs and outputs. Continuing with the database restore example, we can have the first step pass the name of our backup file to the second step, so that the second step can print it.

First, we modify the long description of the loadBackupData step so that it contains new, templated instructions.

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("loadBackupData")
    step.Short("Load the backup data into the database")
    step.Long(`
      Run this command to load the backup data into the database:

          psql < {{.Input "backupFilePath"}}
    `)
  })

With no further changes, our main function now looks like this:

func main() {
  pcd := donothing.NewProcedure()
  pcd.Short(`Restore a database backup`)
  pcd.Long(`
    This is our procedure for restoring a database backup. To familiarize yourself with our database
    setup, see [the database docs](https://example.com/docs/database.html).
  `)

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("retrieveBackupFile")
    step.Short("Retrieve the backup file")
    step.HasStringOutput("backupFilePath")

    step.Run(func(inputs *donothing.Inputs) (*donothing.Outputs, error) {
      filename, err := downloadBackupFile()
      if err != nil {
        return err
      }
      step.OutputString("backupFilePath", filename)
    }
  })

  pcd.AddStep(func(pcd donothing.Procedure) {
    step.Name("loadBackupData")
    step.Short("Load the backup data into the database")
    step.Long(`
      Run this command to load the backup data into the database:

          psql < {{.Input "backupFilePath"}}
    `)
  })

  // ... further steps
}

The output from our script will now be:

# Restore a database backup

This is our procedure for restoring a database backup. To familiarize yourself with our database
setup, see [the database docs](https://example.com/docs/database.html).

[Enter] to begin:

## Retrieve the backup file

Executing step `retrieveBackupFile` automatically.

**Outputs**:
  - `backupFilePath`: ./backup_20200226.sql

## Load the backup data into the database

Run this command to load the backup data into the database:

    psql < ./backup_20200226.sql

[Enter] when done:
...

Now the user doesn't have to construct their own command for the loadBackupData step: they can just copy and paste the command they need to run. And when it comes time to automate the loadBackupData step as well, our new Run function can use the backupFilePath input:

func main() {
  // ...

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("retrieveBackupFile")
    step.Short("Retrieve the backup file")
    step.HasStringOutput("backupFilePath")

    step.Run(func(inputs *donothing.Inputs) (*donothing.Outputs, error) {
      filename, err := downloadBackupFile()
      if err != nil {
        return nil, err
      }
      return &donothing.Outputs{"backupFilePath": filename}, nil
    }
  })

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("loadBackupData")
    step.Short("Load the backup data into the database")
    // This step has a required string input called `backupFilePath`
    step.HasStringInput("backupFilePath", true)

    step.Run(func(inputs *donothing.Inputs) (*donothing.Outputs, error) {
      backupFilePath, _ := inputs.GetString("backupFilePath")
      err := loadBackupData(backupFilePath)
      if err != nil {
        return err
      }
      fmt.Println("Data loaded successfully.")
      return nil, nil
    }
  })

  // ...
}

The output from our script will now look like this:

# Restore a database backup

This is our procedure for restoring a database backup. To familiarize yourself with our database
setup, see [the database docs](https://example.com/docs/database.html).

[Enter] to begin:

## Retrieve the backup file

Executing step `retrieveBackupFile` automatically.

**Outputs**:
  - `backupFilePath`: ./backup_20200226.sql

## Load the backup data into the database

Executing step `loadData` automatically.

Data loaded successfully.

## Check the restored data for consistency

Log in to the database and make sure there are recent records in the events table.

[Enter] when done:

Generating procedure documentation

donothing can print Markdown documentation for a procedure. Going back to our original, non-automated database restore example, let's add a --print flag to our script:

package main

import (
  "os"
  "github.com/danslimmon/donothing"
)

func main() {
  pcd := donothing.NewProcedure()
  pcd.Short(`Restore a database backup`)
  pcd.Long(`
    This is our procedure for restoring a database backup. To familiarize yourself with our database
    setup, see [the database docs](https://example.com/docs/database.html).
  `)

  pcd.AddStep(func(step *donothing.Step) {
    step.Name("retrieveBackupFile")
    step.Short("Retrieve the backup file")
  })
  step.AddStep(func(step *donothing.Step) {
    step.Name("loadBackupData")
    step.Short("Load the backup data into the database")
  })
  pcd.AddStep(func(step *donothing.Step) {
    step.Name("testRestoredData")
    step.Short("Check the restored data for consistency")
  })

  if len(os.Args) > 0 && os.Args[1] == "--print" {
    pcd.Render()
  } else {
    pcd.Execute()
  }
}

When we invoke our script with the --print flag, it will print out our whole procedure as Markdown:

# Restore a database backup

This is our procedure for restoring a database backup. To familiarize yourself with our database
setup, see [the database docs](https://example.com/docs/database.html).

## Retrieve the backup file

Log in to the [storage control panel](https://example.com/storage/) and locate the latest file
of the form "backup_YYYYMMDD.sql". Download that file to your workstation.

## Load the backup data into the database

Run this command to load the backup data into the database:

    psql < backup_YYYYMMDD.sql

## Check the restored data for consistency

Log in to the database and make sure there are recent records in the events table.

About

do-nothing scripting framework

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages