Skip to content

Commit

Permalink
Add checks package for runtime condition verification
Browse files Browse the repository at this point in the history
Summary:
This feature will power verification of:

* TTP Prerequisites (right tools being installed, running in correct environment, etc)
* Verification that individual steps return the correct results

I'm adding it as an isolated package for now, in a subsequent diff I will hook it into `step.go` like this:

```
// CommonStepFields contains the fields
// common to every type of step (such as Name).
// It centralizes validation to simplify the code
type CommonStepFields struct {
	Name        string         `yaml:"name,omitempty"`
	Description string         `yaml:"description,omitempty"`
	Checks      []checks.Check `yaml:"checks,omitempty"`
```

Note that the initial diff only

Reviewed By: cedowens

Differential Revision: D51428503

fbshipit-source-id: 78b873cf65082328c97a2a4cc1474930415da18c
  • Loading branch information
d3sch41n authored and facebook-github-bot committed Nov 17, 2023
1 parent d05be95 commit 4011cf1
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 0 deletions.
75 changes: 75 additions & 0 deletions pkg/checks/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package checks

import (
"errors"
"fmt"

"github.com/spf13/afero"
"gopkg.in/yaml.v3"
)

// CommonCheckFields are common fields across all check types
type CommonCheckFields struct {
Msg string `yaml:"msg"`
}

// Check is wrapper struct around a Condition.
// This wrapping setup is used so that we only
// need to implement the UnmarshalYAML method once (in Check)
// instead of having to implement it in each individual condition type.
// This is similar to what we do with ParseAction
// for decoding the actions associated with steps.
type Check struct {
CommonCheckFields
condition Condition
}

// Verify wraps the Verify method from the underlying condition
func (c *Check) Verify(ctx VerificationContext) error {
if ctx.FileSystem == nil {
ctx.FileSystem = afero.NewOsFs()
}
return c.condition.Verify(ctx)
}

// UnmarshalYAML implements custom deserialization
// process to ensure that the check is decoded
// into the correct struct type
func (c *Check) UnmarshalYAML(node *yaml.Node) error {

// Decode all of the shared fields.
// Use of this auxiliary type prevents infinite recursion
var ccf CommonCheckFields
err := node.Decode(&ccf)
if err != nil {
return err
}
c.CommonCheckFields = ccf

if c.Msg == "" {
return errors.New("no msg specified for check")
}

candidateTypeInstances := []Condition{
&PathExists{},
}
for _, candidateTypeInstance := range candidateTypeInstances {
err := node.Decode(candidateTypeInstance)
if err == nil {
if c.condition != nil {
// Must catch conditions with ambiguous types, such as:
// - path_exists: foo
// command_succeeds: bar
//
// This is a problem because we can't tell into
// which concrete type we should decode
return fmt.Errorf("check %q has ambiguous type", c.Msg)
}
c.condition = candidateTypeInstance
}
}
if c.condition == nil {
return fmt.Errorf("condition with msg %q did not match any valid condition type", c.Msg)
}
return nil
}
87 changes: 87 additions & 0 deletions pkg/checks/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package checks

import (
"testing"

"github.com/facebookincubator/ttpforge/pkg/testutils"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestCheckVerify(t *testing.T) {

testCases := []struct {
name string
contentStr string
fsysContents map[string][]byte
expectUnmarshalError bool
expectVerifyError bool
}{
{
name: "Check if Regular File Exists (Yes)",
contentStr: `msg: File does not exist,
path_exists: should-exist.txt`,
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")},
},
{
name: "Check if Regular File Exists (No)",
contentStr: `msg: File does not exist,
path_exists: does-not-exist.txt`,
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")},
expectVerifyError: true,
},
{
name: "path_exists + Checksum Verification (Success)",
contentStr: `msg: File does not exists or does not have expected content,
path_exists: should-exist.txt
checksum:
sha256: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae`,
fsysContents: map[string][]byte{"should-exist.txt": []byte("foo")},
expectUnmarshalError: false,
expectVerifyError: false,
},
{
name: "Check if Checksum is Correct (Yes)",
contentStr: `msg: File does not exists or does not have expected content,
path_exists: has-correct-hash.txt
checksum:
sha256: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae`,
fsysContents: map[string][]byte{"has-correct-hash.txt": []byte("foo")},
},
{
name: "Check if Checksum is Correct (Yes)",
contentStr: `msg: File does not exists or does not have expected content,
path_exists: incorrect-hash.txt
checksum:
sha256: "absolutely wrong"`,
fsysContents: map[string][]byte{"incorrect-hash.txt": []byte("foo")},
expectVerifyError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// prep filesystem
fsys, err := testutils.MakeAferoTestFs(tc.fsysContents)
require.NoError(t, err)

// decode the check
var check Check
err = yaml.Unmarshal([]byte(tc.contentStr), &check)
if tc.expectUnmarshalError {
require.Error(t, err)
return
}
require.NoError(t, err)

// run verification
err = check.Verify(VerificationContext{FileSystem: fsys})
if tc.expectVerifyError {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}

}
27 changes: 27 additions & 0 deletions pkg/checks/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package checks

import (
"crypto/sha256"
"fmt"
)

// Checksum is a struct that contains different types
// of checksums against which a file can be verified.
// Right now we just support SHA256 checksums, but in the future
// others such as MD5 can be added if needed.
type Checksum struct {
SHA256 string `yaml:"sha256"`
}

// Verify computes the checksum of the contents
// and compares it to the expected value
func (c *Checksum) Verify(contents []byte) error {
if c.SHA256 == "" {
return fmt.Errorf("Checksum is empty")
}
rawResult := sha256.Sum256(contents)
if fmt.Sprintf("%x", rawResult) != c.SHA256 {
return fmt.Errorf("contents do not match checksum")
}
return nil
}
7 changes: 7 additions & 0 deletions pkg/checks/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package checks

// Condition is the common interface
// implemented by all condition types
type Condition interface {
Verify(ctx VerificationContext) error
}
12 changes: 12 additions & 0 deletions pkg/checks/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package checks

import (
"github.com/spf13/afero"
)

// VerificationContext contains contextual
// information required to verify conditions
// of various types
type VerificationContext struct {
FileSystem afero.Fs
}
38 changes: 38 additions & 0 deletions pkg/checks/pathexists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package checks

import (
"fmt"

"github.com/spf13/afero"
)

// PathExists is a condition that verifies that a file exists at a given path
// It can also verify the contents of the file against a checksum
type PathExists struct {
Path string `yaml:"path_exists"`
Checksum *Checksum `yaml:"checksum"`
}

// Verify checks the condition and returns an error if it fails
func (c *PathExists) Verify(ctx VerificationContext) error {
fsys := ctx.FileSystem

// basic existence check
exists, err := afero.Exists(fsys, c.Path)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("file %q does not exist", c.Path)
}

// verify the checksum if provided
if c.Checksum != nil {
contentBytes, err := afero.ReadFile(fsys, c.Path)
if err != nil {
return err
}
return c.Checksum.Verify(contentBytes)
}
return nil
}

0 comments on commit 4011cf1

Please sign in to comment.