Skip to content

Commit

Permalink
WIP! Refactoring to make this easier to test
Browse files Browse the repository at this point in the history
  • Loading branch information
micahlee committed Feb 7, 2023
1 parent b3018b3 commit 904c0c8
Show file tree
Hide file tree
Showing 14 changed files with 604 additions and 198 deletions.
117 changes: 0 additions & 117 deletions pkg/checks/disk/fio.go

This file was deleted.

19 changes: 19 additions & 0 deletions pkg/checks/disk/fio/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fio

type Command interface {
Run() (Result, error)
}

type ConcreteCommand struct {
args []string
}

func NewCommand(args ...string) Command {
return &ConcreteCommand{
args: args,
}
}

func (*ConcreteCommand) Run() (Result, error) {
return Result{}, nil
}
149 changes: 149 additions & 0 deletions pkg/checks/disk/fio/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package fio

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"

"github.com/conjurinc/conjur-preflight/pkg/log"
"github.com/conjurinc/conjur-preflight/pkg/maybe"
)

const fioExecutable = "fio"

type commandWrapper struct {
command *exec.Cmd
stdout bytes.Buffer
stderr bytes.Buffer
}

func newCommandWrapper(name string, args ...string) *commandWrapper {
wrapper := commandWrapper{}

// Instantiate the command
command := exec.Command(name, args...)

// Bind the stdout and stderr
command.Stdout = &wrapper.stdout
command.Stderr = &wrapper.stderr

// Wrap the command
wrapper.command = command

return &wrapper
}

func (wrapper *commandWrapper) Run() ([]byte, error) {
err := wrapper.command.Run()

if err != nil {
log.Error("Failed to run command: %s", err)
return nil, newErrorFromStderr(&wrapper.stderr)
}

return wrapper.stdout.Bytes(), nil
}

func Exec(
jobName string,
args []string,
) (*Result, error) {
// Create the directory for running the fio test within
cleanup, err := usingTestDirectory(jobName)
if err != nil {
return nil, fmt.Errorf("unable to create test directory: %s", err)
}
defer cleanup()

// Run the fio command
maybeOutput := maybeJobOutput(args)
maybeWriteResultToFile(maybeOutput, jobName)
return maybeResultFromJson(maybeOutput).ValueE()
}

func maybeJobOutput(args []string) maybe.Maybe[[]byte] {
return maybe.Bind(
maybe.Bind(
maybe.Result(exec.LookPath(fioExecutable)),
func(fioPath string) (*commandWrapper, error) {
return newCommandWrapper(fioPath, args...), nil
},
),
func(commandWrapper *commandWrapper) ([]byte, error) {
return commandWrapper.Run()
},
)
}

func usingTestDirectory(jobName string) (func(), error) {
err := os.MkdirAll(jobName, os.ModePerm)
if err != nil {
return nil, err
}

return func() {
err := os.RemoveAll(jobName)
if err != nil {
log.Warn("Unable to clean up test directory for job: %s", jobName)
}
}, nil
}

func maybeWriteResultToFile(maybeBuffer maybe.Maybe[[]byte], jobName string) {
// Attempt to write the full fio result to a file
maybe.BindVoid(
maybeBuffer,
func(resultBytes []byte) error {
return writeResultToFile(resultBytes, jobName)
},
)
}

func writeResultToFile(buffer []byte, jobName string) error {

outputFilename := fmt.Sprintf("%s.json", jobName)

err := os.WriteFile(outputFilename, buffer, 0644)

if err != nil {
log.Warn("Failed to write result file for %s: %s", jobName, err)
}

return err
}

func maybeResultFromJson(maybeBuffer maybe.Maybe[[]byte]) maybe.Maybe[*Result] {
return maybe.Bind(
maybeBuffer,
func(resultBytes []byte) (*Result, error) {
return newResultFromJson(resultBytes)
},
)
}

func newResultFromJson(buffer []byte) (*Result, error) {
result := Result{}

err := json.Unmarshal(buffer, &result)
if err != nil {
return nil, err
}

return &result, nil
}

func newErrorFromStderr(buffer *bytes.Buffer) error {
str := buffer.String()

// Trim any extra space at the start or end (e.g. trailing newline)
str = strings.TrimSpace(str)

// Convert multi-line response into comma separated
str = strings.ReplaceAll(str, "\n", ", ")

return errors.New(str)
}
45 changes: 45 additions & 0 deletions pkg/checks/disk/fio/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package fio

// Result is a data structure that represents the JSON
// output returned from running `fio`.
type Result struct {
Version string `json:"fio version"`
Jobs []JobResult `json:"jobs"`
}

// JobResult represents the results from an individual fio job. A FioResult
// may include multiple job results.
type JobResult struct {
Sync JobModeResult `json:"sync"`
Read JobModeResult `json:"read"`
Write JobModeResult `json:"write"`
}

// JobModeResult represents the measurements for a given test mode
// (e.g. read, write). Not all modes provide all values. The populated
// values depend on the fio job parameters.
type JobModeResult struct {
Iops float64 `json:"iops"`
IopsMin int64 `json:"iops_min"`
IopsMax int64 `json:"iops_max"`
IopsMean float64 `json:"iops_mean"`
IopsStddev float64 `json:"iops_stddev"`

LatNs ResultStats `json:"lat_ns"`
}

// ResultStats represents the statistical measurements provided by fio.
type ResultStats struct {
Min int64 `json:"min"`
Max int64 `json:"max"`
Mean float64 `json:"mean"`
StdDev float64 `json:"stddev"`
N int64 `json:"N"`
Percentile Percentile `json:"percentile"`
}

// Percentile provides a simple interface to return particular statistical
// percentiles from the fio stats results.
type Percentile struct {
NinetyNinth int64 `json:"99.000000"`
}
Loading

0 comments on commit 904c0c8

Please sign in to comment.